ruby regex csv fastercsv

ruby - ¿Cómo puedo analizar robustamente CSV con formato incorrecto?



regex fastercsv (3)

Es posible subclasificar el archivo de Ruby para procesar cada línea del archivo CSV antes de pasarlo al analizador CSV de Ruby. Por ejemplo, así es cómo utilicé este truco para reemplazar las comillas escapadas no estándar / "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

En principio, podría hacer todo tipo de procesamiento adicional, por ejemplo, limpiezas UTF-8. 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.

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.

Es de origen externo y autoritativo. 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. Los campos contienen UTF-8 malformado (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é allí.
  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 debe ser razonablemente rápido.

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

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

¿Cómo puedo procesar este tipo de datos sucios de forma robusta?

ETA: Aquí hay un ejemplo simplificado de lo que puede estar 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"


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 la base de datos, 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 usar 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


Primero, aquí hay un intento bastante ingenuo: http://rubular.com/r/gvh3BJaNTc

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

Las suposiciones aquí son:

  • Un campo puede comenzar con comillas. En ese caso, debe terminar con una cita que sea:
    • antes de una coma
    • antes de una nueva línea (si es el último campo en su línea)
    • antes del final del archivo (si es el último campo en la última línea)
  • O bien, su primer carácter no es una cita, por lo que contiene caracteres hasta que se cumple la misma condición que antes.

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

1 comma and linebreaks"

Como TC había señalado en los comentarios , su texto es ambiguo. Estoy seguro de que ya lo sabes, pero para completarlo:

  • "a" - ¿es eso o "a" ? ¿Cómo se representa un valor que quiere que esté entre comillas?
  • "1","2" : se pueden 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, debería indicarle el número de columnas y sus tipos; esto puede proporcionarle la información adicional que falta para analizar el archivo ( por ejemplo, si sabe que debe haber otro campo en esta línea, todas las líneas nuevas pertenecen al valor actual). Incluso entonces, parece que hay serios problemas aquí ...