2012-07-31 10 views
13

Estoy procesando datos de fuentes gubernamentales (FEC, bases de datos de votantes estatales, etc.). Está inconsistentemente mal formado, lo que rompe mi analizador CSV en todo tipo de formas deliciosas.¿Cómo puedo analizar robustamente CSV con formato incorrecto?

Es de origen externo y autorizado. Debo analizarlo, y no puedo volver a ingresarlo, validarlo en la entrada ni nada por el estilo. Es lo que es; No controlo la entrada.

Propiedades:

  1. campos que contienen malformado UTF-8 (por ejemplo Foo \xAB bar)
  2. El primer campo de una línea especifica el tipo de registro de un conjunto conocido. Conociendo el tipo de registro, usted sabe cuántos campos hay y sus respectivos tipos de datos, pero no hasta que lo haga.
  3. Cualquier línea dentro de un archivo puede usar cadenas entre comillas ("foo",123,"bar") o sin comillas (foo,123,bar). Todavía no he encontrado ningún lugar donde se mezcle dentro de una línea determinada (es decir, "foo",123,bar), pero probablemente esté ahí.
  4. Las cadenas pueden incluir caracteres internos de nueva línea, comillas y/o comas.
  5. Las cadenas pueden incluir números separados por comas.
  6. Los archivos de datos pueden ser muy grandes (millones de filas), por lo que es necesario que sigan siendo razonablemente rápidos.

Estoy usando Ruby FasterCSV (conocido simplemente como CSV en 1.9), pero la pregunta debe ser independiente del idioma.

Supongo que una solución requerirá una sustitución de preprocesamiento con caracteres de separador de registro/comillas inequívocos (por ejemplo, ASCII RS, STX). Empecé un poco here pero no funciona para todo lo que obtengo.

¿Cómo puedo procesar este tipo de datos sucios con solidez?

ETA: He aquí un ejemplo simplificado de lo que puede ser en un solo archivo:

 
"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

Nada aquí realmente parece como CSV con formato incorrecto. ** 1 ** es problemático si los caracteres escapados reemplazan comas o comillas, pero no sugirió que ese sea el caso. ** 2 ** es increíble, es solo otro campo. ** 3 ** Legal csv - los campos pueden ser envueltos con comillas. ** 4 ** De nuevo, csv legal, siempre que se salgan las comillas: '" "'.** 5 ** No hay problema, igual que * 4 *. ** 6 ** Solo es un problema si intenta leer o analizar todo de una vez. Entonces, mientras su analizador pueda manejar filas de diferentes longitudes, debería estar bien. ¿Tiene un ejemplo de dónde es realmente CSV inválido? (el enlace muestra un problema con las comillas) – Kobi

+0

Por cierto, una búsqueda rápida encuentra esto: http://www.fec.gov/support/DataConversionTools.shtml (sí, me llevó 20 minutos pensar "tal vez alguien lo haya hecho" que antes ") – Kobi

+0

¿Podemos ver fragmentos de los bits que el analizador CSV de Ruby no puede manejar? –

Respuesta

2

primer lugar, aquí es un intento bastante ingenua: http://rubular.com/r/gvh3BJaNTc

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

Los supuestos aquí:

  • Un campo puede comenzar con comillas. En cuyo caso, se debe terminar con una cita que es ya sea:
    • antes de una coma
    • antes de una nueva línea (si es último campo en su línea)
    • antes del final del archivo (si es el último campo en la última línea)
  • O, su primer carácter no es una cita, por lo que contiene caracteres hasta que se cumplan las mismas condiciones que antes.

Este casi hace lo que quiere, pero falla en estos campos:

 
1 comma and 
linebreaks" 

Como TC had pointed out in the comments, el texto es ambiguo. Estoy seguro de que ya lo sabe, pero está completo:

  • "a" - es que a o "a"? ¿Cómo se representa un valor que quiere para estar entre comillas?
  • "1","2" - se puede analizar como 1, 2, o como 1","2 - ambos son legales.
  • ,1 \n 2, - ¿Fin de línea o nueva línea en el valor? No se puede decir, especialmente si se supone que es el último valor de su línea.
  • 1 \n 2 \n 3 - ¿Un valor con nuevas líneas? Dos valores (1\n2, 3 o 1, 2\n3)? Tres valores?

Es posible que pueda obtener algunas pistas si examina el primer valor en cada fila, que como ha dicho, debe indicar el número de columnas y sus tipos; esto puede proporcionarle información adicional. falta para analizar el archivo (por ejemplo, si sabe debe haber otro campo en esta línea, entonces todas las líneas nuevas pertenecen al valor actual). Incluso entonces, parece que hay problemas serios aquí ...

5

Es posible subclasificar el archivo de Ruby para procesar cada línea del archivo CSV antes de que se pase al analizador CSV de Ruby. Por ejemplo, aquí está cómo he utilizado este truco para reemplazar comillas no estándar escapados \" con comillas dobles estándar ''

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 

Se podría, en principio, hacer todo tipo de procesamiento adicional, por ejemplo, UTF-8 limpiezas Lo bueno de este enfoque es que maneja el archivo línea por línea, por lo que no necesita cargarlo todo en la memoria o crear un archivo intermedio.

+0

while row = incsv.shift – baash05

-1

Creé una aplicación para reformatear archivos CSV, doblando las comillas simples dentro de los campos y reemplazando las nuevas líneas dentro de ellas con una cadena como '\ n'.

Una vez que los datos están dentro de los datos base podemos reemplazar la '\ n' por nuevas líneas.

Necesitaba hacer esto porque las aplicaciones que tuve que procesar CSV no tratan correctamente las nuevas líneas.

Siéntase libre de utilizar y cambiar.

en 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) 

En 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 
Cuestiones relacionadas