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

Чтобы дать ответ на все эти вопросы, нам на помощь приходит Python для задач NLP (Natural language processing). Разберем код, решающий задачу форматирования PDF в TXT. Ведь именно хорошо представленные данные позволяют нам проводить качественный и количественный анализ текста.
Алгоритм следующий:
Загружаем страницу учебника и конвертируем в PDF.
Если изображение плохого качества, препроцессим - улучшая качество(preprocess_image).
Используем EasyOCR для распознавания текста.
Собираем текст в блоки с номером страницы.
Если страница “Битая” - обрабатываем ошибки.
Для того, чтобы наш результат был максимально точным используем 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 , Тг-канал
Комментарии (4)
nilske
01.09.2025 15:53fitz это сильно устаревшее название библиотеки, но его всегда упорно советует ИИ, который видимо и является автором вашего кода. И эта библиотека имеет встроенную поддержку OCR, основанную на Tesseract.
kruglikle Автор
01.09.2025 15:53Спасибо, fitz - это модуль pymupdf, оставшийся для совместимости. импортировать можно как его, так и pymupdf. Для ocr мы используем easy ocr
onkruglikov
Удивительно, у нас одинаковая фамилия и я тоже пишу на python. Кажется, что это судьба