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:
- Los campos contienen UTF-8 malformado (por ejemplo,
Foo /xAB bar
) - 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.
- 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í. - Las cadenas pueden incluir caracteres internos de nueva línea, comillas y / o comas.
- Las cadenas pueden incluir números separados por comas.
- 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 como1
,2
o como1","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
o1
,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í ...