16

我正在处理来自政府来源(FEC、州选民数据库等)的数据。它的格式不一致,以各种令人愉快的方式破坏了我的 CSV 解析器。

它是外部来源和权威的。我必须解析它,并且我不能让它重新输入、输入验证等。就是这样; 我不控制输入。

特性:

  1. 字段包含格式错误的 UTF-8(例如Foo \xAB bar
  2. 行的第一个字段指定已知集合中的记录类型。知道了记录类型,你就知道有多少个字段以及它们各自的数据类型,但直到你知道。
  3. 文件中的任何给定行都可能使用带引号的字符串 ( "foo",123,"bar") 或不带引号的 ( foo,123,bar)。我还没有遇到任何它在给定行(即"foo",123,bar)中混合的地方,但它可能在那里。
  4. 字符串可能包括内部换行符、引号和/或逗号字符。
  5. 字符串可能包含逗号分隔的数字。
  6. 数据文件可能非常大(数百万行),因此仍需要相当快。

我正在使用 Ruby FasterCSV(在 1.9 中称为 CSV),但问题应该与语言无关。

我的猜测是,解决方案将需要使用明确的记录分隔符/引号字符(例如 ASCII RS、STX)进行预处理替换。我在这里开始了一点,但它不适用于我得到的所有东西。

如何稳健地处理这种脏数据?

ETA:这是单个文件中可能包含的内容的简化示例:

"this","is",123,"a","normal","line"
"line","with "an" internal","quote"
"短线","与
一个
“内部报价”,1个逗号和
换行符"
un "quot" ed,text,with,1,2,3,numbers
"引用","编号","系列","1,2,3"
“无效的 \xAB utf-8”
4

3 回答 3

8

在将 CSV 文件传递​​给 Ruby 的 CSV 解析器之前,可以对 Ruby 的 File 进行子类化以处理它的每一行。例如,这是我使用此技巧将非标准反斜杠转义引号 \" 替换为标准双引号 "" 的方法

class MyFile < File
  def gets(*args)
    line = super
    if line != nil
      line.gsub!('\\"','""')  # fix the \" that would otherwise cause a parse error
    end
    line
  end
end

infile = MyFile.open(filename)
incsv = CSV.new(infile)

while row = incsv.shift
  # process each row here
end

原则上,您可以进行各种附加处理,例如 UTF-8 清理。这种方法的好处是您可以逐行处理文件,因此您无需将其全部加载到内存中或创建中间文件。

于 2012-10-07T19:50:20.803 回答
2

首先,这是一个相当幼稚的尝试:http ://rubular.com/r/gvh3BJaNTc

/"(.*?)"(?=[\r\n,]|$)|([^,"\s].*?)(?=[\r\n,]|$)/m

这里的假设是:

  • 字段可能以引号开头。在这种情况下,它应该以以下任一引号结尾:
    • 逗号前
    • 在新行之前(如果它是该行的最后一个字段)
    • 在文件末尾之前(如果它是最后一行的最后一个字段)
  • 或者,它的第一个字符不是引号,因此它包含字符,直到满足与之前相同的条件。

几乎可以满足您的要求,但在以下领域失败:

1个逗号和
换行符"

正如TC 在评论中指出的那样,您的文字模棱两可。我相信你已经知道了,但为了完整起见:

  • "a"- 那是a还是"a"?您如何表示包含在引号中的值?
  • "1","2"- 可能被解析为1,21","2- 两者都是合法的。
  • ,1 \n 2,- 行尾,还是值中的换行符?您无法判断,特别是这是否应该是其行的最后一个值。
  • 1 \n 2 \n 3- 一个带有换行符的值?两个值(1\n2,31, 2\n3)?三个价值观?

如果您检查每一行的第一个值,您可能会得到一些线索,正如您所说,它应该告诉您列数及其类型 - 这可以为您提供解析文件时缺少的附加信息(例如,如果您知道该行中应该有另一个字段,那么所有换行符都属于当前值)。即便如此,看起来这里也有严重的问题......

于 2012-08-01T06:13:51.183 回答
-1

我制作了一个应用程序来重新格式化 CSV 文件,将字段内的单引号加倍,并将其中的新行替换为类似“\n”的字符串。

一旦数据进入数据库,我们就可以将 '\n' 替换回新行。

我需要这样做,因为我必须处理 CSV 的应用程序无法正确处理新行。

随意使用和更改。

在蟒蛇中:

import sys

def ProcessCSV(filename):
    file1 = open(filename, 'r')
    filename2 = filename + '.out'
    file2 = open(filename2, 'w')
    print 'Reformatting {0} to {1}...', filename, filename2
    line1 = file1.readline()
    while (len(line1) > 0):
        line1 = line1.rstrip('\r\n')
        line2 = ''
        count = 0
        lastField = ( len(line1) == 0 )
        while not lastField:
            lastField = (line1.find('","') == -1)
            res = line1.partition('","')
            field = res[0]
            line1 = res[2]
            count = count + 1
            hasStart = False
            hasEnd = False

            if  ( count == 1 )  and  ( field[:1] == '"' ) :
                field = field[1:]
                hasStart = True
            elif count > 1:
                hasStart = True

            while (True):
                if  ( lastField == True )  and  ( field[-1:] == '"' ) :
                    field = field[:-1]
                    hasEnd = True
                elif not lastField:
                    hasEnd = True

                if lastField and not hasEnd:
                    line1 = file1.readline()
                    if (len(line1) == 0): break
                    line1 = line1.rstrip('\r\n')
                    lastField = (line1.find('","') == -1)
                    res = line1.partition('","')
                    field = field + '\\n' + res[0]
                    line1 = res[2]
                else:
                    break

            field = field.replace('"', '""')

            line2 = line2 + iif(count > 1, ',', '') + iif(hasStart, '"', '') + field + iif(hasEnd, '"', '')

        if len(line2) > 0:
            file2.write(line2)
            file2.write('\n')

        line1 = file1.readline()

    file1.close()
    file2.close()
    print 'Done'

def iif(st, v1, v2):
    if st:
        return v1
    else:
        return v2

filename = sys.argv[1]
if len(filename) == 0:
    print 'You must specify the input file'
else:
    ProcessCSV(filename)

在 VB.net 中:

Module Module1

Sub Main()
    Dim FileName As String
    FileName = Command()
    If FileName.Length = 0 Then
        Console.WriteLine("You must specify the input file")
    Else
        ProcessCSV(FileName)
    End If
End Sub

Sub ProcessCSV(ByVal FileName As String)
    Dim File1 As Integer, File2 As Integer
    Dim Line1 As String, Line2 As String
    Dim Field As String, Count As Long
    Dim HasStart As Boolean, HasEnd As Boolean
    Dim FileName2 As String, LastField As Boolean
    On Error GoTo locError

    File1 = FreeFile()
    FileOpen(File1, FileName, OpenMode.Input, OpenAccess.Read)

    FileName2 = FileName & ".out"
    File2 = FreeFile()
    FileOpen(File2, FileName2, OpenMode.Output)

    Console.WriteLine("Reformatting {0} to {1}...", FileName, FileName2)

    Do Until EOF(File1)
        Line1 = LineInput(File1)
        '
        Line2 = ""
        Count = 0
        LastField = (Len(Line1) = 0)
        Do Until LastField
            LastField = (InStr(Line1, """,""") = 0)
            Field = Strip(Line1, """,""")
            Count = Count + 1
            HasStart = False
            HasEnd = False
            '
            If (Count = 1) And (Left$(Field, 1) = """") Then
                Field = Mid$(Field, 2)
                HasStart = True
            ElseIf Count > 1 Then
                HasStart = True
            End If
            '
locFinal:
            If (LastField) And (Right$(Field, 1) = """") Then
                Field = Left$(Field, Len(Field) - 1)
                HasEnd = True
            ElseIf Not LastField Then
                HasEnd = True
            End If
            '
            If LastField And Not HasEnd And Not EOF(File1) Then
                Line1 = LineInput(File1)
                LastField = (InStr(Line1, """,""") = 0)
                Field = Field & "\n" & Strip(Line1, """,""")
                GoTo locFinal
            End If
            '
            Field = Replace(Field, """", """""")
            '
            Line2 = Line2 & IIf(Count > 1, ",", "") & IIf(HasStart, """", "") & Field & IIf(HasEnd, """", "")
        Loop
        '
        If Len(Line2) > 0 Then
            PrintLine(File2, Line2)
        End If
    Loop

    FileClose(File1, File2)
    Console.WriteLine("Done")

    Exit Sub
locError:
    Console.WriteLine("Error: " & Err.Description)
End Sub

Function Strip(ByRef Text As String, ByRef Separator As String) As String
    Dim nPos As Long
    nPos = InStr(Text, Separator)
    If nPos > 0 Then
        Strip = Left$(Text, nPos - 1)
        Text = Mid$(Text, nPos + Len(Separator))
    Else
        Strip = Text
        Text = ""
    End If
End Function

End Module
于 2015-11-24T07:34:13.757 回答