如何有力地解析格式错误的CSV?

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

它是外部采购和权威的。 我必须解析它,我不能重新输入,在输入时validation等。 就是这样; 我不控制输入。

属性:

  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:以下是单个文件中可能包含的简化示例:

 “此”, “是”,123, “一”, “正常”, “线”
 “行”,“带”“内部”,“引用”
 “短线”,“有
一个
 “内部引用”,1个逗号和
换行符”
 un“quot”ed,text,with,1,2,3,numbers
 “引用”, “号码”, “系列”, “1,2,3”
 “无效\ xAB utf-8”

在将CSV文件传递给Ruby的CSV解析器之前,可以将Ruby的文件子类化为处理CSV文件的每一行。 例如,以下是我使用此技巧用标准双引号替换非标准反斜杠转义引号“”

 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清理。 这种方法的好处在于您逐行处理文件,因此您无需将其全部加载到内存中或创建中间文件。

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

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

这里的假设是:

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

几乎可以满足您的需求,但在这些领域失败:

 1个逗号和
换行符”

正如TC在评论中指出的那样 ,你的文字含糊不清。 我相信你已经知道了,但为了完整性:

  • "a" – 是a还是"a" ? 您如何表示包含在引号中的值?
  • "1","2" – 可能被解析为11","2 – 两者都是合法的。
  • ,1 \n 2, – 行尾,还是换行值? 你无法分辨,特别是这应该是它的行的最后一个值。
  • 1 \n 2 \n 3 – 带换行符的一个值? 两个值( 1\n22\n3 )? 三个值?

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

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

一旦数据在数据库中,我们就可以将’\ n’替换为新行。

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

随意使用和更改。

在python中:

 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