Полный процесс извлечения текстовой информации из таблиц, изображений и обычного текста из файла PDF.

Введение

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

Существует множество типов документов, в которых содержится такая неструктурированная информация: от веб-статей и сообщений в блогах до рукописных писем и стихов. Однако значительная часть этих текстовых данных хранится и передается в формате PDF. В частности, было обнаружено, что более 2 миллиардов PDF-файлов открываются в Outlook каждый год, а 73 миллиона новых PDF-файлов сохраняются на Google Диске и в электронной почте ежедневно (2).

Таким образом, разработка более систематического способа обработки этих документов и извлечения из них информации дала бы нам возможность иметь автоматизированный поток и лучше понимать и использовать этот огромный объем текстовых данных. И для этой задачи, конечно же, нашим лучшим другом может быть никто иной, как Python.

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

  1. Файлы PDF, созданные программным способом. Эти PDF-файлы создаются на компьютере с использованием либо технологий W3C, таких как HTML, CSS и Javascript, либо другого программного обеспечения, например Adobe Acrobat. Файлы этого типа могут содержать различные компоненты, такие как изображения, текст и ссылки, которые доступны для поиска и легко редактируются.
  2. Традиционные отсканированные документы. Эти PDF-файлы создаются на неэлектронных носителях с помощью сканера или мобильного приложения. Эти файлы представляют собой не что иное, как набор изображений, хранящихся вместе в файле PDF. При этом элементы, появляющиеся на этих изображениях, такие как текст или ссылки, не могут быть выбраны или найдены. По сути, PDF служит контейнером для этих изображений.
  3. Сканирование документов с помощью оптического распознавания символов. В этом случае после сканирования документа используется программное обеспечение оптического распознавания символов (OCR) для идентификации текста в каждом изображении в файле и преобразования его в текст, доступный для поиска и редактирования. Затем программа добавляет к изображению слой с реальным текстом, и таким образом вы можете выбрать его как отдельный компонент при просмотре файла. (3)

Несмотря на то, что в настоящее время все больше и больше машин оснащены системами оптического распознавания символов, которые идентифицируют текст отсканированных документов, все еще существуют документы, содержащие полные страницы в формате изображения. Вы, наверное, видели это, когда читали отличную статью и пытались выделить предложение, но вместо этого выделяли всю страницу. Это может быть результатом ограничения конкретной машины OCR или ее полного отсутствия. Таким образом, чтобы не оставить эту информацию незамеченной в этой статье, я попытался создать процесс, который также учитывает эти случаи и максимально использует наши ценные и богатые информацией PDF-файлы.

Теоретический подход

Учитывая все эти различные типы PDF-файлов и различные элементы, из которых они состоят, важно выполнить первоначальный анализ макета PDF-файла, чтобы определить подходящий инструмент, необходимый для каждого компонента. В частности, на основе результатов этого анализа мы применим соответствующий метод для извлечения текста из PDF-файла, будь то текст, отображаемый в блоке корпуса с его метаданными, текст внутри изображений или структурированный текст в таблицах. В отсканированном документе без оптического распознавания символов всю тяжелую работу возьмет на себя подход, который идентифицирует и извлекает текст из изображений. Результатом этого процесса будет словарь Python, содержащий информацию, извлеченную для каждой страницы PDF-файла. Каждый ключ в этом словаре будет представлять номер страницы документа, а его соответствующее значение будет списком со следующими 5 вложенными списками, содержащими:

  1. Текст, извлеченный из каждого текстового блока корпуса.
  2. Формат текста в каждом текстовом блоке с точки зрения семейства шрифтов и размера.
  3. Текст, извлеченный из изображений на странице
  4. Текст, извлеченный из таблиц в структурированном формате.
  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

Для достижения этой цели мы следуем следующему процессу:

  1. Мы используем метаданные объекта LTFigure, обнаруженного PDFMiner, для обрезки поля изображения, используя его координаты в макете страницы. Затем мы сохраняем его как новый PDF-файл в нашем каталоге, используя библиотеку PyPDF2.
  2. Затем мы используем функцию convert_from_file() из библиотеки pdf2image, чтобы преобразовать все PDF-файлы в каталоге в список изображений и сохранить их в формате PNG.
  3. Наконец, теперь, когда у нас есть файлы изображений, мы читаем их в нашем скрипте, используя пакет 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():

  1. Открываем PDF-файл.
  2. Переходим на исследуемую страницу PDF-файла.
  3. Из списка таблиц, найденного на странице pdfplumber, выбираем нужную.
  4. Мы извлекаем содержимое таблицы и выводим его в виде списка вложенных списков, представляющих каждую строку таблицы.

В функции table_converter():

  1. Мы перебираем каждый вложенный список и очищаем его контекст от нежелательных разрывов строк, возникающих из любого обернутого текста.
  2. Мы соединяем каждый элемент строки, разделяя их с помощью | символ для создания структуры ячейки таблицы.
  3. Наконец, мы добавляем разрыв строки в конце, чтобы перейти к следующей строке.

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

Добавляем все вместе

Теперь, когда у нас есть все компоненты кода, давайте добавим их все в полнофункциональный код. Вы можете скопировать код отсюда или найти его вместе с примером 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().

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

  1. Найдите ограничивающую рамку таблицы, чтобы не извлекать ее текст снова с помощью функции text_extraction().
  2. Извлеките содержимое таблицы и преобразуйте его в строку.
  3. Затем добавьте логический параметр, чтобы уточнить, что мы извлекаем текст из таблицы.
  4. Этот процесс завершится после того, как последний LTRect попадет в ограничивающую рамку таблицы и следующий элемент макета не будет прямоугольным объектом. (Все остальные объекты, составляющие таблицу, будут переданы)

Результаты процесса будут храниться в 5 списках на итерацию с именами:

  1. page_text: содержит текст, поступающий из текстовых контейнеров в PDF-файле (заполнитель будет помещен, когда текст будет извлечен из другого элемента)
  2. line_format: содержит форматы текстов, извлеченных выше (заполнитель будет помещен, когда текст был извлечен из другого элемента)
  3. text_from_images: содержит тексты, извлеченные из изображений на странице.
  4. text_from_tables: содержит табличную строку с содержимым таблиц.
  5. page_content: содержит весь текст, отображаемый на странице, в списке элементов.

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

После этого мы закроем PDF-файл.

Затем мы удалим все дополнительные файлы, созданные в процессе.

Наконец, мы можем отобразить содержимое страницы, объединив элементы списка page_content.

Заключение

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

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

📖 Ссылки:

  1. https://www.techopedia.com/12-practical-large-language-model-llm-applications
  2. https://www.pdfa.org/wp-content/uploads/2018/06/1330_Johnson.pdf
  3. Технология https://pdfpro.com/blog/guides/pdf-ocr-guide/#:~:text=OCR считывает текст из PDF-файла с возможностью поиска и редактирования.
  4. https://pdfminersix.readthedocs.io/en/latest/topic/converting_pdf_to_text.html#id1
  5. https://github.com/pdfminer/pdfminer.six