2012-07-31 91 views
13

我正在处理来自政府来源(FEC,州选民数据库等)的数据。它的格式不一致,它以各种令人愉快的方式打破了我的CSV解析器。如何稳健地解析格式不正确的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)进行预处理替换。我已经开始了here,但它并不适用于我所得到的一切。

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

埃塔:这里有什么可能是在单个文件中一个简单的例子:

 
"this","is",123,"a","normal","line" 
"line","with "an" internal","quote" 
"short line","with 
an 
"internal quote", 1 comma and 
linebreaks" 
un "quot" ed,text,with,1,2,3,numbers 
"quoted","number","series","1,2,3" 
"invalid \xAB utf-8" 
+0

真的没什么,好像畸形的CSV。如果转义字符替换逗号或引号,则** 1 **会有问题,但您并不认为是这种情况。 ** 2 **非常棒,这只是另一个领域。 ** 3 **法定csv - 字段可能用引号包装。 ** 4 **再一次,只要引号被转义,合法的csv:''“'。** 5 **没问题,与* 4 *相同。 ** 6 **如果您尝试一次读取或解析所有内容,则只有一个问题。所以,只要你的解析器可以处理不同长度的行,你应该没问题。你有没有一个CSV实际上无效的例子? (该链接显示引号有问题) – Kobi 2012-07-31 06:10:09

+0

顺便说一句,快速搜索找到了这个:http://www.fec.gov/support/DataConversionTools.shtml(是的,我花了20分钟想“可能有人已经完成了之前“) – Kobi 2012-07-31 06:30:20

+0

我们能否看到Ruby的CSV解析器无法处理的片段? – 2012-07-31 15:54:03

回答

2

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

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

的假设这里有:

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

几乎你想要做什么,但没有对这些领域:

 
1 comma and 
linebreaks" 

由于TC had pointed out in the comments,你的文字是不明确的。我相信你已经知道了,但对于完整性:

  • "a" - 是a"a"?你如何表示一个值,你想要被包裹在引号?
  • "1","2" - 可能被解析为1,21","2 - 两者都是合法的。
  • ,1 \n 2, - 线,或在值换行符结束了吗?你不能说,特别是如果这应该是它的最后一个价值。
  • 1 \n 2 \n 3 - 带换行符的一个值?两个值(1\n2,31,2\n3)?三个值?

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

5

有可能继承Ruby的文件时,它被传递给Ruby的CSV解析器之前处理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清理。这种方法的好处是你逐行处理文件,所以你不需要把它全部加载到内存或创建一个中间文件。

+0

while row = incsv.shift – baash05 2013-05-29 06:26:55

-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