Учебник под микроскопом. Часть 1: из PDF в TXT

от автора

Недавно мы с научным руководителем задались вопросами: Какая лексика чаще всего встречается в учебнике, а какая появляется всего один раз? Какие упражнения присутствуют чаще – языковые или коммуникативные? Соответствует ли лексика в учебнике заявленному уровню? Сколько всего текстов в учебнике? О чем большинство?

Чтобы дать ответ на все эти вопросы, нам на помощь приходит Python для задач NLP (Natural language processing). Разберем код, решающий задачу форматирования PDF в TXT. Ведь именно хорошо представленные данные позволяют нам проводить качественный и количественный анализ текста. 

Алгоритм следующий:

  1. Загружаем страницу учебника и конвертируем в PDF.

  2. Если изображение плохого качества, препроцессим — улучшая качество(preprocess_image).

  3. Используем EasyOCR для распознавания текста.

  4. Собираем текст в блоки с номером страницы.

  5. Если страница “Битая” — обрабатываем ошибки.

Для того, чтобы наш результат был максимально точным используем OCR (Optical Character Recognition), предназначенного для оптического распознавания текста, даже самый «сложный» PDF-учебник можно быстро превратить в текстовый файл.

Для чего это нужно:

  • Учитель получает готовый текст для анализа словарного материала.

  • Разработчик видит пример работы с PDF, изображениями, OCR и многопоточностью.

  • Методист получает инструмент для проверки учебников и сопоставления лексики между изданиями.

Реализация

Для начала необходимо импортировать библиотеки для работы с файлами, потоками, временем (os, io, time), а также библиотеку concurrent.futures для многопоточных задач, а также: numpy, cv2 (про open cv и его возможности). Для работы с PDF необходимы fitz и easyocr для извлечения текстов с картинок и PIL для работы с изображениями.

Пример для Visual Code (version 3.11.9.).

Начнем с конфигурации:

#Конфигурация PDF_PATH = r"C:\Учебники\Кузовлев_3кл.pdf" # путь к учебнику OUTPUT_TXT = os.path.join(os.path.dirname(PDF_PATH), "kuzovlev_output.txt") LANGUAGES = ['en', 'ru']  THREADS = 4  # количество потоков. можно менять числа, но распознавание не особо улучшается  USE_GPU = True #GPU для ускорения обработки PREPROCESS_IMAGES = True #улучшение качества картинок 

Препроцессинг изображения

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

Важно: Если скан бледный(ура, у учебников это в большинстве случаев), то пиксели занимают узкий диапазон 50–180 и буквы не четко отделяются. Растяжение даёт полный 0–255 диапазон, повышая контраст. Однако возможна ошибка деления на ноль, если img.max() == img.min() (однотонная картинка). Поэтому необходима нормализация яркости— линейное растяжение контрастного диапазона (contrast stretching).
Альтернативы: cv2.normalize, skimage.exposure.rescale_intensity, CLAHE (локальное усиление контраста).

#Улучшение качества изображения def preprocess_image(img):     img = img.convert('L') # перевод в градации серого     img = img.resize((img.width  2, img.height  2), Image.LANCZOS) # масштабирование ×2 с сохранением качества     img = np.array(img) #необходим перевод PIL.Image в NumPy-массив, потому что OpenCV функции работают с массивами.     # нормализация яркости (растяжение контраста до 0–255)     img = img.astype(np.float32)     img = (img - img.min()) * (255 / (img.max() - img.min()))     img = np.clip(img, 0, 255).astype(np.uint8)     # бинаризация (OTSU подбирает оптимальный порог)     img = cv2.threshold(img, 0, 255, cv2.THRESHBINARY + cv2.THRESH_OTSU)     return Image.fromarray(img) 

Конвертация PDF в текст

Функция обработки страницы  отвечает за извлечение текста с одной страницы PDF. Она загружает страницу по номеру и конвертирует её в изображение с высоким разрешением (300 DPI), затем превращает это изображение в пискельный объект. При включённой опции предобработки изображение улучшается: переводится в градации серого, масштабируется, нормализуется яркость и бинаризуется методом Оцу (об этом читать выше) для лучшего распознавания. Далее изображение передаётся в EasyOCR, который распознаёт текст блоками, объединяет строки в абзацы и игнорирует мелкие элементы.

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

#Обработка одной страницы def process_page(args):     page_num, doc, reader = args     try:         page = doc.load_page(page_num)         pix = page.get_pixmap(dpi=300)         img_data = Image.open(io.BytesIO(pix.tobytes("png")))         if PREPROCESS_IMAGES:             img_data = preprocess_image(img_data)         result = reader.readtext(np.array(img_data),                                  batch_size=10, #обрабатываем блоками для ускорения                                  detail=0, #возвращаем только текст, без координат                                  paragraph=True, #paragraph=True — объединяем строки в абзацы                                  min_size=10, #min_size=10 — минимальный размер текста для распознавания                                   contrast_ths=0.3) #порог чувствительности к контрасту         return f"\n=== Страница {page_num+1} ===\n" + "\n".join(result)     except Exception as e:         return f"\nОшибка на странице {page_num+1}: {str(e)}" 

Основной код

Спойлер: В начале инициализируется OCR-модель и открывается документ, затем подсчитывается количество страниц. Обработка страниц выполняется параллельно через ThreadPoolExecutor (класс из модуля concurrent.futures), где каждая страница распознаётся предыдущей функцией process_page. Результаты собираются в список, сохраняются в текстовый файл и выводится прогресс обработки. В конце показывается затраченное время и средняя скорость распознавания страниц. 

#Главная функция def main():     #Запоминаем время начала выполнения программы для последующего расчета времени обработки     start_time = time.time()          #Выводим сообщение о начале инициализации EasyOCR     print("Инициализация EasyOCR...")          #Создаем объект reader для распознавания текста с использованием EasyOCR     # LANGUAGES - список языков, которые будут использоваться для распознавания     # USE_GPU - флаг, указывающий, использовать ли GPU для ускорения обработки     # model_storage_directory - путь к директории хранения модели (None означает использование стандартной)     # download_enabled - если True, разрешает автоматическую загрузку моделей (False отключает)     reader = easyocr.Reader(LANGUAGES,                             gpu=USE_GPU,                             model_storage_directory=None,                             download_enabled=False)       #Открываем PDF-документ по заданному пути     doc = fitz.open(PDF_PATH)          #Получаем общее количество страниц в документе     total_pages = len(doc)     print(f"Начало обработки {total_pages} страниц...")      #Список для хранения результатов обработки страниц     results = []          #Создаем пул потоков для параллельной обработки страниц     with concurrent.futures.ThreadPoolExecutor(max_workers=THREADS) as executor:         #Отправляем задачи на обработку каждой страницы в пул потоков         futures = [executor.submit(process_page, (page_num, doc, reader))                    for page_num in range(total_pages)]           #Обрабатываем завершенные задачи по мере их завершения         for i, future in enumerate(concurrent.futures.as_completed(futures), 1):             #Получаем результат обработки страницы и добавляем его в список результатов             results.append(future.result())             #Выводим информацию о количестве обработанных страниц, обновляя строку в терминале             print(f"Обработано {i}/{total_pages} страниц", end='\r')       #Открываем файл для записи результатов в текстовом формате с кодировкой UTF-8     with open(OUTPUT_TXT, "w", encoding="utf-8") as f:         #Записываем все результаты, разделенные новой строкой         f.write("\n".join(results))      #Вычисляем общее время выполнения программы     elapsed = time.time() - start_time          print(f"\nГотово! Время обработки: {elapsed//60:.0f} мин {elapsed%60:.2f} сек")     print(f"Средняя скорость: {total_pages/(elapsed/60):.1f} страниц/мин")     print(f"Результат сохранен в: {OUTPUT_TXT}") #Проверяем, что этот файл запускается как основная программа if __name__ == "__main__":     main()  

Итого

Так, Python, EasyOCR и простые приёмы предобработки изображений позволяют превратить даже PDF-учебник в удобный текстовый файл для дальнейшего лингвистического анализа (частота встречаемости лексики, типы упражнений, составление учебных корпусов текстов и т.д.). 

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

Ссылки на нас: Git проекта , EduText Analyzer , Тг-канал


ссылка на оригинал статьи https://habr.com/ru/articles/942866/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *