pdfplumber помогает извлекать из PDF точный текст, слова с координатами, таблицы, линии, прямоугольники, аннотации и метаданные, а затем проверять результат на изображении страницы. Основные инструменты — объекты PDF и Page, методы extract_text(), extract_words(), extract_table(), обрезка по координатам и визуальная отладка PageImage.
Работа обычно начинается с открытия файла, выбора страницы и проверки того, какие объекты действительно записаны внутри документа. После этого текст можно собирать с обычным или приближённым сохранением макета, таблицы — находить по линиям либо выравниванию слов, а проблемные области — изолировать через crop() и настраивать отдельно.
У pdfplumber нет графического меню с кнопками импорта и экспорта: управление выполняется командами Python или через консольный вызов. Такой подход требует точных параметров, зато позволяет повторять обработку сотен файлов, сохранять координаты каждого фрагмента и строить проверяемые правила для документов одного шаблона.
Скачать pdfplumber
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет встроенного OCR
- Нет графического меню
- Не редактирует PDF
Как устроен рабочий процесс pdfplumber
Программа рассматривает PDF не как готовую страницу с абзацами и таблицами, а как набор низкоуровневых объектов: символов, линий, прямоугольников, кривых, изображений и аннотаций. Это важное различие. Внутри файла слово может быть представлено десятком отдельных знаков с абсолютными координатами, а граница таблицы — четырьмя линиями, сторонами прямоугольника или вообще только визуальным выравниванием текста. Поэтому надёжный сценарий строится от проверки объектов к извлечению, а не наоборот.
Типовая последовательность состоит из пяти шагов. Сначала файл открывают через pdfplumber.open(). Затем выбирают одну страницу и смотрят её размеры, число символов, линий и прямоугольников. После этого запускают простой метод извлечения и оценивают, насколько результат соответствует чтению человеком. Если порядок или столбцы нарушены, страницу визуализируют, ограничивают рабочую область и меняют параметры группировки. В конце результат нормализуют и проверяют автоматическими условиями: числом колонок, обязательными заголовками, диапазонами значений и отсутствием пустых строк.
Такой порядок особенно полезен для отчётов, счетов, реестров и форм, которые регулярно формируются одной системой. Один правильно настроенный набор координат и допусков можно применять ко всей партии. Для документов с разными макетами обычно создают несколько профилей и выбирают профиль по текстовому маркеру, размеру страницы или расположению заголовка.
Установка и первая проверка
Пакет устанавливается стандартной командой Python. Практичнее выполнять её в отдельном виртуальном окружении, чтобы версии библиотек обработки PDF не конфликтовали с другими проектами. После установки полезно сразу проверить импорт и обработать небольшой машинно-сформированный документ, в котором текст выделяется обычным курсором.
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux и macOS
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pdfplumber
Минимальная проверка должна не только вывести текст, но и показать число страниц и размеры первой страницы. Если файл открывается, однако extract_text() возвращает None или пустую строку, причина чаще всего находится в самом PDF: страница содержит только растровое изображение, текст переведён в контуры, шрифт не имеет корректной карты Unicode либо полезный слой закрыт особенностями структуры документа.
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
print("Страниц:", len(pdf.pages))
page = pdf.pages[0]
print("Размер:", page.width, page.height)
print(page.extract_text())
Контекстный менеджер with предпочтительнее ручного открытия: он закрывает поток файла после завершения блока. При пакетной обработке это предотвращает накопление открытых дескрипторов и позволяет перемещать или удалять обработанные документы без задержки со стороны операционной системы.
Объекты PDF и страницы
Объект PDF содержит список pages и словарь metadata. В метаданных могут присутствовать автор, заголовок, производитель, дата создания и дата изменения, но эти поля необязательны и нередко заполнены неточно. Их стоит использовать как дополнительный признак, а не как единственный способ классификации документов.
Основная работа выполняется с объектом Page. Свойства page_number, width и height описывают номер и геометрию страницы. Списки chars, lines, rects, curves, images, annots и hyperlinks дают доступ к найденным объектам. Каждый элемент представлен словарём, поэтому его можно фильтровать обычными средствами Python.
with pdfplumber.open("document.pdf") as pdf:
page = pdf.pages[0]
print("Символы:", len(page.chars))
print("Линии:", len(page.lines))
print("Прямоугольники:", len(page.rects))
print("Кривые:", len(page.curves))
print("Изображения:", len(page.images))
print("Первый символ:", page.chars[0])
У символа обычно доступны текст, имя шрифта, размер, ширина, высота, координаты, матрица преобразования, направление и цвета. Это позволяет отличать заголовки по размеру шрифта, находить красные предупреждения, отбрасывать колонтитулы и восстанавливать структуру там, где простой текстовый вывод смешивает элементы.
У линий и прямоугольников есть координаты, толщина и цвета. Для таблиц особенно важны производные свойства rect_edges, curve_edges и edges: они превращают стороны прямоугольников и кривых в общий набор рёбер. Поэтому рамка, нарисованная как залитый прямоугольник, всё равно может участвовать в поиске ячеек при стратегии lines.
Координаты и система отсчёта
Большинство операций использует ограничивающий прямоугольник вида (x0, top, x1, bottom). Значение x0 отсчитывается от левого края, x1 — координата правого края, top и bottom — расстояния от верхней границы страницы. Одновременно в словарях объектов могут присутствовать y0 и y1, измеряемые от нижнего края. Смешивание этих систем — частая причина пустой обрезки или выделения не той области.
Перед тем как закреплять координаты в коде, полезно вывести размеры страницы и координаты найденного заголовка. Если документы могут быть в разных ориентациях или иметь нестандартный CropBox, абсолютные числа лучше нормализовать относительно ширины и высоты. Например, область правой половины страницы можно определить как (page.width / 2, 0, page.width, page.height).
page_width = page.width
page_height = page.height
right_half = page.crop((page_width / 2, 0, page_width, page_height))
Свойство doctop задаёт вертикальную позицию относительно начала всего документа, а не отдельной страницы. Оно удобно для объединения слов из многостраничного файла в единую последовательность. Если собственный алгоритм сортирует элементы только по top, верхние строки каждой страницы будут иметь одинаковые значения и перемешаются; сортировка по doctop сохраняет порядок страниц.
Матрица символа пригодится для повёрнутого текста. Класс CTM из модуля pdfplumber.ctm помогает получить угол наклона из матрицы преобразования. Это позволяет отдельно обрабатывать вертикальные подписи осей, боковые заголовки таблиц или штампы, не смешивая их с основным чтением слева направо.
Открытие файлов, паролей и потоков
pdfplumber.open() принимает путь, бинарный файловый объект или совместимый поток. Поток полезен, когда PDF уже получен из базы данных, HTTP-клиента или контейнера хранения и не требуется промежуточная запись на диск. Важно передавать байты, а не строковый текст: внутренний парсер ожидает бинарную структуру PDF.
from io import BytesIO
import pdfplumber
pdf_bytes = load_from_storage()
with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
Для защищённого файла используется аргумент password. Если пароль неверен или политика шифрования не допускает извлечение, открытие завершится исключением. Пароли не следует записывать в исходный код и журналы; безопаснее получать их из переменной окружения или менеджера секретов.
with pdfplumber.open("protected.pdf", password=secret) as pdf:
print(pdf.pages[0].extract_text())
Параметр unicode_norm может заранее нормализовать текст в форму NFC, NFD, NFKC или NFKD. Это полезно, когда одинаково выглядящие символы записаны разными последовательностями Unicode. Для поиска и сопоставления чаще выбирают NFC или NFKC, однако совместимая нормализация NFKC способна заменить некоторые типографские знаки на более простые эквиваленты, поэтому исходные значения лучше сохранять отдельно, если важна юридическая точность.
Некорректные значения метаданных по умолчанию вызывают предупреждение, а не прерывают обработку. В режиме strict_metadata=True такие проблемы становятся исключением. Строгий режим полезен в контролируемом конвейере, где любое повреждение должно остановить импорт; для больших коллекций разнородных файлов обычно удобнее фиксировать предупреждение и продолжать работу.
Извлечение обычного текста
extract_text() группирует отдельные символы в строки. При layout=False пробел добавляется, когда горизонтальный зазор между соседними символами превышает x_tolerance, а перевод строки — когда вертикальная разница превышает y_tolerance. Эти параметры измеряются в пунктах PDF, а не в пикселях изображения.
text = page.extract_text(
x_tolerance=2,
y_tolerance=3,
layout=False,
)
print(text)
Слишком маленький x_tolerance разбивает слово на отдельные фрагменты, особенно при разреженном шрифте. Слишком большой объединяет соседние колонки. Значение следует подбирать на одной репрезентативной странице, а затем проверять на документах с наиболее длинными и короткими значениями.
Параметр x_tolerance_ratio задаёт динамический допуск как долю размера предыдущего символа. Он полезен в документах, где рядом встречаются крупные заголовки и мелкий табличный текст: фиксированный зазор, подходящий для одного размера, может быть неверным для другого. При включённом коэффициенте допустимое расстояние масштабируется вместе со шрифтом.
Метод extract_text_simple() выполняет более простой и немного более быстрый вариант группировки. Его уместно использовать в массовом проходе по однотипным документам, когда не нужны дополнительные параметры направления, пунктуации и макета. Перед заменой основного метода стоит сравнить результаты на страницах с колонками, сносками и повёрнутыми подписями.
Нельзя считать любой текстовый вывод точным воспроизведением чтения человеком. PDF хранит позиционирование, а не логическую последовательность абзацев. Двухколоночная статья может извлечься построчно через обе колонки; подпись рисунка способна оказаться в середине предложения; скрытый текстовый слой после OCR иногда содержит повторные символы. Решение строится на координатах и фильтрации, а не на одном вызове метода.
Режим сохранения макета
При layout=True метод пытается приблизить расположение текста к странице, вставляя пробелы и переводы строк в соответствии с координатной сеткой. Параметры x_density и y_density определяют плотность этой сетки. Результат удобен для фиксированных отчётов и быстрого визуального сравнения, но не превращает PDF в настоящий текстовый документ с абзацами и колонками.
layout_text = page.extract_text(
layout=True,
x_density=7.25,
y_density=13,
)
print(layout_text)
Если строки выглядят слишком растянутыми, увеличивают плотность по горизонтали; если столбцы слипаются — уменьшают её или переходят к извлечению слов с координатами. По вертикали аналогично: чрезмерное число пустых строк связано с масштабом сетки и реальными интервалами между элементами.
Параметры line_dir_render и char_dir_render управляют направлением вывода для строк и символов. Допустимы направления сверху вниз, снизу вверх, слева направо и справа налево. Их применяют осознанно: изменение направления не исправляет неверный порядок объектов в исходном PDF, а только задаёт способ рендеринга уже сгруппированного текста.
Для экспорта в моноширинный текстовый файл режим макета бывает удобнее обычного извлечения. Для последующей аналитики, поиска полей и построения таблиц чаще надёжнее работать с extract_words(), потому что каждый фрагмент сохраняет координаты и может быть отнесён к конкретной области.
Слова, границы и атрибуты
extract_words() возвращает список словарей. В каждом находятся текст, границы x0, x1, top, bottom, высота, ширина и направление. Такой результат является базой для собственных правил: можно выбрать все слова правее заданной линии, сгруппировать их по строкам, найти ближайшее значение к подписи или восстановить колонки по общим координатам.
words = page.extract_words()
for word in words[:10]:
print(word["text"], word["x0"], word["top"])
keep_blank_chars=True сохраняет пробельные символы внутри слова. Это редко нужно для обычного текста, но помогает при фиксированных кодах, где пробел является частью значения. use_text_flow=True ориентируется на внутренний поток символов, похожий на порядок выделения курсором. Такой порядок иногда лучше геометрической сортировки, однако в сложных PDF может выглядеть нелогично, поэтому его нельзя включать без проверки.
Параметры line_dir и char_dir задают ожидаемые направления для обычного текста, а line_dir_rotated и char_dir_rotated — для повёрнутого. Это важно в таблицах с вертикальными заголовками: без разделения направлений символы могут объединяться в неверной последовательности.
extra_attrs запрещает объединять в одно слово символы с разными выбранными атрибутами. Например, extra_attrs=["fontname", "size"] разделит фрагмент на границе смены шрифта или размера. При распознавании заголовков и значений это предотвращает склейку жирной подписи с обычным текстом.
split_at_punctuation=True разбивает слова по знакам пунктуации. Вместо логического значения можно передать строку с конкретными разделителями. Это удобно для кодов вида ABC-123: если дефис значим, его не включают в набор разделителей; если нужно получить две части, дефис добавляют.
Лигатуры вроде fi по умолчанию разворачиваются в обычные буквы. Параметр expand_ligatures=False сохраняет исходный знак. Для поиска по естественным словам разворачивание полезно, а для побайтного сравнения и типографического анализа — нет. return_chars=True добавляет к каждому слову список составляющих символов, что позволяет проверить шрифт, цвет и точную геометрию отдельных букв.
Поиск текста и регулярные выражения
Метод search() возвращает не только найденную строку, но и координаты совпадения. Это позволяет искать подпись Итого, а затем извлекать число справа от неё, выделять обязательные маркеры на изображении или определять тип документа по расположению заголовка.
matches = page.search(r"Итого\s*:?", regex=True, case=False)
for match in matches:
print(match["text"], match["x0"], match["top"])
При regex=False шаблон трактуется как обычная строка. case=False включает поиск без учёта регистра. main_group ограничивает координаты конкретной группой регулярного выражения: например, можно найти всю конструкцию Номер счёта: 12345, но вернуть границы только числа.
Нулевые совпадения и совпадения из одних пробелов отбрасываются, потому что у них обычно нет осмысленной области на странице. Если задача состоит в обнаружении пустого поля, следует искать соседнюю подпись и проверять, отсутствуют ли слова в ожидаемом прямоугольнике.
Для устойчивой автоматизации лучше разделять поиск маркера и извлечение значения. Сначала определяют координаты подписи, затем создают относительно неё область и выбирают слова внутри. Такой алгоритм переживает небольшие сдвиги макета лучше, чем жёсткая строка регулярного выражения по всему извлечённому тексту.
label = page.search("Invoice No", regex=False)[0]
value_area = (
label["x1"] + 4,
label["top"] - 2,
min(page.width, label["x1"] + 160),
label["bottom"] + 4,
)
value = page.crop(value_area).extract_text()
Удаление повторных символов
Некоторые PDF содержат дублирующий текстовый слой: один и тот же символ нарисован несколько раз почти в одинаковой позиции. Визуально страница выглядит нормально, но извлечённый текст превращается в ддввооййнныыее буквы. Метод dedupe_chars() создаёт производную страницу без совпадающих символов.
clean_page = page.dedupe_chars(
tolerance=1,
extra_attrs=("fontname", "size"),
)
text = clean_page.extract_text()
tolerance задаёт допустимую разницу координат, а extra_attrs — атрибуты, которые тоже должны совпадать. Слишком большой допуск может удалить настоящие символы в плотном тексте или теневой эффект, поэтому сначала сравнивают количество chars до и после обработки.
Дедупликация не исправляет OCR-ошибки и не удаляет повторяющиеся строки, расположенные в разных местах. Она предназначена именно для наложенных символов. Если на странице есть два одинаковых колонтитула на разных координатах, они сохранятся и должны фильтроваться по области.
Обрезка и фильтрация области
crop() возвращает страницу, ограниченную прямоугольником. Объекты, пересекающие границу частично, подрезаются. Это удобно, когда на одной странице находятся основная таблица, заголовок, сноски и декоративные элементы: алгоритм работает только с нужной зоной и не принимает посторонние линии за границы ячеек.
table_area = page.crop((30, 90, page.width - 30, 520))
rows = table_area.extract_table()
При relative=True координаты считаются относительно текущей области страницы. Такой режим полезен для последовательных обрезок: сначала удаляют поля, затем задают прямоугольник внутри оставшейся части. При strict=True область обязана полностью лежать внутри страницы; отключение строгого режима допускает выход границ наружу, но обычно скрывает ошибку в расчётах.
within_bbox() оставляет только объекты, полностью попавшие внутрь прямоугольника. В отличие от crop(), частично пересекающиеся элементы исключаются. Это подходит для выбора слов, которые должны целиком находиться в колонке. outside_bbox() делает обратное и полезен для удаления штампа, боковой панели или зоны с примечаниями.
filter() принимает функцию и оставляет объекты, для которых она возвращает истину. Например, можно исключить символы определённого цвета, тонкие линии или текст меньше заданного размера. Следует помнить, что визуализация to_image() корректно отражает обрезанные страницы, но не встраивает изменения FilteredPage в фоновый рендер. Для проверки фильтра лучше рисовать оставшиеся объекты поверх изображения исходной страницы.
large_text = page.filter(
lambda obj: obj.get("object_type") != "char"
or obj.get("size", 0) >= 10
)
print(large_text.extract_text())
Координаты для обрезки лучше получать из найденных маркеров, а не задавать вручную. Например, верх таблицы можно привязать к нижней границе заголовка, а низ — к верхней границе слова Примечания. Такой подход выдерживает изменение числа строк в шапке и небольшие сдвиги при печати.
Визуальная отладка PageImage
to_image() превращает страницу или обрезанную область в объект PageImage. Он отображается как результат ячейки Jupyter, открывается методом show() и сохраняется через save(). Визуальная проверка нужна не для украшения отчёта, а для ответа на конкретные вопросы: видит ли парсер линии, где проходят границы слов, почему колонка не замкнулась и какие объекты попали в обрезку.
image = page.to_image(
resolution=150,
antialias=True,
)
image.save("page-debug.png", quantize=False)
resolution задаёт плотность в точках на дюйм. Вместо неё можно указать ширину или высоту изображения. antialias=True сглаживает текст и линии, но увеличивает размер файла. force_mediabox=True заставляет использовать MediaBox вместо CropBox, что помогает при диагностике документов, где видимая область обрезана относительно физического размера страницы.
reset() очищает нанесённые пометки, copy() создаёт копию, draw_line(), draw_vline(), draw_hline(), draw_rect() и draw_circle() рисуют поверх страницы. Есть массовые варианты методов для списков объектов. Им можно передавать не только координаты, но и словари символов, линий и прямоугольников, которые уже содержат нужные границы.
При сохранении PNG по умолчанию используется квантование палитры. Для мелкого текста и полупрозрачных заливок полезно указать quantize=False, иначе близкие цвета могут слиться. Для служебных изображений с простыми контурами стандартная палитра уменьшает размер без заметной потери.
Как pdfplumber находит таблицы
Алгоритм сначала собирает явные линии и линии, подразумеваемые выравниванием слов. Затем близкие или перекрывающиеся сегменты объединяются, вычисляются пересечения вертикальных и горизонтальных рёбер, из пересечений строятся минимальные прямоугольные ячейки, а соседние ячейки группируются в таблицы. Из-за этой последовательности отсутствие одного короткого сегмента может повлиять на целую колонку.
find_tables() возвращает список объектов Table. У каждого доступны cells, rows, columns, bbox и метод extract(). find_table() выбирает таблицу с наибольшим числом ячеек, а при равенстве — ближайшую к верхнему краю. Это правило важно: самая большая не обязательно является нужной, если на странице есть основная ведомость и большая сетка формы.
extract_tables() возвращает структуру таблица — строка — ячейка, а extract_table() — строки и ячейки одной выбранной таблицы. Пустая ячейка обычно представлена None или пустым текстом в зависимости от структуры. Перед записью в CSV значения нормализуют явно, чтобы отличать отсутствие ячейки от присутствующей, но пустой ячейки.
tables = page.find_tables()
for index, table in enumerate(tables):
print(index, table.bbox, len(table.rows), len(table.columns))
data = table.extract()
for row in data[:3]:
print(row)
debug_tablefinder() возвращает объект TableFinder с коллекциями edges, intersections, cells и tables. Это самый быстрый способ понять, на каком этапе возникла ошибка. Если нужной линии нет в edges, меняют стратегию или фильтрацию рёбер. Если линии есть, но не соединяются, настраивают допуск пересечений. Если ячейки построены, а текст распределён неверно, корректируют текстовые допуски.
Стратегии распознавания границ таблицы
vertical_strategy и horizontal_strategy принимают значения lines, lines_strict, text или explicit. Вертикальную и горизонтальную стратегии можно комбинировать. Например, вертикальные границы брать из выравнивания слов, а горизонтальные — из напечатанных линий.
Стратегия lines
lines использует графические линии и стороны прямоугольников. Это основной выбор для отчётов с полноценной сеткой. Если фон ячейки нарисован прямоугольником, его стороны тоже считаются потенциальными границами. Недостаток проявляется в дизайнерских формах: цветные панели и рамки заголовков могут создать лишние ячейки.
Стратегия lines_strict
lines_strict учитывает линии, но не стороны прямоугольников. Она помогает, когда заливки и декоративные блоки дают множество ложных границ. Если настоящая таблица тоже построена прямоугольниками, строгий режим, наоборот, потеряет её; это сразу видно по пустому набору вертикальных или горизонтальных рёбер в отладке.
Стратегия text
text выводит воображаемые вертикальные линии по совпадающим левым, правым или центральным координатам слов, а горизонтальные — по их верхним границам. Этот режим предназначен для таблиц без нарисованной сетки. Он чувствителен к качеству группировки слов, числу совпадающих элементов и постороннему тексту, поэтому страницу почти всегда предварительно обрезают.
Стратегия explicit
explicit использует только координаты из explicit_vertical_lines и explicit_horizontal_lines. В списках можно передавать числа, означающие линию на всю высоту или ширину области, а также реальные объекты линий, прямоугольников и кривых. Это наиболее предсказуемый вариант для стабильного шаблона, если границы известны заранее.
settings = {
"vertical_strategy": "explicit",
"explicit_vertical_lines": [40, 120, 260, 420, 560],
"horizontal_strategy": "text",
"min_words_horizontal": 2,
}
rows = page.extract_table(settings)
Координаты явных линий лучше хранить как доли ширины страницы, если система-генератор иногда формирует Letter и A4 или меняет масштаб. При фиксированном шаблоне абсолютные пункты дают более точный результат. Полезно проверять, что последняя вертикальная граница действительно правее самого правого текста; иначе крайняя колонка не образует замкнутых ячеек.
Допуски snap, join и intersection
snap_tolerance объединяет параллельные линии, находящиеся рядом, в одну координату. Отдельные значения snap_x_tolerance и snap_y_tolerance позволяют настраивать направления независимо. Этот параметр помогает при сканировании через промежуточный PDF-принтер, когда формально одинаковые линии записаны с разницей в доли пункта.
join_tolerance соединяет сегменты, лежащие на одной бесконечной линии, если их концы достаточно близки. Вертикальный и горизонтальный допуски также можно разделить. Слишком маленькое значение оставляет разрывы, из-за которых не строятся ячейки; слишком большое соединяет независимые линии разных блоков.
intersection_tolerance определяет, насколько близко должны подходить перпендикулярные рёбра, чтобы считаться пересекающимися. Если визуально линия заканчивается у границы, но в данных не дотягивается на один-два пункта, небольшое увеличение восстанавливает вершину. Большой допуск создаёт ложные пересечения между соседними элементами.
Настраивать эти параметры лучше по отладочному изображению и числам TableFinder. Изменяют один тип допуска за раз, сохраняют кадр и сравнивают количество рёбер, пересечений и ячеек. Одновременное увеличение всех значений может случайно дать прямоугольную сетку, которая не соответствует смыслу таблицы.
finder = page.debug_tablefinder({
"snap_x_tolerance": 2,
"snap_y_tolerance": 4,
"join_x_tolerance": 2,
"join_y_tolerance": 5,
"intersection_x_tolerance": 3,
"intersection_y_tolerance": 3,
})
print(len(finder.edges), len(finder.intersections), len(finder.cells))
Минимальная длина рёбер и пунктир
edge_min_length отбрасывает короткие рёбра перед реконструкцией таблицы. Увеличение значения удаляет засечки, подчёркивания и декоративные фрагменты, но может потерять маленькие ячейки. edge_min_length_prefilter действует ещё на раннем этапе. Его уменьшение помогает сохранить короткие сегменты пунктирной линии, которые иначе исчезнут до объединения.
Для пунктирных таблиц логика обычно такая: снизить предварительный порог, увеличить допуск соединения вдоль направления линии и проверить, не объединились ли штрихи из соседних строк. Если пунктир состоит из очень мелких прямоугольников, стратегия lines может видеть их стороны, а lines_strict — нет.
Не следует автоматически ставить нулевой порог для всех документов. На страницах с мелким шрифтом и подчеркиваниями это создаст тысячи рёбер и замедлит поиск. Правильнее активировать профиль для конкретного типа бланка после проверки заголовка или размера страницы.
Порог числа слов для таблиц без сетки
При стратегии text параметр min_words_vertical задаёт, сколько слов должно иметь общее выравнивание, чтобы возникла вертикальная граница. min_words_horizontal выполняет аналогичную роль по горизонтали. Низкий порог на насыщенной странице создаёт линии по случайным совпадениям, высокий пропускает короткие таблицы.
Для ведомости с десятками строк вертикальный порог можно повысить: настоящие колонки повторяются много раз. Для таблицы из трёх строк значение должно быть небольшим. Если в одной колонке много пустых ячеек, выравнивание по словам может не набрать порог; тогда границу задают явно или комбинируют text с реальными линиями.
Параметры text_x_tolerance и text_y_tolerance участвуют не только в извлечении текста ячеек, но и в поиске слов для стратегии text. Если отдельное значение распадается на части, меняется и геометрия предполагаемых колонок. Поэтому настройку начинают с корректного extract_words(), а уже затем оценивают сетку.
Извлечение текста из найденных ячеек
Параметры с префиксом text_ передаются в извлечение содержимого каждой ячейки. Например, text_x_tolerance влияет на пробелы внутри значения, а text_y_tolerance — на объединение строк. Геометрия таблицы может быть правильной, но данные всё равно окажутся неудобными, если числа склеены или многострочный адрес разделён неправильно.
settings = {
"vertical_strategy": "lines",
"horizontal_strategy": "lines",
"text_x_tolerance": 2,
"text_y_tolerance": 2,
}
rows = page.extract_table(settings)
Многострочные ячейки обычно возвращают текст с переводом строки. Перед экспортом нужно решить, сохранять его, заменять пробелом или разбивать запись на отдельные сущности. Универсальная замена всех переводов строк пробелами может испортить список позиций; лучше обрабатывать колонки по назначению.
Числовые поля нормализуют после извлечения, а не через чрезмерное увеличение допусков. Пробелы-разделители тысяч, неразрывные пробелы, разные десятичные знаки и валютные символы следует приводить отдельной функцией. Тогда геометрические настройки остаются ориентированы на структуру страницы.
Несколько таблиц на странице
Когда на странице несколько сеток, extract_table() выбирает только одну. Для надёжной обработки вызывают find_tables(), сортируют объекты по top и x0, проверяют заголовки каждой области и затем извлекают данные из подходящей.
tables = sorted(
page.find_tables(),
key=lambda t: (t.bbox[1], t.bbox[0]),
)
for table in tables:
data = table.extract()
header = data[0] if data else []
if "Amount" in header:
process_amount_table(data)
Вложенные рамки формы могут определяться как отдельные таблицы или как части одной крупной сетки. Предварительная обрезка по смысловым заголовкам обычно надёжнее попытки отфильтровать результат по размеру. Если структура страницы стабильна, каждой зоне назначают собственные настройки.
При одинаковом числе ячеек find_table() предпочитает верхнюю таблицу. Поэтому автоматический выбор опасен для документа, где сверху расположена небольшая шапка с плотной сеткой, а ниже — целевая ведомость. Проверка bbox и первой строки должна быть обязательной.
Таблицы на нескольких страницах
pdfplumber извлекает таблицу в пределах отдельной страницы; объединение продолжения выполняется пользовательским кодом. Обычно у каждой страницы повторяется заголовок, а последняя строка одной части может быть перенесена на следующую. Нельзя просто склеить списки строк без проверок.
Практический алгоритм удаляет повторные заголовки, сравнивает число колонок, переносит незавершённую строку и фиксирует номер исходной страницы. Поле номера страницы помогает вернуться к месту ошибки после загрузки данных в базу.
all_rows = []
expected_header = None
with pdfplumber.open("report.pdf") as pdf:
for page in pdf.pages:
rows = page.extract_table(settings) or []
if not rows:
continue
if expected_header is None:
expected_header = rows[0]
body = rows[1:]
elif rows[0] == expected_header:
body = rows[1:]
else:
body = rows
for row in body:
all_rows.append({"page": page.page_number, "cells": row})
Если на новой странице меняется ширина колонок, один набор явных координат может перестать работать. Тогда координаты вычисляют по повторному заголовку или нормализуют относительно ширины. Для отчётов, где первая страница имеет отдельную шапку, создают настройки первой и последующих страниц.
Экспорт в CSV, JSON и DataFrame
Результат extract_table() уже имеет форму списка строк, поэтому его можно передать модулю csv. Нужно явно задать кодировку и обработку переводов строк. Для русского текста обычно используют UTF-8; вариант utf-8-sig упрощает открытие файла в некоторых версиях табличных редакторов.
import csv
rows = page.extract_table(settings) or []
with open("table.csv", "w", encoding="utf-8-sig", newline="") as file:
writer = csv.writer(file)
writer.writerows(rows)
Для анализа удобно создать pandas.DataFrame. Заголовок нельзя безусловно брать из первой строки: в PDF часто есть объединённые ячейки, двухуровневая шапка и повторяющиеся названия. Перед созданием таблицы заголовки нормализуют, объединяют уровни и делают уникальными.
import pandas as pd
header = normalize_header(rows[:2])
data = rows[2:]
df = pd.DataFrame(data, columns=header)
df = df.dropna(how="all")
JSON подходит для сохранения координат и исходной структуры. Помимо текста полезно записывать страницу, ограничивающий прямоугольник таблицы и параметры извлечения. Тогда результат можно воспроизвести и проверить после изменения алгоритма.
Не следует превращать все значения в строки навсегда. После сохранения исходного текста создают отдельные нормализованные поля: дату, число, валюту, идентификатор. Ошибку преобразования фиксируют вместе с исходным значением и координатами, а не заменяют нулём.
Командная строка pdfplumber
Консольная команда позволяет быстро получить сведения без написания скрипта. По умолчанию можно вывести объекты страницы в CSV, JSON или текст. Форматы CSV и JSON содержат данные об объектах; JSON сохраняет больше вложенных атрибутов и метаданных. Текстовый формат использует извлечение с сохранением макета.
pdfplumber document.pdf --format text > document.txt
pdfplumber document.pdf --format json > objects.json
pdfplumber document.pdf --format csv > objects.csv
--pages ограничивает обработку списком страниц и диапазонами с нумерацией от единицы. --types выбирает типы объектов, например символы, линии и прямоугольники. --precision округляет числовые координаты до заданного числа знаков, что уменьшает шум в сравнении JSON-файлов.
pdfplumber document.pdf \
--format json \
--pages 1,3-5 \
--types char,line,rect \
--precision 2 \
> selected-pages.json
--laparams принимает JSON со значениями для анализатора макета pdfminer.six. В оболочке важно правильно экранировать кавычки. Для сложных настроек и последующей обработки предпочтительнее Python-скрипт, поскольку командная строка предназначена прежде всего для инспекции и простого экспорта объектов.
Консольный CSV не равен извлечённой бизнес-таблице. Он описывает отдельные символы, линии и прямоугольники. Для получения строк и колонок требуется метод таблиц в Python или собственная обработка JSON.
Параметры анализа макета pdfminer.six
Аргумент laparams передаётся внутреннему анализатору pdfminer.six. При его использовании в page.objects могут появиться высокоуровневые объекты, например горизонтальные текстовые блоки. Они полезны для разделения колонок и абзацев, но не заменяют координаты символов.
laparams = {
"line_overlap": 0.7,
"detect_vertical": True,
}
with pdfplumber.open("document.pdf", laparams=laparams) as pdf:
page = pdf.pages[0]
print(page.objects.keys())
Параметры макета зависят от структуры конкретного PDF. Излишне агрессивное объединение может слить соседние колонки, а слабое — раздробить строку. Настройки следует хранить вместе с профилем документа и тестовыми примерами, а не применять ко всей коллекции документов.
Если задача ограничивается таблицей с чёткой сеткой, дополнительные высокоуровневые объекты часто не нужны. Они увеличивают объём анализа и усложняют диагностику. Их включают, когда действительно требуется логика текстовых блоков.
Изображения внутри PDF
Список page.images содержит позицию, размеры, цветовое пространство, глубину цвета, имя ресурса и поток объекта. Эти сведения помогают определить, что страница является сканом, найти область печати или исключить логотип из анализа. Однако библиотека не предоставляет готовый универсальный метод реконструкции исходного изображения из каждого PDF-потока.
Важно различать рендер страницы и извлечение встроенного изображения. to_image() создаёт визуальное представление всей страницы через рендерер. Это подходит для отладки и OCR. Восстановление оригинального JPEG или маски из PDFStream требует учёта фильтров, цветового пространства и масок; для такой задачи часто удобнее специализированный PDF-инструмент.
Для определения скана можно проверить сочетание признаков: на странице мало или нет chars, присутствует крупное изображение, занимающее почти всю площадь, а обычное извлечение пусто. Один признак не достаточен: векторная страница может содержать большой фон, а скан после OCR — одновременно изображение и тысячи невидимых символов.
Аннотации и гиперссылки
annots описывает PDF-аннотации, а hyperlinks — аннотации типа Link с URI-действием. Координаты позволяют связать ссылку с видимым текстом. Это полезно при аудите документов, извлечении ссылок из отчётов и проверке, что подпись действительно ведёт на ожидаемый адрес.
Ссылка может быть задана прямоугольной областью поверх текста, поэтому текст и URI извлекаются разными объектами. Для сопоставления находят слова, пересекающиеся с bbox аннотации. Следует учитывать, что часть ссылок использует внутренний переход по документу, а не внешний URI.
Аннотации могут содержать экспериментальные поля mcid и tag, связанные с размеченным содержимым. Наличие этих значений зависит от того, насколько авторская программа соблюдает структуру PDF. На них нельзя полагаться без проверки на всей серии документов.
Значения интерактивных форм
Видимый текст поля формы может не входить в обычный текстовый слой. Значение хранится в структуре AcroForm. Готового высокоуровневого метода для форм нет, но внутренние обёртки над pdfminer позволяют пройти по полям, дочерним узлам и значениям.
У поля может быть техническое имя T, альтернативное имя TU, значение V и дочерние элементы Kids. Для вложенных полей имя собирают рекурсивно. Значения нужно декодировать функцией resolve_and_decode, а косвенные PDF-объекты — разрешать через resolve.
from pdfplumber.utils.pdfinternals import resolve, resolve_and_decode
form = resolve(pdf.doc.catalog["AcroForm"])
fields = resolve(form["Fields"])
for field in fields:
data = field.resolve()
name = resolve_and_decode(data.get("T"))
value = resolve_and_decode(data.get("V")) if "V" in data else None
print(name, value)
Пример необходимо расширить рекурсией для Kids, иначе часть составных форм будет пропущена. Также следует обрабатывать отсутствие T и использовать TU как более понятную подпись, когда она доступна.
Форма и нарисованный вид могут расходиться. Пользователь мог сплющить документ, после чего значение превратилось в обычный текст или изображение. Поэтому надёжный импорт сначала проверяет AcroForm, затем текстовый слой и только после этого OCR.
Сканированные документы и OCR
pdfplumber не распознаёт текст на изображениях. Если страница является сканом без текстового слоя, сначала нужен OCR. Практический конвейер рендерит страницу в изображение с достаточным разрешением, передаёт его распознавателю, а затем либо работает с координатами OCR, либо создаёт PDF с текстовым слоем и повторно открывает его.
Для мелкого табличного текста обычно требуется более высокое разрешение, чем для обычного абзаца. Однако чрезмерное увеличение повышает расход памяти и время распознавания. Перед OCR полезно обрезать поля, выровнять страницу, повысить контраст и удалить фон, если эти операции не искажают линии таблицы.
Даже после OCR таблицы могут извлекаться хуже, чем из исходного цифрового PDF. Распознанные слова получают неточные координаты, линии могут остаться частью изображения, а текстовый слой — не совпадать с видимыми символами. В таких случаях таблицу строят по координатам OCR или явно задают границы, а pdfplumber используют для рендера, обрезки и проверки.
Проверить наличие текста можно до запуска дорогого OCR. Если len(page.chars) велико и extract_text() возвращает содержимое, распознавание, вероятно, не требуется. Если символы есть, но они бессмысленны, проблема может быть в кодировке шрифта; OCR тогда становится запасным способом.
Для смешанных PDF решение принимают по страницам. Титульная страница может быть сканом, а приложения — цифровыми таблицами. Обработка всего файла единым режимом либо тратит ресурсы на ненужный OCR, либо пропускает растровые страницы.
Производительность и память
Каждая страница кэширует результаты анализа, чтобы повторные обращения к chars, lines и макету не запускали парсер заново. На больших документах кэш может занимать значительную память. Метод Page.close() очищает кэш страницы; PDF.close() закрывает страницы и поток.
with pdfplumber.open("large-report.pdf") as pdf:
for page in pdf.pages:
process(page)
page.close()
Не стоит заранее превращать все страницы в изображения, если визуализация нужна только для ошибок. Сначала выполняют извлечение и проверки, а PNG сохраняют лишь для страниц, не прошедших контроль. Это резко уменьшает объём временных файлов и нагрузку на рендерер.
Обрезка уменьшает число объектов, участвующих в поиске таблицы, и ускоряет вычисление пересечений. Особенно заметен эффект на чертежах и формах с тысячами линий. Перед точной обрезкой можно быстро найти текстовый маркер и построить область относительно него.
При параллельной обработке безопаснее открывать PDF отдельно в каждом рабочем процессе, а не передавать один объект между потоками. Размер пула выбирают по памяти и сложности страниц. Рендер изображений и анализ большого числа объектов могут быть тяжелее чтения файла с диска.
Профилирование должно разделять этапы: открытие, анализ объектов, таблицу, OCR, нормализацию и запись. Без этого легко пытаться ускорить extract_text(), когда основное время уходит на рендер или внешнее распознавание.
Обработка ошибок и повреждённых PDF
Файл может иметь корректное расширение, но содержать HTML, страницу ошибки или обрезанную загрузку. До открытия проверяют сигнатуру %PDF-, размер и, по возможности, завершающий маркер. Сигнатура не гарантирует целостность, но быстро отбрасывает очевидно неверные входы.
Исключения следует фиксировать вместе с именем файла, номером страницы и этапом. Сообщение не удалось обработать PDF бесполезно для диагностики; запись ошибка разбора страницы 18 при чтении объектов позволяет воспроизвести проблему.
from pathlib import Path
import pdfplumber
path = Path("document.pdf")
try:
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
try:
process_page(page)
except Exception as page_error:
log_page_error(path, page.page_number, page_error)
except Exception as file_error:
log_file_error(path, file_error)
Широкий except Exception допустим на внешней границе пакетного конвейера, чтобы один файл не остановил очередь, но внутри функций лучше ловить конкретные исключения и не скрывать программные ошибки. Неожиданные исключения должны попадать в отдельный журнал и тестовый набор.
При ошибке рендера to_image() текстовое извлечение может продолжать работать, поскольку это разные этапы. И наоборот, страница может красиво рендериться, но не иметь читаемого текстового слоя. Режимы восстановления следует выбирать по месту сбоя, а не заменять весь процесс.
Проверка качества извлечения
Правильная сетка на изображении ещё не гарантирует правильные данные. Автоматическая проверка должна учитывать смысл: ожидаемое число колонок, наличие обязательных заголовков, допустимые форматы дат и сумм, уникальность ключей, равенство итогов сумме строк.
Для таблицы полезно хранить метрики: число найденных таблиц, рёбер, ячеек, строк, долю пустых значений и ширину каждой колонки. Резкое изменение по сравнению с типичным документом сигнализирует об изменении шаблона или повреждении файла.
def validate_rows(rows):
if not rows:
return ["Таблица не найдена"]
errors = []
widths = {len(row) for row in rows}
if len(widths) != 1:
errors.append("Разное число ячеек в строках")
if max(widths) < 4:
errors.append("Слишком мало колонок")
return errors
Эталонные файлы должны включать не только простой пример, но и крайние случаи: длинные названия, пустые ячейки, отрицательные числа, переносы строк, последнюю страницу, документ с изменённым масштабом. После изменения допусков тесты сравнивают нормализованные данные и отладочные метрики.
Полезно сохранять небольшую выборку визуальных кадров для ручной проверки. Кадр должен показывать страницу и нанесённые границы, а его имя — содержать идентификатор документа и номер страницы. Это быстрее, чем открывать исходный PDF и воспроизводить параметры вручную.
Практический сценарий: счёт или накладная
В счёте обычно нужны номер, дата, поставщик, позиции и итог. Сначала методом search() находят подписи номера и даты и извлекают значения из областей справа. Затем по заголовкам Описание, Количество и Сумма определяют верх таблицы. Нижнюю границу привязывают к Итого или к нижнему краю страницы.
Для таблицы с сеткой используют lines, а для печатной формы без рамок — text или явные вертикальные координаты. После извлечения строка с итогом отделяется от позиций. Числа нормализуются с учётом валюты и разделителя десятичной части.
На многостраничном счёте повторная шапка удаляется, а описание, перенесённое на следующую строку без количества и цены, присоединяется к предыдущей позиции. Такое правило должно проверять пустые числовые колонки, а не только отступ текста.
Контроль включает сравнение вычисленной суммы позиций с напечатанным итогом. Небольшая разница может быть связана с округлением налогов, поэтому допустимый порог задаётся бизнес-правилом. Несовпадение сохраняется как ошибка извлечения или документальная аномалия.
Практический сценарий: государственный отчёт
В статистических отчётах часто встречаются многоуровневые заголовки, объединённые ячейки и сноски. Сначала визуально определяют, какие линии образуют настоящие колонки. Затем извлекают несколько строк шапки и строят составные имена: верхний заголовок распространяют на пустые ячейки справа, после чего соединяют с нижним уровнем.
Сноски и примечания обычно находятся ниже таблицы и могут иметь вертикальные линии, продолжающие сетку. Обрезка по последней строке данных предотвращает появление лишних ячеек. Конец данных можно найти по слову Примечание, уменьшенному шрифту или отсутствию чисел в ожидаемых колонках.
Значения —, н/д и пустая ячейка имеют разный смысл. Их нельзя безусловно превращать в ноль. В выходной схеме полезно хранить исходный текст, нормализованное значение и признак причины отсутствия.
Если отчёт публикуется ежемесячно, тесты сравнивают набор колонок с предыдущим выпуском. Появление новой колонки должно вызвать предупреждение, а не молчаливое смещение значений.
Практический сценарий: поиск фрагментов по координатам
Иногда таблица не нужна: требуется найти все номера дел, суммы или адреса и сохранить положение. search() подходит для регулярных шаблонов, а extract_words() — для пространственных правил. Координаты позволяют выделить найденное на странице и передать человеку для проверки.
amounts = page.search(
r"\b\d{1,3}(?:[ .]\d{3})*[,.]\d{2}\b",
regex=True,
)
image = page.to_image(resolution=140)
image.draw_rects(amounts, stroke="red", stroke_width=2)
image.save("amounts.png")
Регулярное выражение следует адаптировать к локали и контексту. Без привязки к валюте или колонке оно найдёт даты и номера. Пространственный фильтр, например правее заголовка Amount и ниже строки Total, уменьшает число ложных совпадений.
Для аудита сохраняют текст совпадения, страницу, координаты и несколько слов вокруг. Такой набор позволяет повторно проверить результат даже после преобразования данных.
Когда нужны явные правила вместо универсальных настроек
Универсальный набор допусков редко одинаково хорошо работает на банковской выписке, научной статье и инженерной форме. Если документы приходят из ограниченного числа систем, надёжнее определить шаблон и применить специализированный профиль.
Шаблон можно распознавать по размеру страницы, производителю PDF, заголовку, координатам логотипа и набору колонок. После выбора профиля задаются обрезка, стратегии таблиц, допуски и нормализация. Неизвестный шаблон отправляется на ручную проверку вместо попытки угадать данные.
Явные координаты не являются недостатком, если система-генератор стабильна. Они дают воспроизводимость и простую диагностику. Риск появляется, когда система-генератор меняет макет без уведомления; поэтому профиль должен иметь проверки заголовков и границ, а не слепо извлекать прямоугольник.
Ограничения, которые важно учитывать
pdfplumber не создаёт и не изменяет PDF. Он не предназначен для перестановки страниц, добавления подписей, замены текста или сохранения отредактированного документа. Для таких задач нужен редактор или библиотека генерации PDF.
Встроенного OCR нет. Растровые страницы требуют внешнего распознавания. Даже PDF с OCR-слоем может давать слабые таблицы из-за неточных координат. Визуальная отладка показывает расхождение, но не исправляет распознавание автоматически.
Графического мастера настройки таблиц нет. Пользователь работает с кодом, координатами и параметрами. Это повышает порог входа, особенно если нужно один раз извлечь небольшую таблицу. Для повторяемых задач отсутствие ручного интерфейса превращается в преимущество: процесс полностью автоматизируется и тестируется.
Сложный визуальный макет не всегда соответствует логической структуре. Объединённые ячейки, наклонные линии, вложенные таблицы и произвольные формы могут потребовать собственного алгоритма. Результат следует считать гипотезой, пока он не прошёл проверки данных.
Сравнение pdfplumber с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| pdfplumber | Точного доступа к символам, координатам, линиям и настраиваемому извлечению таблиц | Нет OCR и редактирования PDF |
| Camelot | Таблиц в цифровых PDF с выраженной структурой и экспортом в DataFrame | Не работает как универсальный анализатор всех объектов страницы |
| Tabula | Ручного выбора областей и быстрого извлечения таблиц через графическое окно | Требует Java и ориентирована прежде всего на таблицы |
| PyMuPDF | Быстрого рендера, чтения, изменения и генерации PDF | Другая модель таблиц и визуальной отладки |
| pypdf | Объединения, разделения, поворота, шифрования и базового извлечения текста | Нет встроенного анализа табличной сетки по объектам |
| pdfminer.six | Низкоуровневого анализа текста и макета с гибкими параметрами | Нет интегрированного поиска таблиц и PageImage |
pdfplumber выбирают, когда важны координаты, линии, слова и объяснимая настройка таблиц. Camelot удобен для задачи, почти полностью состоящей из цифровых таблиц. Tabula подходит человеку, которому проще выделить область мышью. PyMuPDF предпочтителен для скорости, рендера и изменения документов, pypdf — для операций со страницами и структурой файла, а pdfminer.six — для низкоуровневого текстового анализа без дополнительного слоя инструментов pdfplumber.
PDF Commander решает другую практическую задачу: визуальное редактирование PDF пользователем. Он полезен, когда нужно изменить текст, страницы или оформление, но не заменяет программное извлечение координат и повторяемый Python-конвейер.
Как выбрать между pdfplumber и табличными инструментами
Если требуется один раз выгрузить таблицу и границы легко выделяются мышью, графический инструмент с предпросмотром будет быстрее. Если документы приходят регулярно и результат должен воспроизводиться без ручных действий, кодовая настройка pdfplumber даёт больше контроля.
Camelot и Tabula сосредоточены на таблицах. pdfplumber полезнее, когда таблица связана с окружающим текстом: сначала нужно найти заголовок, определить тип документа, извлечь реквизиты, а затем обработать сетку и сохранить координаты.
Для сканов ни один геометрический инструмент не заменяет OCR. Выбор делается после распознавания: если OCR возвращает координаты слов, таблицу можно строить напрямую по ним; если создаётся PDF с текстовым слоем, его можно передать pdfplumber и проверить визуально.
Типичные ошибки и способы устранения
extract_text() возвращает пустой результат
Проверьте len(page.chars) и page.images. При отсутствии символов страница, вероятно, является изображением и требует OCR. Если символы есть, выведите первые элементы chars: бессмысленные коды указывают на проблему шрифта или карты Unicode.
Слова слипаются между колонками
Уменьшите x_tolerance или используйте x_tolerance_ratio. Затем перейдите к extract_words() и разделите колонки координатами. Увеличение количества пробелов в готовой строке не восстановит границы надёжно.
Одно слово распадается на буквы
Увеличьте горизонтальный допуск и проверьте, одинаковы ли размер и шрифт символов. Если слово состоит из разных атрибутов, extra_attrs может намеренно разделять его.
Текст извлекается дважды
Примените dedupe_chars() с небольшим допуском и сравните число символов. Если повторные строки находятся в разных координатах, удаляйте область колонтитула или фильтруйте объекты по позиции.
Таблица не найдена
Создайте PageImage и вызовите debug_tablefinder(). Если видимых линий нет в наборе рёбер, попробуйте text или explicit. Если на странице много посторонних элементов, сначала обрежьте область.
Пропадает крайняя колонка
Проверьте, замыкаются ли горизонтальные рёбра с последней вертикальной линией. Уточните правую явную границу или увеличьте допуск пересечения только по горизонтали. Не расширяйте все допуски одновременно.
Появляются лишние ячейки
Переключитесь с lines на lines_strict, если помехи создают стороны цветных прямоугольников. Увеличьте минимальную длину рёбер или ограничьте область таблицы.
Пунктирная сетка распадается
Уменьшите edge_min_length_prefilter и увеличьте join_tolerance вдоль линии. Контролируйте, чтобы штрихи соседних строк не объединились.
Многострочные ячейки теряют пробелы
Настройте text_x_tolerance и text_y_tolerance отдельно от геометрии сетки. Сохраняйте переводы строк до этапа нормализации, чтобы не потерять границы пунктов.
Память растёт на каждой странице
Обрабатывайте страницы последовательно, вызывайте page.close() после сохранения результата и не храните все PageImage в списке. Для ошибок сохраняйте только нужные PNG.
Обрезка даёт пустую область
Проверьте порядок координат (x0, top, x1, bottom) и не смешивайте top с y0. Выведите page.width и page.height, затем нарисуйте прямоугольник обрезки на исходной странице.
Порядок текста в двух колонках неверен
Извлекайте левую и правую колонки отдельными обрезками либо группируйте слова по диапазонам x. layout=True может улучшить вид, но не гарантирует логический порядок статьи.
Пакетная обработка папки с документами
Для каталога PDF удобно отделить обход файлов от функции извлечения одного документа. Функция должна возвращать структурированный результат и список ошибок, а управляющий код — решать, куда записать данные и как поступить с неуспешным файлом. Такое разделение упрощает повторный запуск и тестирование.
from pathlib import Path
import pdfplumber
def extract_document(path: Path):
result = {"file": path.name, "pages": []}
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
result["pages"].append({
"number": page.page_number,
"text": page.extract_text() or "",
"tables": page.extract_tables(settings),
})
page.close()
return result
for path in Path("incoming").glob("*.pdf"):
try:
save_result(extract_document(path))
except Exception as error:
move_to_error_queue(path, error)
Порядок файлов лучше фиксировать сортировкой, если результат зависит от последовательности. Перед обработкой проверяют, что выходной файл ещё не существует или соответствует тому же хэшу входа. При повторном запуске завершённые документы можно пропускать, а ошибки — обрабатывать отдельно после изменения профиля.
Один большой JSON на весь каталог неудобен: повреждение записи или остановка процесса затрагивает весь результат. Надёжнее сохранять данные по документам либо использовать транзакционную базу. После успешной записи исходник перемещают в каталог завершённых файлов только после проверки целостности результата.
Безопасная обработка недоверенных PDF
PDF, полученный извне, следует считать недоверенным вводом. Обработку выполняют с ограниченными правами, без доступа к секретам и критическим каталогам. Для больших очередей полезны ограничения по размеру файла, числу страниц, времени работы и объёму создаваемых изображений, иначе повреждённый или специально сложный документ может занять всю память.
Имена файлов не используют напрямую для команд оболочки. Пути передают библиотекам как объекты Path, а временные файлы создают в отдельном каталоге со случайными именами. Пароль, URI аннотации и метаданные не выводят в журнал без очистки: они могут содержать персональные данные или управляющие символы.
Если в конвейере участвуют OCR, конвертер или внешний рендерер, их запускают с теми же ограничениями и обновляют независимо. Ошибка стороннего этапа не должна подменять исходный файл или записывать результат в произвольный путь. После завершения временные изображения удаляют, если они не нужны для аудита.
Связь извлечённых значений с исходной страницей
Для проверяемого импорта недостаточно сохранить только значение. Полезная запись содержит номер страницы, координаты, способ извлечения и текстовый контекст. Когда оператор видит сомнительную сумму, система может сразу показать фрагмент страницы, а не заставлять искать его вручную.
record = {
"field": "total_amount",
"value_raw": match["text"],
"value_normalized": parse_money(match["text"]),
"page": page.page_number,
"bbox": [match["x0"], match["top"], match["x1"], match["bottom"]],
"method": "regex_search",
}
Для таблицы координаты можно хранить на уровне всей таблицы, строки или отдельной ячейки. Чем критичнее данные, тем подробнее происхождение. При массовой статистике достаточно прямоугольника таблицы и страницы; для финансового или юридического документа лучше сохранять границы каждого поля.
После изменения алгоритма происхождение помогает сравнить старый и новый результат. Если нормализованное значение изменилось, координаты показывают, извлечён ли другой фрагмент или изменилось только правило преобразования текста.
Регрессионные тесты для настроек таблиц
Тест должен проверять не только отсутствие исключения. Для каждого эталонного PDF сохраняют ожидаемый заголовок, число строк, несколько ключевых ячеек и контрольную сумму нормализованного результата. Отдельно можно проверять число рёбер и ячеек, чтобы заметить изменение геометрии до того, как оно повлияет на данные.
def test_monthly_report():
with pdfplumber.open("tests/monthly-report.pdf") as pdf:
page = pdf.pages[0]
finder = page.debug_tablefinder(settings)
rows = page.extract_table(settings)
assert len(finder.tables) == 1
assert len(rows) == 54
assert rows[0][:3] == ["Region", "Count", "Amount"]
assert rows[-1][0] == "Total"
Изображения отладки не обязательно сравнивать пиксель в пиксель: обновление рендерера может изменить сглаживание. Более устойчивы геометрические метрики и выборочная ручная проверка. Для критичных шаблонов можно сохранять координаты основных колонок с небольшим допустимым отклонением.
Проблемный документ после исправления добавляют в тестовый набор вместе с объяснением причины: разорванная линия, дублированный текст, изменённая шапка или пустая колонка. Так набор постепенно отражает реальные риски проекта, а не только идеальные примеры.
Рекомендации для устойчивого проекта
- Храните параметры извлечения рядом с идентификатором шаблона документа.
- Сохраняйте исходный текст и нормализованное значение отдельно.
- Записывайте номер страницы и координаты каждого важного поля.
- Проверяйте число колонок, заголовки и контрольные суммы.
- Создавайте визуальный кадр только для ошибок и выборочного контроля.
- Добавляйте новый проблемный PDF в регрессионный набор.
- Не заменяйте ошибку пустой строкой или нулём без отдельного статуса.
Конфигурация должна быть версионируемой вместе с кодом. Изменение одного допуска способно исправить конкретный документ и одновременно сломать десятки других. Регрессионные тесты и сохранённые метрики показывают побочный эффект до запуска на всей коллекции документов.
В журнале обработки полезно указывать хэш файла. Документы с одинаковым именем могут иметь разное содержимое, а повторная загрузка одного файла — создавать дубликаты. Хэш связывает исходник, параметры и результат однозначно.
Итоговый подход к работе
Надёжное извлечение начинается с просмотра объектов страницы, а не с бесконечного подбора одной функции. Сначала определяется, есть ли текстовый слой и реальные линии. Затем выбирается ограниченная область, проверяется группировка слов, после чего настраивается таблица и только потом выполняется нормализация данных.
Визуальная отладка делает процесс объяснимым: на изображении видно, какие рёбра найдены, где образовались пересечения и почему конкретная ячейка отсутствует. Координаты позволяют связать каждое значение с местом в исходном документе и построить проверяемый конвейер.
pdfplumber особенно эффективен для повторяющихся цифровых PDF, где требуется не просто получить строку текста, а сохранить структуру, геометрию и возможность расследовать ошибку. Для редактирования, OCR и ручного выбора областей нужны дополнительные инструменты, однако в программной обработке отчётов, реестров и форм сочетание объектов страницы, обрезки, таблиц и PageImage даёт точный контроль над результатом.