Полный процесс извлечения текстовой информации из таблиц, изображений и обычного текста из файла PDF.
Введение
В эпоху больших языковых моделей (LLM) и их широкого спектра применений, от простого суммирования и перевода текста до прогнозирования динамики акций на основе настроений и тем финансовых отчетов, важность текстовых данных никогда не была такой высокой.
Существует множество типов документов, в которых содержится такая неструктурированная информация: от веб-статей и сообщений в блогах до рукописных писем и стихов. Однако значительная часть этих текстовых данных хранится и передается в формате PDF. В частности, было обнаружено, что более 2 миллиардов PDF-файлов открываются в Outlook каждый год, а 73 миллиона новых PDF-файлов сохраняются на Google Диске и в электронной почте ежедневно (2).
Таким образом, разработка более систематического способа обработки этих документов и извлечения из них информации дала бы нам возможность иметь автоматизированный поток и лучше понимать и использовать этот огромный объем текстовых данных. И для этой задачи, конечно же, нашим лучшим другом может быть никто иной, как Python.
Однако, прежде чем мы начнем наш процесс, нам необходимо указать различные типы PDF-файлов, которые существуют в наши дни, а точнее, три наиболее часто встречающихся:
- Файлы PDF, созданные программным способом. Эти PDF-файлы создаются на компьютере с использованием либо технологий W3C, таких как HTML, CSS и Javascript, либо другого программного обеспечения, например Adobe Acrobat. Файлы этого типа могут содержать различные компоненты, такие как изображения, текст и ссылки, которые доступны для поиска и легко редактируются.
- Традиционные отсканированные документы. Эти PDF-файлы создаются на неэлектронных носителях с помощью сканера или мобильного приложения. Эти файлы представляют собой не что иное, как набор изображений, хранящихся вместе в файле PDF. При этом элементы, появляющиеся на этих изображениях, такие как текст или ссылки, не могут быть выбраны или найдены. По сути, PDF служит контейнером для этих изображений.
- Сканирование документов с помощью оптического распознавания символов. В этом случае после сканирования документа используется программное обеспечение оптического распознавания символов (OCR) для идентификации текста в каждом изображении в файле и преобразования его в текст, доступный для поиска и редактирования. Затем программа добавляет к изображению слой с реальным текстом, и таким образом вы можете выбрать его как отдельный компонент при просмотре файла. (3)
Несмотря на то, что в настоящее время все больше и больше машин оснащены системами оптического распознавания символов, которые идентифицируют текст отсканированных документов, все еще существуют документы, содержащие полные страницы в формате изображения. Вы, наверное, видели это, когда читали отличную статью и пытались выделить предложение, но вместо этого выделяли всю страницу. Это может быть результатом ограничения конкретной машины OCR или ее полного отсутствия. Таким образом, чтобы не оставить эту информацию незамеченной в этой статье, я попытался создать процесс, который также учитывает эти случаи и максимально использует наши ценные и богатые информацией PDF-файлы.
Теоретический подход
Учитывая все эти различные типы PDF-файлов и различные элементы, из которых они состоят, важно выполнить первоначальный анализ макета PDF-файла, чтобы определить подходящий инструмент, необходимый для каждого компонента. В частности, на основе результатов этого анализа мы применим соответствующий метод для извлечения текста из PDF-файла, будь то текст, отображаемый в блоке корпуса с его метаданными, текст внутри изображений или структурированный текст в таблицах. В отсканированном документе без оптического распознавания символов всю тяжелую работу возьмет на себя подход, который идентифицирует и извлекает текст из изображений. Результатом этого процесса будет словарь Python, содержащий информацию, извлеченную для каждой страницы PDF-файла. Каждый ключ в этом словаре будет представлять номер страницы документа, а его соответствующее значение будет списком со следующими 5 вложенными списками, содержащими:
- Текст, извлеченный из каждого текстового блока корпуса.
- Формат текста в каждом текстовом блоке с точки зрения семейства шрифтов и размера.
- Текст, извлеченный из изображений на странице
- Текст, извлеченный из таблиц в структурированном формате.
- Полное текстовое содержимое страницы
Таким образом, мы можем добиться более логического разделения извлеченного текста по каждому исходному компоненту, и иногда это может помочь нам легче получить информацию, которая обычно появляется в конкретном компоненте (например, название компании в изображении логотипа). Кроме того, метаданные, извлеченные из текста, такие как семейство и размер шрифта, можно использовать для простой идентификации текстовых заголовков или выделенного текста, имеющего более важное значение, что поможет нам в дальнейшем разделить или постобработать текст на несколько разных фрагментов. Наконец, сохранение структурированной информации таблицы таким образом, чтобы ее мог понять LLM, значительно повысит качество выводов, сделанных о связях внутри извлеченных данных. Затем эти результаты можно скомпоновать как вывод всей текстовой информации, появившейся на каждой странице.
Блок-схему этого подхода вы можете увидеть на изображениях ниже.
Установка всех необходимых библиотек
Однако прежде чем мы начнем этот проект, нам следует установить необходимые библиотеки. Мы предполагаем, что на вашем компьютере установлен Python 3.10 или выше. В противном случае вы можете установить его здесь. Затем давайте установим следующие библиотеки:
PyPDF2: для чтения PDF-файла по пути к репозиторию.
pip install PyPDF2
Pdfminer: для анализа макета и извлечения текста и формата из PDF-файла. (версия библиотеки .six поддерживает Python 3)
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, который представляет собой машину оптического распознавания символов, основанную на модели LSTM для распознавания строк и шаблонов символов.
Вы можете установить это на свой компьютер, если вы являетесь пользователем Mac, через Brew со своего терминала, и все готово.
brew install tesseract
Для пользователей Windows вы можете выполнить следующие действия, чтобы установить ссылку. Затем, когда вы загружаете и устанавливаете программное обеспечение, вам необходимо добавить пути к его исполняемым файлам в переменные среды на вашем компьютере. Альтернативно вы можете запустить следующие команды, чтобы напрямую включить их пути в скрипт Python, используя следующий код:
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
Затем вы можете установить библиотеку Python
pip install pytesseract
Наконец, мы импортируем все библиотеки в начале нашего скрипта.
# To read the PDF import PyPDF2 # To analyze the PDF layout and extract text from pdfminer.high_level import extract_pages, extract_text from pdfminer.layout import LTTextContainer, LTChar, LTRect, LTFigure # To extract text from tables in PDF import pdfplumber # To extract the images from the PDFs from PIL import Image from pdf2image import convert_from_path # To perform OCR to extract text from images import pytesseract # To remove the additional created files import os
Итак, теперь у нас все готово. Перейдем к самому интересному.
Анализ макета документа с помощью Python
Для предварительного анализа мы использовали библиотеку PDFMiner Python, чтобы разделить текст объекта документа на несколько объектов страницы, а затем разбить и изучить макет каждой страницы. PDF-файлам по своей сути не хватает структурированной информации, такой как абзацы, предложения или слова, видимые человеческим глазом. Вместо этого они понимают только отдельные символы текста и их положение на странице. Таким образом, PDFMiner пытается восстановить содержимое страницы по отдельным символам и их положению в файле. Затем, сравнивая расстояния этих символов от других, он составляет соответствующие слова, предложения, строки и абзацы текста. (4) Для этого библиотека:
Отделяет отдельные страницы от файла PDF с помощью функции высокого уровня extract_pages() и преобразует их в объекты LTPage.
Затем для каждого объекта LTPage он выполняет итерацию от каждого элемента сверху вниз и пытается идентифицировать соответствующий компонент одним из следующих способов:
- LTFigure, представляющий область PDF-файла, в которой могут быть представлены рисунки или изображения, встроенные в другой PDF-документ на странице.
- LTTextContainer, представляющий группу текстовых строк в прямоугольной области, затем анализируется и превращается в список объектов LTTextLine. Каждый из них представляет собой список объектов LTChar, в которых хранятся отдельные символы текста вместе с их метаданными. (5)
- LTRect представляет собой двумерный прямоугольник, который можно использовать для обрамления изображений и фигур или создания таблиц в объекте LTPage.
Таким образом, на основе этой реконструкции страницы и классификации ее элементов либо на LTFigure, который содержит изображения или рисунки страницы, либо на LTTextContainer, который представляет текстовую информацию. страницы или LTRect, что будет явным признаком наличия таблицы, мы можем применить соответствующую функцию для лучшего извлечения информации.
for pagenum, page in enumerate(extract_pages(pdf_path)): # Iterate the elements that composed a page for element in page: # Check if the element is a text element if isinstance(element, LTTextContainer): # Function to extract text from the text block pass # Function to extract text format pass # Check the elements for images if isinstance(element, LTFigure): # Function to convert PDF to Image pass # Function to extract text with OCR pass # Check the elements for tables if isinstance(element, LTRect): # Function to extract table pass # Function to convert table content into a string pass
Итак, теперь, когда мы понимаем часть процесса анализа, давайте создадим функции, необходимые для извлечения текста из каждого компонента.
Определите функцию для извлечения текста из PDF
С этого момента извлечение текста из текстового контейнера становится очень простым.
# Create a function to extract text def text_extraction(element): # Extracting the text from the in-line text element line_text = element.get_text() # Find the formats of the text # Initialize the list with all the formats that appeared in the line of text line_formats = [] for text_line in element: if isinstance(text_line, LTTextContainer): # Iterating through each character in the line of text for character in text_line: if isinstance(character, LTChar): # Append the font name of the character line_formats.append(character.fontname) # Append the font size of the character line_formats.append(character.size) # Find the unique font sizes and names in the line format_per_line = list(set(line_formats)) # Return a tuple with the text in each line along with its format return (line_text, format_per_line)
Поэтому, чтобы извлечь текст из текстового контейнера, мы просто используем метод get_text() элемента LTTextContainer. Этот метод извлекает все символы, составляющие слова, в определенном блоке корпуса, сохраняя выходные данные в списке текстовых данных. Каждый элемент в этом списке представляет собой необработанную текстовую информацию, содержащуюся в контейнере.
Теперь, чтобы определить формат этого текста, мы перебираем объект LTTextContainer для индивидуального доступа к каждой текстовой строке этого корпуса. На каждой итерации создается новый объект LTTextLine, представляющий строку текста в этом фрагменте корпуса. Затем мы проверяем, содержит ли вложенный элемент строки текст. Если это так, мы получаем доступ к каждому отдельному элементу символа как LTChar, который содержит все метаданные для этого символа. Из этих метаданных мы извлекаем два типа форматов и сохраняем их в отдельном списке, расположенном соответственно исследуемому тексту:
- Семейство шрифтов символов, включая то, выделен ли символ жирным шрифтом или курсивом.
- Размер шрифта для символа
Обычно символы в определенном фрагменте текста имеют единообразное форматирование, если только некоторые из них не выделены жирным шрифтом. Чтобы облегчить дальнейший анализ, мы фиксируем уникальные значения форматирования текста для всех символов в тексте и сохраняем их в соответствующем списке.
Определите функцию для извлечения текста из изображений.
Здесь я считаю, что это более сложная часть.
Как обрабатывать текст на изображениях, найденных в PDF?
Во-первых, нам необходимо установить, что элементы изображения, хранящиеся в PDF-файлах, не имеют формата, отличного от формата файла, например JPEG или PNG. Таким образом, чтобы применить к ним программное обеспечение OCR, нам нужно сначала отделить их от файла, а затем преобразовать в формат изображения.
# Create a function to crop the image elements from PDFs def crop_image(element, pageObj): # Get the coordinates to crop the image from the PDF [image_left, image_top, image_right, image_bottom] = [element.x0,element.y0,element.x1,element.y1] # Crop the page using coordinates (left, bottom, right, top) pageObj.mediabox.lower_left = (image_left, image_bottom) pageObj.mediabox.upper_right = (image_right, image_top) # Save the cropped page to a new PDF cropped_pdf_writer = PyPDF2.PdfWriter() cropped_pdf_writer.add_page(pageObj) # Save the cropped PDF to a new file with open('cropped_image.pdf', 'wb') as cropped_pdf_file: cropped_pdf_writer.write(cropped_pdf_file) # Create a function to convert the PDF to images 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") # Create a function to read text from images def image_to_text(image_path): # Read the image img = Image.open(image_path) # Extract the text from the image text = pytesseract.image_to_string(img) return text
Для достижения этой цели мы следуем следующему процессу:
- Мы используем метаданные объекта LTFigure, обнаруженного PDFMiner, для обрезки поля изображения, используя его координаты в макете страницы. Затем мы сохраняем его как новый PDF-файл в нашем каталоге, используя библиотеку PyPDF2.
- Затем мы используем функцию convert_from_file() из библиотеки pdf2image, чтобы преобразовать все PDF-файлы в каталоге в список изображений и сохранить их в формате PNG.
- Наконец, теперь, когда у нас есть файлы изображений, мы читаем их в нашем скрипте, используя пакет Image модуля PIL, и реализуем image_to_string(). функция pytesseract для извлечения текста из изображений с использованием механизма OCR tesseract.
В результате этот процесс возвращает текст из изображений, который мы затем сохраняем в третьем списке выходного словаря. Этот список содержит текстовую информацию, извлеченную из изображений на исследуемой странице.
Определите функцию для извлечения текста из таблиц
В этом разделе мы извлечем более логично структурированный текст из таблиц на странице PDF. Это немного более сложная задача, чем извлечение текста из корпуса, поскольку нам необходимо учитывать степень детализации информации и связи, образующиеся между точками данных, представленными в таблице.
Хотя существует несколько библиотек, используемых для извлечения табличных данных из PDF-файлов, среди которых Tabula-py является одной из самых известных, мы выявили определенные ограничения в их функциональности.
Самый яркий из них, на наш взгляд, связан с тем, что библиотека идентифицирует различные строки таблицы с помощью специального символа разрыва строки \n в тексте таблицы. В большинстве случаев это работает довольно хорошо, но не позволяет правильно фиксировать текст в ячейке, заключенный в 2 или более строк, что приводит к добавлению ненужных пустых строк и потере контекста извлеченной ячейки.
Вы можете увидеть пример ниже, когда мы попытались извлечь данные из таблицы с помощью tabula-py:
Затем извлеченная информация выводится в DataFrame Pandas вместо строки. В большинстве случаев это может быть желательный формат, но в случае преобразователей, учитывающих текст, эти результаты необходимо преобразовать перед подачей в модель.
По этой причине для решения этой задачи мы по разным причинам использовали библиотеку pdfplumber. Во-первых, он построен на базе pdfminer.six, который мы использовали для предварительного анализа, а это значит, что он содержит похожие объекты. Кроме того, его подход к обнаружению таблиц основан на линейных элементах и их пересечениях, которые создают ячейку, содержащую текст, а затем и саму таблицу. Таким образом, после того как мы идентифицируем ячейку таблицы, мы можем извлечь только содержимое внутри ячейки, не указывая, сколько строк необходимо отобразить. Затем, когда у нас будет содержимое таблицы, мы отформатируем его в виде табличной строки и сохраним в соответствующем списке.
# Extracting tables from the page def extract_table(pdf_path, page_num, table_num): # Open the pdf file pdf = pdfplumber.open(pdf_path) # Find the examined page table_page = pdf.pages[page_num] # Extract the appropriate table table = table_page.extract_tables()[table_num] return table # Convert table into the appropriate format def table_converter(table): table_string = '' # Iterate through each row of the table for row_num in range(len(table)): row = table[row_num] # Remove the line breaker from the wrapped texts 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] # Convert the table into a string table_string+=('|'+'|'.join(cleaned_row)+'|'+'\n') # Removing the last line break table_string = table_string[:-1] return table_string
Для этого мы создали две функции: extract_table() для извлечения содержимого таблицы в список списков и table_converter() для объединения содержимого этих списков. в табличной строке.
В функции extract_table():
- Открываем PDF-файл.
- Переходим на исследуемую страницу PDF-файла.
- Из списка таблиц, найденного на странице pdfplumber, выбираем нужную.
- Мы извлекаем содержимое таблицы и выводим его в виде списка вложенных списков, представляющих каждую строку таблицы.
В функции table_converter():
- Мы перебираем каждый вложенный список и очищаем его контекст от нежелательных разрывов строк, возникающих из любого обернутого текста.
- Мы соединяем каждый элемент строки, разделяя их с помощью | символ для создания структуры ячейки таблицы.
- Наконец, мы добавляем разрыв строки в конце, чтобы перейти к следующей строке.
В результате получится текстовая строка, которая будет представлять содержимое таблицы без потери детализации представленных в ней данных.
Добавляем все вместе
Теперь, когда у нас есть все компоненты кода, давайте добавим их все в полнофункциональный код. Вы можете скопировать код отсюда или найти его вместе с примером PDF в моем репозитории Github здесь.
# Find the PDF path pdf_path = 'OFFER 3.pdf' # create a PDF file object pdfFileObj = open(pdf_path, 'rb') # create a PDF reader object pdfReaded = PyPDF2.PdfReader(pdfFileObj) # Create the dictionary to extract text from each image text_per_page = {} # We extract the pages from the PDF for pagenum, page in enumerate(extract_pages(pdf_path)): # Initialize the variables needed for the text extraction from the page pageObj = pdfReaded.pages[pagenum] page_text = [] line_format = [] text_from_images = [] text_from_tables = [] page_content = [] # Initialize the number of the examined tables table_num = 0 first_element= True table_extraction_flag= False # Open the pdf file pdf = pdfplumber.open(pdf_path) # Find the examined page page_tables = pdf.pages[pagenum] # Find the number of tables on the page tables = page_tables.find_tables() # Find all the elements page_elements = [(element.y1, element) for element in page._objs] # Sort all the elements as they appear in the page page_elements.sort(key=lambda a: a[0], reverse=True) # Find the elements that composed a page for i,component in enumerate(page_elements): # Extract the position of the top side of the element in the PDF pos= component[0] # Extract the element of the page layout element = component[1] # Check if the element is a text element if isinstance(element, LTTextContainer): # Check if the text appeared in a table if table_extraction_flag == False: # Use the function to extract the text and format for each text element (line_text, format_per_line) = text_extraction(element) # Append the text of each line to the page text page_text.append(line_text) # Append the format for each line containing text line_format.append(format_per_line) page_content.append(line_text) else: # Omit the text that appeared in a table pass # Check the elements for images if isinstance(element, LTFigure): # Crop the image from the PDF crop_image(element, pageObj) # Convert the cropped pdf to an image convert_to_images('cropped_image.pdf') # Extract the text from the image image_text = image_to_text('PDF_image.png') text_from_images.append(image_text) page_content.append(image_text) # Add a placeholder in the text and format lists page_text.append('image') line_format.append('image') # Check the elements for tables if isinstance(element, LTRect): # If the first rectangular element if first_element == True and (table_num+1) <= len(tables): # Find the bounding box of the table lower_side = page.bbox[3] - tables[table_num].bbox[3] upper_side = element.y1 # Extract the information from the table table = extract_table(pdf_path, pagenum, table_num) # Convert the table information in structured string format table_string = table_converter(table) # Append the table string into a list text_from_tables.append(table_string) page_content.append(table_string) # Set the flag as True to avoid the content again table_extraction_flag = True # Make it another element first_element = False # Add a placeholder in the text and format lists page_text.append('table') line_format.append('table') # Check if we already extracted the tables from the page 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 # Create the key of the dictionary dctkey = 'Page_'+str(pagenum) # Add the list of list as the value of the page key text_per_page[dctkey]= [page_text, line_format, text_from_images,text_from_tables, page_content] # Closing the pdf file object pdfFileObj.close() # Deleting the additional files created os.remove('cropped_image.pdf') os.remove('PDF_image.png') # Display the content of the page 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().
Проверьте, является ли это прямоугольным элементом. В этом случае мы проверяем, является ли первый прямоугольник частью таблицы страницы, и если да, переходим к следующим шагам:
- Найдите ограничивающую рамку таблицы, чтобы не извлекать ее текст снова с помощью функции text_extraction().
- Извлеките содержимое таблицы и преобразуйте его в строку.
- Затем добавьте логический параметр, чтобы уточнить, что мы извлекаем текст из таблицы.
- Этот процесс завершится после того, как последний LTRect попадет в ограничивающую рамку таблицы и следующий элемент макета не будет прямоугольным объектом. (Все остальные объекты, составляющие таблицу, будут переданы)
Результаты процесса будут храниться в 5 списках на итерацию с именами:
- page_text: содержит текст, поступающий из текстовых контейнеров в PDF-файле (заполнитель будет помещен, когда текст будет извлечен из другого элемента)
- line_format: содержит форматы текстов, извлеченных выше (заполнитель будет помещен, когда текст был извлечен из другого элемента)
- text_from_images: содержит тексты, извлеченные из изображений на странице.
- text_from_tables: содержит табличную строку с содержимым таблиц.
- page_content: содержит весь текст, отображаемый на странице, в списке элементов.
Все списки будут храниться под ключом в словаре, который будет представлять номер просматриваемой каждый раз страницы.
После этого мы закроем PDF-файл.
Затем мы удалим все дополнительные файлы, созданные в процессе.
Наконец, мы можем отобразить содержимое страницы, объединив элементы списка page_content.
Заключение
Я считаю, что это один из подходов, который использует лучшие характеристики многих библиотек и делает процесс устойчивым к различным типам PDF-файлов и элементов, с которыми мы можем столкнуться, однако PDFMiner выполняет большую часть тяжелой работы. Кроме того, информация о формате текста может помочь нам определить потенциальные заголовки, которые могут разделить текст на отдельные логические разделы, а не просто содержание на странице, и могут помочь нам идентифицировать текст, имеющий большую важность.
Однако всегда найдутся более эффективные способы решения этой задачи, и хотя я считаю, что этот подход более инклюзивный, я действительно с нетерпением жду возможности обсудить с вами новые и лучшие способы решения этой проблемы.
📖 Ссылки:
- https://www.techopedia.com/12-practical-large-language-model-llm-applications
- https://www.pdfa.org/wp-content/uploads/2018/06/1330_Johnson.pdf
- Технология https://pdfpro.com/blog/guides/pdf-ocr-guide/#:~:text=OCR считывает текст из PDF-файла с возможностью поиска и редактирования.
- https://pdfminersix.readthedocs.io/en/latest/topic/converting_pdf_to_text.html#id1
- https://github.com/pdfminer/pdfminer.six