- PVSM.RU - https://www.pvsm.ru -

В эпоху больших языковых моделей (Large Language Model, LLM) и постоянно расширяющейся сферы их применений непрерывно растёт и важность текстовых данных.
Существует множество типов документов, содержащих подобные виды неструктурированной информации, от веб-статей и постов в блогах до рукописных писем и стихов. Однако существенная часть этих данных хранится и передаётся в формате PDF. В частности, выяснилось, что за каждый год в Outlook открывают более двух миллиардов PDF, а в Google Drive и электронной почте ежедневно сохраняют 73 миллионов новых файлов PDF (2).
Поэтому разработка более систематического способа обработки этих документов и извлечения из них информации позволит нам автоматизировать процесс и лучше понять этот обширный объём текстовых данных. И в выполнении этой задачи, разумеется, нашим лучшим другом будет Python.
Но прежде чем начать процесс, нам нужно определиться с различными типами современных PDF, и в частности, с тремя, используемыми наиболее часто:
Даже несмотря на то, что сегодня всё больше машин имеет установленные системы OCR для распознавания текста в отсканированных документах, всё равно существуют документы, содержащие страницы в формате изображений. Вероятно, вы сталкивались с ними, когда читая отличную статью и попытавшись выделить предложение, выделяли всю страницу. Это может быть вызвано ограничениями конкретной машины с OCR или его полным отсутствием. Чтобы эта информация не осталась нераспознанной, я решил создать процесс, учитывающий подобные ситуации и извлекающий максимум информации из PDF.
Помня о перечисленных типах файлов PDF и различных составляющих их элементах, важно выполнить первоначальный анализ структуры PDF для определения подходящего инструмента, необходимого для каждого компонента. На основании результатов этого анализа мы будем применять соответствующий способ извлечения текста из PDF, будь то текстовый блок с метаданными, текст в изображениях или структурированный текст в таблицах. В отсканированных документах без OCR основную задачу будет выполнять методика выявления и извлечения текста из изображений. Результатом этого процесса станет словарь Python, содержащий информацию, извлечённую из каждой страницы файла PDF. Каждый ключ в этом словаре будет обозначать номер страницы документа, а соответствующее ему значение будет списком со пятью следующими вложенными списками, содержащими следующие данные:

Таким образом мы сможем обеспечить более логичное разделение извлечённого текста по исходным компонентам; к тому же это иногда может помочь нам проще извлекать информацию, обычно встречаемую в конкретном компоненте (например, название компании в изображении логотипа). Кроме того, извлечённые из текста метаданные, например, тип и размер шрифта, можно использовать для упрощения распознавания заголовков текста или важного выделенного текста, что позволит нам ещё сильнее разделить его или выполнять постобработку текста по отдельным блокам. Наконец, сохранение структурированной информации таблиц в понятном для LLM виде существенно повысит качество инференсов, которые модель будет делать о взаимосвязях внутри извлечённых данных. Далее эти результаты можно будет скомбинировать в вывод всей текстовой информации на каждой странице.
На рисунках ниже показана блок-схема этой методики.

Перед началом этого проекта нам установить библиотеки. Будем предполагать, что у вас установлен Python 3.10 или более новая версия. В противном случае можно установить его отсюда [1]. Затем установим следующие библиотеки:
PyPDF2: для считывания файла PDF по пути репозитория.
pip install PyPDF2
Pdfminer: для выполнения анализа структуры и извлечения текста и формата из PDF. (Python 3 поддерживает версия .six библиотеки.)
pip install pdfminer.six
Pdfplumber: для распознавания таблиц на странице PDF и извлечения информации из них.
pip install pdfplumber
Pdf2image: для преобразования обрезанного изображения PDF в изображение PNG.
pip install pdf2image
PIL: для чтения изображения PNG.
pip install Pillow
Pytesseract: для извлечения текста из изображений при помощи технологии OCR
Её устанавливать немного сложнее, потому что сначала необходимо установить Google Tesseract OCR [2] — платформу OCR, основанную на модели LSTM, которая занимается распознаванием линий и паттернов символов.
Для установки на машину Mac через Brew достаточно сделать следующее:
brew install tesseract
Пользователи Windows для установки библиотеки могут воспользоваться этой ссылкой [3]. Затем после скачивания и установки ПО необходимо добавить пути к его исполняемым файлам в переменные окружения компьютера. Или же можно выполнить следующие команды, чтобы напрямую добавить их пути в скрипт Python:
pytesseract.pytesseract.tesseract_cmd = r'C:Program FilesTesseract-OCRtesseract.exe'
Затем можно установить библиотеку Python
pip install pytesseract
Наконец, мы импортируем все библиотеки в начале нашего скрипта.
# Для считывания PDF
import PyPDF2
# Для анализа структуры PDF и извлечения текста
from pdfminer.high_level import extract_pages, extract_text
from pdfminer.layout import LTTextContainer, LTChar, LTRect, LTFigure
# Для извлечения текста из таблиц в PDF
import pdfplumber
# Для извлечения изображений из PDF
from PIL import Image
from pdf2image import convert_from_path
# Для выполнения OCR, чтобы извлекать тексты из изображений
import pytesseract
# Для удаления дополнительно созданных файлов
import os
Итак, теперь всё готово, можно приступать к самому интересному.

Для предварительного анализа мы воспользовались библиотекой Python PDFMiner, чтобы разделить текст из документа на несколько объектов страниц, а затем разбить и исследовать структуру каждой страницы. В файлах PDF отсутствует структурированная информация (абзацы, предложения и слова, воспринимаемые человеческим глазом). Они понимают только отдельные символы текста и их положение на странице. PDFMiner пытается воссоздать содержимое страницы в отдельных символах и их расположении на в файле. Затем, сравнивая расстояния этих символов от других, он собирает слова, предложения, строки и абзацы текста. (4) Чтобы добиться этого, библиотека:
Разделяет отдельные страницы из файла PDF при помощи высокоуровневой функции extract_pages() и преобразует их в объекты LTPage.
Затем для каждого объекта LTPage, она итеративно обходит каждый элемент сверху вниз и пытается идентифицировать один из соответствующих компонентов:
Следовательно, на основании этого воссоздания страницы и классификации её элементов (LTFigure содержит изображения страницы, LTTextContainer содержит текстовую информацию страницы, LTRect является показателем наличия таблицы) мы можем применить соответствующую функцию, чтобы лучше извлекать информацию.
for pagenum, page in enumerate(extract_pages(pdf_path)):
# Итеративно обходим элементы, из которых состоит страница
for element in page:
# Проверяем, является ли элемент текстовым
if isinstance(element, LTTextContainer):
# Функция для извлечения текста из текстового блока
pass
# Функция для извлечения формата текста
pass
# Проверка элементов на наличие изображений
if isinstance(element, LTFigure):
# Функция для преобразования PDF в изображение
pass
# Функция для извлечения текста при помощи OCR
pass
# Проверка элементов на наличие таблиц
if isinstance(element, LTRect):
# Функция для извлечения таблицы
pass
# Функция для преобразования содержимого таблицы в строку
pass
Разобравшись с аналитической частью процесса, давайте создадим функции, необходимые для извлечения текста из каждого компонента.
После этого извлечение текста из контейнера текста становится очень простой задачей.
# Создаём функцию для извлечения текста
def text_extraction(element):
# Извлекаем текст из вложенного текстового элемента
line_text = element.get_text()
# Находим форматы текста
# Инициализируем список со всеми форматами, встречающимися в строке текста
line_formats = []
for text_line in element:
if isinstance(text_line, LTTextContainer):
# Итеративно обходим каждый символ в строке текста
for character in text_line:
if isinstance(character, LTChar):
# Добавляем к символу название шрифта
line_formats.append(character.fontname)
# Добавляем к символу размер шрифта
line_formats.append(character.size)
# Находим уникальные размеры и названия шрифтов в строке
format_per_line = list(set(line_formats))
# Возвращаем кортеж с текстом в каждой строке вместе с его форматом
return (line_text, format_per_line)
То есть чтобы извлечь текст из контейнера текста, мы просто используем метод get_text() элемента LTTextContainer. Этот метод получает все символы, из которых состоят слова в конкретном прямоугольнике корпуса, сохраняя вывод в список текстовых данных. Каждый элемент в этом списке представляет сырую текстовую информацию, содержащуюся в контейнере.
Теперь для определения формата текста мы итеративно обойдём объект LTTextContainer для получения доступа по отдельности к каждой строке текста в этом корпусе. На каждой итерации создаётся новый объект LTTextLine, обозначающий строку текста в этом блоке корпуса. Затем мы проверяем, содержит ли текст вложенный элемент строки. Если да, то мы получаем доступ к каждому отдельному символу как LTChar, который содержит все метаданные этого символа. Из этих метаданных мы извлекаем два типа форматов и сохраняем их в отдельном списке, расположенном в соответствии с исследуемым текстом:
В общем случае символы внутри отдельного блока текста обычно имеют согласованное форматирование, если только некоторые из них не выделены полужирным. Чтобы упростить дальнейший анализ, мы находим уникальные значения форматирования текста для всех символов внутри текста и сохраняем их в соответствующий список.

Здесь всё будет несколько сложнее.
Как обрабатывать текст в изображениях, найденных в PDF?
Во-первых, нам нужно определить, что хранящиеся в PDF элементы изображений не отличаются по формату от файла, например, JPEG или PNG. В таком случае для применения к ним ПО OCR нам сначала нужно будет отделить их от файла, а затем преобразовать их в формат изображения.
# Создаём функцию для вырезания элементов изображений из PDF
def crop_image(element, pageObj):
# Получаем координаты для вырезания изображения из PDF
[image_left, image_top, image_right, image_bottom] = [element.x0,element.y0,element.x1,element.y1]
# Обрезаем страницу по координатам (left, bottom, right, top)
pageObj.mediabox.lower_left = (image_left, image_bottom)
pageObj.mediabox.upper_right = (image_right, image_top)
# Сохраняем обрезанную страницу в новый PDF
cropped_pdf_writer = PyPDF2.PdfWriter()
cropped_pdf_writer.add_page(pageObj)
# Сохраняем обрезанный PDF в новый файл
with open('cropped_image.pdf', 'wb') as cropped_pdf_file:
cropped_pdf_writer.write(cropped_pdf_file)
# Создаём функцию для преобразования PDF в изображения
def convert_to_images(input_file,):
images = convert_from_path(input_file)
image = images[0]
output_file = "PDF_image.png"
image.save(output_file, "PNG")
# Создаём функцию для считывания текста из изображений
def image_to_text(image_path):
# Считываем изображение
img = Image.open(image_path)
# Извлекаем текст из изображения
text = pytesseract.image_to_string(img)
return text
Чтобы добиться этого, мы выполняем следующий процесс:
В результате этот процесс возвращает нам текст из изображений, который мы сохраняем в третий список в выходном словаре. Этот список содержит текстовую информацию, извлечённую из изображений на исследуемой странице.
В этом разделе мы будем извлекать более логически структурированный текст из таблиц на странице PDF. Это чуть более сложная задача, чем извлечение текста из корпуса, потому что нам нужно учитывать дробность информации и взаимосвязи между примерами данных, представленными в таблице.
Хотя существует множество библиотек для извлечения из PDF табличных данных (самая популярная из них — это Tabula-py [4]), мы выявили в их функциональности определённые ограничения.
На наш взгляд, самое очевидное из них — это то, что библиотека помечает разные строки в таблице при помощи специальным символом разрыва строки n в тексте таблицы. В большинстве случаев это работает достаточно неплохо, но не позволяет распознать таблицу правильно, когда текст в ячейке разделён на две или более строки, что приводит к добавлению ненужных пустых строк и потере контекста извлечённой ячейки.
Ниже показан пример этого при попытке извлечения данных из таблицы при помощи tabula-py:

Кроме того, извлечённая информация выводится в Pandas DataFrame, а не в строку. В большинстве случаев этот формат может быть предпочтительным, но в случае трансформеров, работающих с текстом, перед отправкой в модель эти результаты необходимо преобразовывать.
Поэтому для выполнения этой задачи мы использовали библиотеку pdfplumber. Во-первых, она создана на основе pdfminer.six, которую мы уже использовали для предварительного анализа, то есть она содержит схожие объекты. Кроме того, её методика распознавания таблиц основана на элементах строк и их пересечениях, составляющих ячейку, содержащую текст, а затем и саму таблицу. Благодаря этому после определения ячейки таблицы мы можем извлечь только содержимое внутри ячейки, не перенося информацию о том, сколько строк должно рендериться. Получим содержимое таблицы, мы сформатируем его в напоминающую таблицу строку и сохраним в соответствующий список.
# Извлечение таблиц из страницы
def extract_table(pdf_path, page_num, table_num):
# Открываем файл pdf
pdf = pdfplumber.open(pdf_path)
# Находим исследуемую страницу
table_page = pdf.pages[page_num]
# Извлекаем соответствующую таблицу
table = table_page.extract_tables()[table_num]
return table
# Преобразуем таблицу в соответствующий формат
def table_converter(table):
table_string = ''
# Итеративно обходим каждую строку в таблице
for row_num in range(len(table)):
row = table[row_num]
# Удаляем разрыв строки из текста с переносом
cleaned_row = [item.replace('n', ' ') if item is not None and 'n' in item else 'None' if item is None else item for item in row]
# Преобразуем таблицу в строку
table_string+=('|'+'|'.join(cleaned_row)+'|'+'n')
# Удаляем последний разрыв строки
table_string = table_string[:-1]
return table_string
Чтобы сделать это, мы создали две функции, extract_table() для извлечения содержимого таблицы в список списков, и table_converter(), для объединения содержимого этих списков напоминающую таблицу строку.
В функции extract_table():
В функции table_converter():
В результате этого мы получаем строку текста, описывающую содержимое таблицы без потери дробности представленных в ней данных.
Теперь, когда все компоненты кода готовы, давайте объединим их, чтобы получить полнофункциональный код. Можете скопировать код отсюда или найти его с примером PDF в моём репозитарии Github здесь [5].
# Находим путь к PDF
pdf_path = 'OFFER 3.pdf'
# создаём объект файла PDF
pdfFileObj = open(pdf_path, 'rb')
# создаём объект считывателя PDF
pdfReaded = PyPDF2.PdfReader(pdfFileObj)
# Создаём словарь для извлечения текста из каждого изображения
text_per_page = {}
# Извлекаем страницы из PDF
for pagenum, page in enumerate(extract_pages(pdf_path)):
# Инициализируем переменные, необходимые для извлечения текста со страницы
pageObj = pdfReaded.pages[pagenum]
page_text = []
line_format = []
text_from_images = []
text_from_tables = []
page_content = []
# Инициализируем количество исследованных таблиц
table_num = 0
first_element= True
table_extraction_flag= False
# Открываем файл pdf
pdf = pdfplumber.open(pdf_path)
# Находим исследуемую страницу
page_tables = pdf.pages[pagenum]
# Находим количество таблиц на странице
tables = page_tables.find_tables()
# Находим все элементы
page_elements = [(element.y1, element) for element in page._objs]
# Сортируем все элементы по порядку нахождения на странице
page_elements.sort(key=lambda a: a[0], reverse=True)
# Находим элементы, составляющие страницу
for i,component in enumerate(page_elements):
# Извлекаем положение верхнего края элемента в PDF
pos= component[0]
# Извлекаем элемент структуры страницы
element = component[1]
# Проверяем, является ли элемент текстовым
if isinstance(element, LTTextContainer):
# Проверяем, находится ли текст в таблице
if table_extraction_flag == False:
# Используем функцию извлечения текста и формата для каждого текстового элемента
(line_text, format_per_line) = text_extraction(element)
# Добавляем текст каждой строки к тексту страницы
page_text.append(line_text)
# Добавляем формат каждой строки, содержащей текст
line_format.append(format_per_line)
page_content.append(line_text)
else:
# Пропускаем текст, находящийся в таблице
pass
# Проверяем элементы на наличие изображений
if isinstance(element, LTFigure):
# Вырезаем изображение из PDF
crop_image(element, pageObj)
# Преобразуем обрезанный pdf в изображение
convert_to_images('cropped_image.pdf')
# Извлекаем текст из изображения
image_text = image_to_text('PDF_image.png')
text_from_images.append(image_text)
page_content.append(image_text)
# Добавляем условное обозначение в списки текста и формата
page_text.append('image')
line_format.append('image')
# Проверяем элементы на наличие таблиц
if isinstance(element, LTRect):
# Если первый прямоугольный элемент
if first_element == True and (table_num+1) <= len(tables):
# Находим ограничивающий прямоугольник таблицы
lower_side = page.bbox[3] - tables[table_num].bbox[3]
upper_side = element.y1
# Извлекаем информацию из таблицы
table = extract_table(pdf_path, pagenum, table_num)
# Преобразуем информацию таблицы в формат структурированной строки
table_string = table_converter(table)
# Добавляем строку таблицы в список
text_from_tables.append(table_string)
page_content.append(table_string)
# Устанавливаем флаг True, чтобы избежать повторения содержимого
table_extraction_flag = True
# Преобразуем в другой элемент
first_element = False
# Добавляем условное обозначение в списки текста и формата
page_text.append('table')
line_format.append('table')
# Проверяем, извлекли ли мы уже таблицы из этой страницы
if element.y0 >= lower_side and element.y1 <= upper_side:
pass
elif not isinstance(page_elements[i+1][1], LTRect):
table_extraction_flag = False
first_element = True
table_num+=1
# Создаём ключ для словаря
dctkey = 'Page_'+str(pagenum)
# Добавляем список списков как значение ключа страницы
text_per_page[dctkey]= [page_text, line_format, text_from_images,text_from_tables, page_content]
# Закрываем объект файла pdf
pdfFileObj.close()
# Удаляем созданные дополнительные файлы
os.remove('cropped_image.pdf')
os.remove('PDF_image.png')
# Удаляем содержимое страницы
result = ''.join(text_per_page['Page_0'][4])
print(result)
Приведённый выше скрипт выполняет следующие действия:
Импортирует необходимые библиотеки.
Открывает файл PDF при помощи библиотеки pyPDF2.
Извлекает каждую страницу в PDF и итеративно выполняет следующие этапы.
Определяет, есть ли на странице таблицы и создаёт их список при помощи pdfplumner.
Находит все вложенные в страницу элементы и сортирует в порядке их нахождения в структуре страницы.
Затем для каждого элемента:
Проверяет, текстовый ли это контейнер, и не встречается ли он в табличном элементе. Затем использует функцию text_extraction() для извлечения текста с его форматом, в противном случае пропускает этот текст.
Проверяет, изображение ли это, и использует функцию crop_image() для вырезания компонента изображения из PDF, преобразует его в файл изображения при помощи convert_to_images() и извлекает из него текст при помощи OCR функцией image_to_text().
Проверяет, прямоугольный ли это элемент. Если да, то проверяет, является ли первый прямоугольник частью таблицы на странице, и если да, то переходит к следующим этапам:
Результаты процесса сохраняются в 5 списков на каждую итерацию, а именно:
Все списки сохраняются под ключами в словаре, описывающими номер страницы, исследуемой каждый раз.
Затем мы закрываем файл PDF и удаляем все дополнительные файлы, созданные в процессе работы.
Наконец, мы можем отобразить содержимое страницы, объединив элементы в списке page_content.
Мне кажется, эта методика использует наилучшие характеристики многих библиотек и делает процесс адаптируемым к различным типам PDF и видам встречаемых элементов, однако основную часть работы выполняет PDFMiner. Кроме того, информация о формате текста может помочь нам в выявлении возможных заголовков, которые разделяют текст на отдельные логические блоки, а не просто содержимого страницы, и идентифицировать текст повышенной важности.
Автор:
ru_vds
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/pdf/387468
Ссылки в тексте:
[1] отсюда: https://www.python.org/
[2] Google Tesseract OCR: https://github.com/tesseract-ocr/tesseract
[3] этой ссылкой: https://linuxhint.com/install-tesseract-windows/
[4] Tabula-py: https://pypi.org/project/tabula-py/
[5] здесь: https://github.com/g-stavrakis/PDF_Text_Extraction
[6] https://www.techopedia.com/12-practical-large-language-model-llm-applications: https://www.techopedia.com/12-practical-large-language-model-llm-applications
[7] https://www.pdfa.org/wp-content/uploads/2018/06/1330_Johnson.pdf: https://www.pdfa.org/wp-content/uploads/2018/06/1330_Johnson.pdf
[8] https://pdfpro.com/blog/guides/pdf-ocr-guide/#:~:text=OCR: https://pdfpro.com/blog/guides/pdf-ocr-guide/#:~:text=OCR
[9] https://pdfminersix.readthedocs.io/en/latest/topic/converting_pdf_to_text.html#id1: https://pdfminersix.readthedocs.io/en/latest/topic/converting_pdf_to_text.html#id1
[10] https://github.com/pdfminer/pdfminer.six: https://github.com/pdfminer/pdfminer.six
[11] Источник: https://habr.com/ru/companies/ruvds/articles/765246/?utm_source=habrahabr&utm_medium=rss&utm_campaign=765246
Нажмите здесь для печати.