Синтаксический анализ текстовых файлов в нестандартных форматах

#pdf #text-parsing

#PDF #синтаксический анализ текста

Вопрос:

У меня есть очень большой текстовый файл, который был создан путем преобразования PDF в текстовый формат с помощью утилиты XPDF ‘pdftotext’. Документ содержит данные о заброшенных свойствах, которые я хотел бы иметь возможность анализировать в формате, удобном для базы данных. Файл отформатирован следующим образом:

 DOE JANE H                                        STATE         1994     0002       SAVINGS       52            33.99   0
                                                  EMPLOYEES                         ACCOUNTS
                                                  CREDIT UNION

                 SOMECITY      ZZ      12345
  

В общем, все записи выглядят так. Некоторые из них занимают одну дополнительную строку (с дополнительным пробелом перед строкой города / штата / почтового индекса.

Чтобы сделать это еще лучше, текст не был преобразован равномерно. Каждая страница PDF конвертируется в «страницу» текстового файла с разной шириной столбцов. Кроме того, данные иногда могут выглядеть следующим образом:

 DOE JANE H      OR SOME OTHER OWNER           STATE         1994     0002       SAVINGS       52            33.99   0
                                              EMPLOYEES                         ACCOUNTS
                                              CREDIT UNION

                SOMECITY      ZZ      12345
  

Или, в качестве альтернативы, вот так:

 DOE                                      STATE         1994     0002       SAVINGS       52            33.99   0
JANE                                     EMPLOYEES                         ACCOUNTS
H                                        CREDIT UNION

            SOMECITY      ZZ      12345
  

Итак, могу ли я что-нибудь с этим сделать? Приветствуется любая помощь.

Обновить:

Для пояснения — я пытался разобрать это, используя регулярные выражения и Perl, но безуспешно (я не смог найти достойный способ работать с фрагментами по 5 строк за раз или с различными фрагментами, поскольку записи иногда занимают 6 строк). В моей лучшей попытке я смог разобрать только первую строку каждой записи, которая содержит только частичную информацию (часть или все имя, первая строка владельца, отчетный год, тип свойства и т.д.). В идеале я хотел бы иметь возможность получать всю информацию для записи, независимо от различных форматов. Язык программирования не имеет значения, поэтому для этой работы подойдет любой подходящий инструмент. Я надеялся получить информацию о различных вариантах разбора файла, подобного этому, возможно, о разных инструментах или даже приложениях, которые решают подобные проблемы.

Комментарии:

1. «Итак, я могу что-нибудь с этим сделать?» Да. Есть ли что-то более конкретное, что вы хотели бы знать? Вы ищете предложения? Подсказки по программированию? Подсказки по настройке? Что вы действительно хотите делать?

2. Извините, я хотел бы знать, есть ли хороший способ разбить данные на поля. Я пытался использовать Perl и множество регулярных выражений, но мне не везет. Если существуют другие методы или приложения, предназначенные для этой цели, мы были бы признательны за подсказку в правильном направлении.

3. Пожалуйста, обновите вопрос, чтобы точно указать, что вы хотите сделать в качестве выходных данных. Какой язык вы хотите использовать? Что вы пробовали (на каких языках)? Какую ошибку вы получаете? Это помогает быть очень, очень конкретным в том, какова ваша реальная цель. Вопрос «что я могу с этим поделать» настолько расплывчатый, что мы не можем толком ответить на него, кроме как догадываться или спрашивать подробности.

4. Я добавил небольшое обновление — дайте мне знать, если вы считаете, что требуется дополнительная информация.

Ответ №1:

Вот начало работы с Python. Что-то подобное, вероятно, можно сделать в perl.

 col_start_pat = re.compile( r's s(?=S)' )
for row in ( r.rstrip() for r in data.splitlines() ):
    if not row: continue
    def matches( row ):
        offset= 0
        for m in col_start_pat.finditer( row ):
            yield offset, row[offset:m.end(0)].rstrip()
            offset= m.end(0)
        yield offset, row[offset:].rstrip()
    columns= list( matches( row ) )
    if not columns: continue
    print( columns )
  

Это не выполняет всю работу. Он просто идентифицирует «поля» в каждой строке как последовательность кортежей, которые выглядят следующим образом.

 [(0, 'DOE JANE H'), (50, 'STATE'), (64, '1994'), (73, '0002'), (84, 'SAVINGS'), (98, '52'), (112, '33.99'), (120, '0')]
[(0, ''), (50, 'EMPLOYEES'), (84, 'ACCOUNTS')]
[(0, ''), (50, 'CREDIT UNION')]
[(0, ''), (17, 'SOMECITY'), (31, 'ZZ'), (39, '12345')]
[(0, 'DOE JANE H'), (16, 'OR SOME OTHER OWNER'), (46, 'STATE'), (60, '1994'), (69, '0002'), (80, 'SAVINGS'), (94, '52'), (108, '33.99'), (116, '0')]
[(0, ''), (46, 'EMPLOYEES'), (80, 'ACCOUNTS')]
[(0, ''), (46, 'CREDIT UNION')]
[(0, ''), (16, 'SOMECITY'), (30, 'ZZ'), (38, '12345')]
[(0, 'DOE'), (41, 'STATE'), (55, '1994'), (64, '0002'), (75, 'SAVINGS'), (89, '52'), (103, '33.99'), (111, '0')]
[(0, 'JANE'), (41, 'EMPLOYEES'), (75, 'ACCOUNTS')]
[(0, 'H'), (41, 'CREDIT UNION')]
[(0, ''), (12, 'SOMECITY'), (26, 'ZZ'), (34, '12345')]
  

Следующая часть заключается в использовании номеров столбцов заголовка для определения формата, а затем свертывании строк в единый объект.


Продолжая об этом…

 pat1= [0, 50, 64, 73, 84, 98, 112, 120]
pat2= [0, 16, 46, 60, 69, 80, 94, 108, 116]
pat3= [0, 41, 55, 64, 75, 89, 103, 111]

def columns( data ):
    col_start_pat = re.compile( r's s(?=S)' )
    for row in ( r.rstrip() for r in data.splitlines() ):
        if not row: continue
        def matches( row ):
            offset= 0
            for m in col_start_pat.finditer( row ):
                yield offset, row[offset:m.end(0)].rstrip()
                offset= m.end(0)
            yield offset, row[offset:].rstrip()
        columns= list( matches( row ) )
        yield columns

def row_groups( data ):
    columns_iter= columns(data)
    record= []
    for parsed in columns_iter:
        offsets= [ c[0] for c in parsed ]
        if offsets == pat1:
            if record: yield( format, record )
            format= 1
            record= []
        elif offsets == pat2:
            if record: yield( format, record )
            format= 2
            record= []
        elif offsets == pat3:
            if record: yield( format, record )
            format= 3
            record= []
        record.extend( parsed )
    yield( format, record )

for record in row_groups( data ):
    print( record )
  

Позволяет получить следующее:

 (1, [(0, 'DOE JANE H'), (50, 'STATE'), (64, '1994'), (73, '0002'), (84, 'SAVINGS'), (98, '52'), (112, '33.99'), (120, '0'), (0, ''), (50, 'EMPLOYEES'), (84, 'ACCOUNTS'), (0, ''), (50, 'CREDIT UNION'), (0, ''), (17, 'SOMECITY'), (31, 'ZZ'), (39, '12345')])
(2, [(0, 'DOE JANE H'), (16, 'OR SOME OTHER OWNER'), (46, 'STATE'), (60, '1994'), (69, '0002'), (80, 'SAVINGS'), (94, '52'), (108, '33.99'), (116, '0'), (0, ''), (46, 'EMPLOYEES'), (80, 'ACCOUNTS'), (0, ''), (46, 'CREDIT UNION'), (0, ''), (16, 'SOMECITY'), (30, 'ZZ'), (38, '12345')])
(3, [(0, 'DOE'), (41, 'STATE'), (55, '1994'), (64, '0002'), (75, 'SAVINGS'), (89, '52'), (103, '33.99'), (111, '0'), (0, 'JANE'), (41, 'EMPLOYEES'), (75, 'ACCOUNTS'), (0, 'H'), (41, 'CREDIT UNION'), (0, ''), (12, 'SOMECITY'), (26, 'ZZ'), (34, '12345')])
  

В котором есть вся информация; ему просто нужна какая-то умная сборка.

Комментарии:

1. Спасибо, мне придется немного повозиться с этим, чтобы разобраться с некоторыми другими странностями в файле (в основном без учета заголовков), но я думаю, что это будет работать просто отлично.