Подтвердить что ты не робот

Как я могу эффективно проанализировать искаженный 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"
4b9b3361

Ответ 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