Как я могу эффективно проанализировать искаженный CSV?
Я обрабатываю данные из правительственных источников (FEC, базы данных государственных избирателей и т.д.). Он непоследовательно искажен, что ломает мой синтаксический анализатор CSV всякими восхитительными способами.
Он внешне получен и авторитетен. Я должен разобрать его, и я не могу его повторно вводить, проверять на входе и т.п. Что есть, то есть; Я не контролирую ввод.
Свойства:
- Поля содержат неверный UTF-8 (например,
Foo \xAB bar
)
- Первое поле строки указывает тип записи из известного набора. Зная тип записи, вы знаете, сколько полей есть и их соответствующие типы данных, но только после этого.
- Любая строка в файле может использовать цитируемые строки (
"foo",123,"bar"
) или некорректные (foo,123,bar
). Я еще не сталкивался с тем, где он смешивался в данной строке (т.е. "foo",123,bar
), но он, вероятно, там.
- Строки могут содержать внутренние символы новой строки, цитаты и/или запятой.
- Строки могут содержать номера, разделенные запятыми.
- Файлы данных могут быть очень большими (миллионы строк), поэтому это должно быть достаточно быстро.
Я использую Ruby FasterCSV (известный как только CSV в версии 1.9), но вопрос должен быть языковым агностиком.
Мое предположение заключается в том, что для решения потребуется предварительная обработка подстановки с однозначными разделителями записей/кавычками (например, ASCII RS, STX). Я начал немного здесь, но он не работает для всего, что я получаю.
Как я могу обработать такие грязные данные?
ETA: Здесь упрощенный пример того, что может быть в одном файле:
"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"
Ответы
Ответ 1
Можно подклассировать файл 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. Самое приятное в этом подходе заключается в том, что вы обрабатываете файл по линейному принципу, поэтому вам не нужно загружать его все в память или создавать промежуточный файл.
Ответ 2
Во-первых, вот довольно наивная попытка: http://rubular.com/r/gvh3BJaNTc
/"(.*?)"(?=[\r\n,]|$)|([^,"\s].*?)(?=[\r\n,]|$)/m
Предположения здесь:
- Поле может начинаться с кавычек. В этом случае она должна заканчиваться цитатой:
- перед запятой
- перед новой строкой (если это последнее поле на своей строке)
- до конца файла (если это последнее поле в последней строке)
- Или его первый символ не является цитатой, поэтому он содержит символы до тех пор, пока не будет выполнено то же условие, что и раньше.
Это почти делает то, что вы хотите, но не работает в этих полях:
1 comma and
linebreaks"
Как TC указал в комментариях, ваш текст неоднозначен. Я уверен, что вы уже это знаете, но для полноты:
-
"a"
- это a
или "a"
? Как вы представляете значение, которое вы хотите должны быть заключены в кавычки?
-
"1","2"
- может быть проанализирован как 1
, 2
или как 1","2
- оба являются законными.
-
,1 \n 2,
- Конец строки или новая строка в значении? Вы не можете сказать, особенно если это должно быть последним значением его строки.
-
1 \n 2 \n 3
- Одно значение с символами новой строки? Два значения (1\n2
, 3
или 1
, 2\n3
)? Три значения?
Вы можете получить некоторые подсказки, если вы исследуете первое значение в каждой строке, которое, как вы сказали, должно указывать количество столбцов и их типы - это может дать вам дополнительную информацию, которую вы не видите, чтобы разобрать файл (например, если вы знаете, в этой строке должно быть другое поле, то все новые строки принадлежат текущему значению). Даже тогда, хотя, похоже, здесь есть серьезные проблемы...
Ответ 3
Я сделал приложение для переформатирования 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