Статьи
December 26, 2022

Извлечение текста с помощью PyMuPDF

Существует куча опенсорс и проприетарных решений, которые реализуют извлечение текста из PDF-документов. Зачем знакомиться с PyMuPDF?

Постараемся ответить на этот вопрос, рассмотрим отличия PyMuPDF от других инструментов, и реализуем базовые действия в этой библиотеке.

PyMuPDF...

  • это библиотека, которая развивается компанией Artifex. Использование абсолютно бесплатно;
  • библиотека обеспечивает удобный доступ к C-библиотеке MuPDF, также принадлежащей и поддерживаемой Artifex;
  • исходный код доступен на GitHub, а библиотека может быть установлена из PyPI.
  • поддерживает многие (если не большинство) возможности MuPDF — извлечение текста является лишь одной из десятков других функций.
  • извлечение текста в библиотеке очень быстрое, как и качество распознавания.
  • не ограничивается PDF документами — библиотека поддерживает ещё XPS, EPUB, HTML и другие форматы. Вроде даже не существует ни одной другой бесплатной библиотеки, которая умела бы это всё вместе.
  • обеспечивает поддержку OCR-движка Tesseract (для распознавания символов). Это позволяет без проблем самому задавать области документа, распознавание текста в которых требуется, и работать с проблемными символами.

Что может пойти не так при извлечении текста?

Если вы работали с каким-нибудь инструментом извлечения текста, вы, вероятно, сталкивались по крайней мере с одной из следующих неприятных ситуаций:

  • Неправильный (не "естественный" / не ожидаемый) порядок блоков текста при распознавании.
  • Появляются неподдерживаемые / нечитаемые символы, например, так: "The �ase �lass fo� P�MuPDF linkDest, ...".
  • Документ вроде читаемый (для человека), но программа ничего из него извлечь не может.

PyMuPDF позволяет решить все эти проблемы.

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

Результат можно сохранять как обычный текстовый документ, так и как HTML, SVG, и JSON форматы.

Извлекаем текст

Как и в случае с любым пакетом Python, вы должны сначала импортировать PyMuPDF. Имя библиотеки — fitz:

import fitz  # Импорт PyMuPDF
doc = fitz.open("PyMuPDF.pdf")  # Открываем документ
page = doc[0]  # Указываем номер нужной страницы
text = page.get_text()  # Извлекаем текст
print(text)  # Выводим его

Для несложных документов нам больше ничего и не требуется.

В документе может быть много страниц, поэтому без проблем можно пройтись по ним циклом. Следующий код извлекает все страницы документа и объединяет их в строку с символом разрыва страницы 0XC:

all_text = ""
for page in doc:
    all_text += page.get_text() + chr(12)
   
 # или короче и быстрее с list comprehension
all_text = chr(12).join([page.get_text() for page in doc])

Код, приведённый выше, выполняется чрезвычайно быстро: например документы в 756 и 3000 страниц обработаются примерно за 0,7 секунды и 2 секунды соответственно.

Это примерно в три раза быстрее, чем pdftotext и в 30-45 раз (!) быстрее, чем простые популярные пакеты Python, такие как pdfminer или PyPDF2.

Если вы сомневаетесь, что текст сохранился в корректном порядке по структурным блокам, то просто используйте параметр sort метода get_text: page.get_text(sort = True). Это отсортирует блоки текста от верхнего левого угла до нижнего правого, и, как правило, обеспечит корректный результат записи для большинства документов.

Также можно ограничить извлечение определёнными областями страницы. Например, если страницы имеют структуры из двух столбцов, то можно определить два прямоугольника, представляющих эти области, а затем отдельно извлечь соответствующие текстовые фрагменты:

page_rect = page.rect  # Прямоугольник на всю страницу
half_width = page_rect.width / 2  # Вычисляем половину ширины страницы

left_rect = +page_rect  # Подготавливаем левую половину страницы: копируем прямоугольник страницы
left_rect.x1 = half_width  # Делаем его в два раза уже

right_rect = +page_rect  # Подготавливаем правую половину страницы: копируем прямоугольник страницы
right_rect.x0 = half_width  # Левая граница - это середина страницы

# Используем эти два прямоугольника в качестве областей обрезки для извлечения:
left_text = page.get_text(sort=True, clip=left_rect)
right_text = page.get_text(sort=True, clip=right_rect)

Извлекаем текст с дополнительной информацией

Использованный метод может предоставлять очень подробную информацию наряду с самим текстом, например:

  • направление записи и режим записи (горизонтальный / вертикальный);
    цвет (RGB);
  • название шрифта и его свойства;
  • информация о позиции (по отдельным символам, строкам и абзацам);
  • изображения;
  • автоматическая замена пробелов;
  • автоматическое определение и обработка переносов.

Давайте возьмём небольшой пример с заглавной страницы PDF-руководства по документации PyMuPDF (можно взять здесь):

import fitz
doc = fitz.open("PyMuPDF.pdf")
page = doc[0]
all_infos = page.get_text("dict", sort=True)
pprint(all_infos)

Получим такое:

{'blocks': [{'bbox': (240.0, 88.94, 540.0, 388.94),
             'bpc': 8,             
             'colorspace': 3,             
             'ext': 'png',             
             'height': 1200,             
             'image':
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x04\xb0'
             << ... omitted data ... >>,             
             'number': 0,             
             'size': 107663,             
             'transform': (300.0, 0.0, -0.0, 300.0, 240.0, 88.94),             
             'type': 1,             
             'width': 1200,             
             'xres': 96,             
             'yres': 96},            
             {'bbox': (236.90, 396.92, 540.0, 432.41),
                          'lines': [{'bbox': (236.90, 396.92, 540.0, 432.41),
                                     'dir': (1.0, 0.0),                        
                                     'spans': [{'ascender': 1.125,
                                                'bbox': (236.90, 396.92, 540.0, 432.41),                                   
                                                'color': 0,                                   
                                                'descender': -0.307,                                   
                                                'flags': 20,                                   
                                                'font': 'TeXGyreHeros-Bold',                                   
                                                'origin': (236.90, 424.80),                                   
                                                'size': 24.79,                                   
                                                'text': 'PyMuPDF Documentation'}],                        
                                     'wmode': 0}],
             'number': 1,             
             'type': 0},            
             {'bbox': (422.28, 433.36, 540.0, 457.98),             
             'lines': [{'bbox': (422.28, 433.36, 540.0, 457.98),
                        'dir': (1.0, 0.0),
                        'spans': [{'ascender': 1.123,
                        
... и так далее

Получаем многоуровневый словарь с различными параметрами, а чтобы наглядно понять, что где находится, можно воспользоваться картинкой из руководства.

  • Верхний уровень — это словари "блоков", которые являются либо изображением, либо абзацам текста. Он, в т.ч. содержит свою позицию на странице ("boundary box" = "bbox").
  • Конкретный текстовый блок содержит список словарей — строк текста (lines).
  • lines помимо своего bbox, такие содержат направление записи ("dir": кортеж), режим записи ("wmode": горизонтальный или вертикальный) и список словарей span.
  • Словарь span содержит символы с идентичными свойствами с точки зрения шрифта, размера шрифта и цвета. Если внутри строки (line) есть символы с разными настройками, то только в таком случае у нас будет несколько словарей span.
  • Помимо своего bbox, span содержит начальную точку вставки (называемую origin) и хранит информацию о свойствах используемого шрифта.

С помощью всех этих данных можно восстановить исходный вид страницы с высокой точностью, в т.ч. меняя какие-то настройки текста.

Работа с другими форматами

Как упоминалось выше, можно просто менять аргумент метода get_text для работы, например, с HTML:

text = page.get_text("html")
html_page = open("page-%i.html" % page.number, "w")
html_page.write(text)
html_page.close()

Как и в словаре выше, в случае с HTML мы так же получим все изображения на странице.

Использование PyMuPDF в качестве скрипта командной строки

При помощи строчки python -m fitz gettext <options> ... в командной строке мы можем получить результат, даже не работая с кодом.

Скрипт создает текстовый файл в трёх возможных режимах:

  1. fitz gettext -mode simple — выдаёт вывод page.get_text().
  2. fitz gettext -mode blocks — выдаёт вывод page.get_text(sort=True).
  3. fitz gettext -mode layout — выдаёт результат, структура которого будет совпадать с исходной странице. Вот здесь можно ознакомиться подробностями.

Другие параметры позволяют выбирать диапазоны страниц, минимальный размер шрифта и многое другое.

Динамическое распознавание текста

Основным назначением формата PDF является визуальное отображение текста и других данных.

PDF-формат не хранит текст, поэтому его корректное извлечение не может быть всегда гарантированно. Самое важное требование для успешного извлечения — наличие данных, которые позволяют сопоставить визуальное представление символов с их unicode отображением. Такие данные ("character map", CMAP) могут отсутствовать для конкретного шрифта, либо для конкретных символов в шрифте. В таком случае корректное извлечение текста будет просто невозможно.

Но мы можем использовать оптическое распознавание символов (Optical Character Recognition, OCR), которое как раз реализовано в MuPDF и PyMuPDF через вызов инструмента распознавания Tesseract. Рассмотрим такой кейс:

  • Допустим, в тексте есть символы, для которых не существует представление в unicode.
  • Извлечение текста через (Py-)MuPDF вернет символ chr(0xFFFD) для каждого такого символа (U+FFFD - недопустимое значение в Юникоде).
  • И каждый раз, когда будет встречаться такой символ, будет автоматически извлекаться картинка с этим символов и окружающим пространством, которая будет использована для распознавания в Tesseract.

Преимущество такого подхода заключается в том, что визуальное распознавание текста используется только там, где это действительно необходимо. Для страницы, где нет таких проблем, скорость обработки не будет снижена.

Тут есть скрипт с наглядной демонстрацией такой реализации. Каждый раз, когда мы будем наталкиваться на проблемный символ, скрипт будет сообщать об этом следующим образом (каждый � представляет один chr(0xFFFD)):

before: 'binaries we generate – our decisions are ��u��t i�� i�to them. 

'after: 'binaries we generate — our decisions are “burnt in” into them. '

В вашем арсенале теперь есть еще один инструмент работы с PDF ^_^

PythonTalk в Telegram

Чат PythonTalk в Telegram

Предложить материал | Поддержать канал

Источник: Harald Lieder