Вместо предисловия
В строительстве из BIM моделей создают КМД и монтажные чертежи, которые необходимы на заводе для изготовления конструкций, либо на площадке, чтобы понимать как эти конструкции сваривать. На этих чертежах обозначаются символы сварных швов и их номера. И имеется необходимость проверять все ли необходимые сварные швы указаны на чертежах. Потому что если какие-либо швы будут пропущены, то QA не примет работу, так как не сможет свериться. Соответственно в таком случае работа встанет до тех пор, пока чертеж не будет перевыпущен. А это новые согласования. В общем сплошная бюрократия. Понятно, что лучше швы не пропускать. Но как это сделать, когда на одном чертеже их может быть несколько сотен.
Возникла идея создать небольшую утилиту на Python, которая будет парсить PDF и сверять со списком швов, взятых из BIM модели. Изначально я обратился к библиотеке pdfminer, вернее к ее форку pdfminer.six. Но скорость работы меня совершенно не устраивала. Вот, например, загрузка файла в 10 страниц и парой картинок.
Как видно файл грузится 8 секунд. Казалось бы, не так уж и долго. Но мне приходится работать с файлами в десятки страниц каждая из которых формата A1. И тогда простое извлечение текста из файла может занимать минуты. Еще одним неприятным моментом оказалось неспособность парсить все документы. На некоторых я получал вот такую ошибку:
TypeError: int() argument must be a string, a bytes-like object or a number, not PSKeyword
Я не первый кто столкнулся с ней. Есть соответствующая ветка на GitHub.
Стал искать альтернативы и наткнулся на библиотеку fitz. На Хабре есть несколько статей в которых она упоминается вскользь. Например тут, тут или тут.
Когда я попытался вытащить текст из документа с помощью fitz, я глазам не поверил. Настолько это было быстро. Вот, например тот же файл:
Практически в 470 раз быстрее. Да и код проще некуда.
Чуть-чуть о самой библиотеке
Fitz или PyMuPDF эта питоновская обертка MuPDF – средства для просмотра, рендеринга и инструментов для работы с такими форматами как PDF, XPS, OpenXPS, CBZ, EPUB и FB2. Создана она была компанией Artifex Software, Inc, ей же и поддерживается.
Установка:
pip install fitz
Но это еще не все. Скорее всего вам также потребуется установить PyMuPDF иначе вы скорей всего получите такую ошибку:
ModuleNotFoundError: No module named 'frontend'
pip install PyMuPDF
Над своей программой я работал в виртуальной среде, и установка этих двух библиотек подтянула за собой установку кучи других. Так что не пугайтесь.
Рассмотрю теперь некоторые возможности fitz. Открытие документа:
import fitz
doc = fitz.open('\путь\к\файлу')
Получение текста постранично:
text = {}
with fitz.open('\путь\к\файлу') as doc:
for num, page in enumerate(doc.pages()):
text[num] = page.getText()
Тут я помещаю текст в словарь. Но тут, как говорится, кто во что горазд.
Как вы поняли doc.pages() это итератор по всем страницам документа. Хотя в данном случае можно итерироваться просто по документу. Например, документация дает такой вариант:
for page in doc:
# ваш код тут
Но с помощью итератора можно задавать некоторые условия:
for page in doc.pages(start, stop, step):
# ваш код тут
Итерируясь по страницам можно, например искать определенный текст:
areas = page.search_for("текст для поиска")
Метод search_for() возвращает список прямоугольников. Каждый прямоугольник представляет собой что-то типа кортежа с четырьмя координатами x0, y0, x1, y1. C помощью метода get_area() можно вычислить площадь прямоугольника. Но еще можно выделить текст желтым цветом (по мне вещь полезная).
page.add_highlight_annot(areas)
doc.save("highlighted_text.pdf")
И в сохраненном документе необходимый текст будет выделен желтым цветом. А с помощью метода add_squiggly_annot() текст будет подчеркнут синей линией. Если же имеете дела с файлом, где текст может быть повернут под углом, то можно использовать параметр quads = True в методе search_for().
Для получения количества страниц можно использовать атрибут page_count, для получения метаданных metadata, проверить является ли файл pdf. Кто использует pdfminer, те знают, что для этого там нужно писать кучу строк кода.
doc.page_count
doc.metadata
doc.is_pdf
Доступ к определенной странице можно получить с помощью метода page_load():
page = doc.load_page(номер страницы)
Страницу можно удалить:
doc.delete_page(номер страницы)
Если же нужно удалить несколько страниц:
doc.delete_pages(500, 519)
doc.delete_pages(from_page=500, to_page=519)
doc.delete_pages((500, 501, 502, ... , 519))
doc.delete_pages(range(500, 520))
Страницу также можно переместить. Первым параметром передается номер страницы, которую нужно переместить, и вторым передается номер страницы, перед которой нужно вставить. По дефолту вставляется после последней.
doc.move_page(1, to=5)
Можно создать pdf из изображений, PDF, XPS, OpenXPS, CBZ, EPUB и FB2:
file_path = 'some.jpg'
picture = fitz.open(file_path)
pdfbytes = picture.convert_to_pdf()
pdf = fitz.open("pdf", pdfbytes)
pdf.save("some.pdf")
И это лишь малая доля того, что может эта библиотека. Документация на нее весьма подробная. Хотя структура, по мне, запутанная.
Если вам необходимо извлекать текст из больших документов, то однозначно могу рекомендовать вам эту библиотеку. При извлечении текста с одностраничного pdf файла разница с тем же самым pdfminer не так уж и значительна. Но когда дело касается многостраничных и тяжелоформатных документов скорость просто поражает.
Надеюсь, данный материал был вам полезен. Всем спасибо.
P.S. Если кому интересно, что вышло с моей утилитой по поиску швов, то код лежит тут.
zoldaten
Ошибку «TypeError: int() argument must be a string, a bytes-like object or a number, not PSKeyword» победили в итоге? )
yaAubakirov Автор
Нет) Лазил в исходный код, попытался явно поменять на str(). Но ничего не вышло. Я так понял она возникает в документах с каким-то специфическим рецензированием. По крайней мере у меня было так. В общем я плюнул и перевел все на fitz
zoldaten
Попробуйте этот код. Он итерационно прогоняет все pdf в папке. Возможно, он освоит не читаемые файлы.
yaAubakirov Автор
Спасибо! Попробую