PyPDF2

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

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

Практическая работа обычно начинается с проверки входного файла, подсчёта страниц и чтения метаданных, после чего скрипт либо извлекает данные, либо создаёт новый PDF. Важно заранее различать операции над структурой страницы и распознавание изображения: библиотека читает текстовые объекты, уже записанные в PDF, но не превращает фотографию страницы в текст. Для сканов сначала нужен OCR, а затем PyPDF2 можно использовать для сборки, разбиения, защиты и дальнейшего анализа полученного документа.

Скачать PyPDF2

Оценка 9.7 Рекомендуем
  • Редактирование PDF
  • Русский интерфейс
  • Просто новичкам
Скачать бесплатно на Windows
Лучшая альтернатива
PyPDF2
Оценка 8.5
  • Нет графического интерфейса
  • Нет встроенного OCR
  • Не ставит цифровую подпись
Скачать PyPDF2
Загрузка начнётся после нажатия

Как устроен рабочий процесс PyPDF2

У библиотеки нет панелей, меню и диалогов, поэтому роль интерфейса выполняют классы, методы и свойства. PdfReader отвечает за разбор исходного файла, доступ к коллекции pages, метаданным, закладкам и полям формы. PdfWriter хранит будущий документ, принимает страницы, вложения, аннотации и параметры шифрования, а затем сериализует структуру в поток. Объект PageObject находится между ними: именно на странице выполняются поворот, изменение прямоугольников, трансформация координат, объединение содержимого и извлечение текста.

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

from pathlib import Path
from PyPDF2 import PdfReader, PdfWriter

source = Path("input.pdf")
target = Path("output.pdf")

reader = PdfReader(source)
writer = PdfWriter()
for page in reader.pages:
    writer.add_page(page)

with target.open("wb") as stream:
    writer.write(stream)

check = PdfReader(target)
assert len(check.pages) == len(reader.pages)

Минимальный пример кажется простым копированием, однако он показывает важное правило: результат записывается в новый файл. Не следует одновременно читать и перезаписывать один и тот же путь, потому что открытие выходного потока на запись обнулит исходные данные раньше, чем Reader закончит обращаться к ним. Для безопасной замены создают временный файл в том же каталоге, проверяют его, а затем выполняют атомарное атомарную замену.

Установка и подготовка окружения

Надёжнее устанавливать зависимость через тот интерпретатор, которым затем запускается скрипт: команда с конструкцией python -m pip исключает частую ситуацию, когда pip относится к другому окружению. После установки полезно выполнить короткую проверку импорта и вывести путь к модулю. Если проект использует виртуальное окружение, его создают до установки и фиксируют зависимость в requirements-файле или конфигурации сборки, чтобы сервер и рабочий компьютер получили одинаковый API.

python -m venv .venv
# Windows: .venv\Scripts\activate
# Linux и macOS: source .venv/bin/activate
python -m pip install PyPDF2
python -c "import PyPDF2; print(PyPDF2.__file__)"

Для некоторых задач нужны дополнительные библиотеки. Работа с современными алгоритмами шифрования требует криптографической зависимости, а извлечение изображений — Pillow. Их удобнее ставить через extras, чтобы состав окружения был явно связан с назначением проекта. Если скрипт только объединяет и делит обычные PDF, базовой установки достаточно; не стоит добавлять неиспользуемые пакеты в контейнер или серверную функцию.

python -m pip install "PyPDF2[crypto]"
python -m pip install "PyPDF2[image]"
# Набор дополнительных зависимостей для нескольких расширенных задач:
python -m pip install "PyPDF2[full]"

Ошибка ModuleNotFoundError чаще означает не отсутствие файла в интернете, а несовпадение интерпретаторов. Сравните вывод python -c с sys.executable внутри программы и python -m pip --version в терминале. В IDE проверьте выбранный interpreter, а в планировщике задач укажите абсолютный путь к python из виртуального окружения. Имя импорта чувствительно к регистру: используется PyPDF2, а не pypdf2.

Чтение документа через PdfReader

PdfReader принимает путь, открытый бинарный поток или объект BytesIO. После разбора коллекция reader.pages поддерживает индексацию и срезы, но индексы начинаются с нуля: первая страница имеет номер 0. Количество страниц определяют через len, не полагаясь на номер, напечатанный в колонтитуле. Документ может содержать пустые, служебные или вложенные страницы, поэтому в задачах архивации физическая позиция страницы надёжнее визуальной нумерации.

from PyPDF2 import PdfReader

reader = PdfReader("report.pdf")
print("Страниц:", len(reader.pages))
first = reader.pages[0]
last = reader.pages[-1]
print("Первая страница:", float(first.mediabox.width), "x", float(first.mediabox.height))

При работе с файловым объектом держите поток открытым до завершения всех обращений к страницам. Reader может читать отдельные части структуры лениво, поэтому возврат объекта из функции после закрытия with способен дать ошибку уже при последующем extract_text или чтении ресурса изображения. Простое решение — выполнить все операции внутри контекстного менеджера либо загрузить байты в BytesIO, если документ должен пережить закрытие исходного файла.

Параметр strict управляет отношением к отклонениям от спецификации. Мягкий режим пытается разобрать распространённые дефекты и сообщает о них через журнал, строгий — удобен для контроля качества, потому что быстрее превращает сомнительную конструкцию в исключение. Для массового импорта обычно сначала применяют мягкое чтение, отдельно регистрируют предупреждения, а документы с проблемами отправляют на проверку. Для тестов генератора PDF полезен строгий режим: он не позволяет незаметно закрепить ошибку.

Проверка шифрования до доступа к страницам

Свойство is_encrypted проверяют сразу после создания Reader. У зашифрованного документа доступ к страницам может быть запрещён, пока не вызван decrypt. Возвращаемое значение decrypt следует проверять, а пароль — получать из безопасного хранилища, а не записывать в код или журнал. Наличие разрешения на печать или копирование в просмотрщике не заменяет пароль владельца и не гарантирует, что библиотека сможет выполнить нужную операцию.

from PyPDF2 import PdfReader

reader = PdfReader("protected.pdf")
if reader.is_encrypted:
    result = reader.decrypt(password)
    if result == 0:
        raise ValueError("Пароль не подошёл")
print(len(reader.pages))

Извлечение текста

Метод page.extract_text() возвращает строку из текстовых операторов страницы. Он не читает буквы с пикселей и не пытается угадывать смысл графики. Если в PDF хранится текстовый слой, результат часто подходит для поиска, индексации и предварительного анализа. Если страница состоит из одного изображения, результат будет пустым или почти пустым — это сигнал передать страницу в OCR, а не многократно менять параметры extract_text.

from PyPDF2 import PdfReader

reader = PdfReader("contract.pdf")
chunks = []
for number, page in enumerate(reader.pages, start=1):
    text = page.extract_text() or ""
    chunks.append(f"\n--- Страница {number} ---\n{text}")

with open("contract.txt", "w", encoding="utf-8") as out:
    out.write("".join(chunks))

Результат извлечения текста из PDF через PyPDF2

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

Ориентационный фильтр помогает исключить повёрнутые надписи. В extract_text можно передать допустимые углы, например оставить обычный текст и текст, повёрнутый на 90 градусов. Это полезно для документов с боковыми штампами и вертикальными служебными полями. Фильтр не исправляет неверную матрицу страницы и не вращает её визуально: он только определяет, какие текстовые фрагменты включать в результат.

page = reader.pages[0]
normal_text = page.extract_text(0)
normal_and_vertical = page.extract_text((0, 90))

Посетители для отбора фрагментов по координатам

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

parts = []

def visitor(text, cm, tm, font_dict, font_size):
    y = tm[5]
    if 50 < y < 720:
        parts.append(text)

reader.pages[0].extract_text(visitor_text=visitor)
body = "".join(parts)

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

Метаданные и свойства страниц

reader.metadata предоставляет словарь документа: заголовок, автора, тему, создателя и программу-производителя, если эти поля действительно записаны в PDF. Метаданные не являются надёжным доказательством происхождения, потому что их легко изменить, а часть генераторов оставляет поля пустыми. Их удобно использовать для предварительного имени файла, сортировки или отчёта, но критические решения следует подтверждать содержимым и внешними данными.

meta = reader.metadata
print(meta.title)
print(meta.author)
print(meta.subject)
print(meta.creator)
print(meta.producer)

При записи собственных полей PdfWriter принимает ключи со слешем, например /Title и /Author. Значения должны быть строками, поэтому даты и идентификаторы предварительно форматируют. Если нужно сохранить исходные поля и заменить только одно, сначала создайте обычный словарь из metadata, затем обновите нужный ключ и передайте его add_metadata. Не копируйте служебные объекты Reader без преобразования: они могут принадлежать исходному графу косвенных ссылок.

writer = PdfWriter()
writer.append("input.pdf")
metadata = {k: str(v) for k, v in (reader.metadata or {}).items() if v is not None}
metadata["/Title"] = "Сводный отчёт"
metadata["/Author"] = "Отдел аналитики"
writer.add_metadata(metadata)

Размер страницы читается из прямоугольников mediabox и cropbox. MediaBox описывает физическую область страницы, CropBox — область, которую обычно показывает просмотрщик. TrimBox, BleedBox и ArtBox могут использоваться полиграфическими файлами. Значения хранятся в пунктах: 72 пункта соответствуют одному дюйму. Для перевода в миллиметры умножайте на 25,4 и делите на 72, а для сравнения форматов допускайте небольшую погрешность округления.

Извлечение встроенных изображений

Коллекция page.images перечисляет изображения, доступные на странице как отдельные графические объекты. У каждого элемента есть имя и байтовые данные, которые можно записать в файл. Для декодирования ряда фильтров требуется Pillow. Этот метод полезен для выгрузки фотографий и иллюстраций, но не гарантирует, что каждая видимая картинка станет одним файлом: изображение может быть разбито на маску и цветовой слой, встроено в форму или нарисовано векторными командами.

from pathlib import Path
from PyPDF2 import PdfReader

out_dir = Path("extracted_images")
out_dir.mkdir(exist_ok=True)
reader = PdfReader("catalog.pdf")

for page_no, page in enumerate(reader.pages, start=1):
    for image_no, image in enumerate(page.images, start=1):
        safe_name = Path(image.name).name
        target = out_dir / f"p{page_no:03d}_{image_no:02d}_{safe_name}"
        target.write_bytes(image.data)

Перед массовой выгрузкой ограничьте допустимый размер и число объектов. Один документ способен содержать множество мелких масок, повторяющиеся ресурсы и изображения с очень большим разрешением. Для устранения дублей вычисляйте хеш байтов, а имя из PDF пропускайте через Path.name, чтобы не позволить вложенному имени записать файл за пределами целевого каталога. После сохранения проверяйте сигнатуру и открытие изображения, а не доверяйте одному расширению.

Если цель — получить растровое изображение всей страницы, page.images не подходит: библиотека не является рендерером. Для такой задачи страницу нужно отрисовать отдельным движком с заданным DPI. PyPDF2 можно использовать до и после рендеринга: выбрать диапазон, нормализовать поворот, снять пароль при наличии разрешения, а затем собрать производный PDF. Такое разделение не смешивает извлечение существующих ресурсов с визуализацией страницы.

Разделение PDF по страницам и диапазонам

Разделение строится на создании нового PdfWriter для каждого результата. Для одиночных страниц цикл добавляет ровно один PageObject, формирует безопасное имя и закрывает выходной поток до перехода к следующей странице. Если исходный документ большой, не храните сразу сотни Writer в списке: создавайте и записывайте их последовательно. Это уменьшает память и позволяет возобновлять обработку после сбоя с последнего подтверждённого файла.

from pathlib import Path
from PyPDF2 import PdfReader, PdfWriter

reader = PdfReader("bundle.pdf")
out_dir = Path("pages")
out_dir.mkdir(exist_ok=True)

for index, page in enumerate(reader.pages, start=1):
    writer = PdfWriter()
    writer.add_page(page)
    target = out_dir / f"page-{index:04d}.pdf"
    with target.open("wb") as stream:
        writer.write(stream)

Для диапазона используйте срез pages[start:stop], помня, что stop не включается. Пользовательский диапазон 5–12 превращается в индексы 4:12. Перед выполнением проверяйте, что начало не меньше единицы, конец не превышает число страниц и начало не больше конца. Явная проверка лучше молчаливого пустого результата: оператор сразу увидит неверно введённый диапазон и не отправит пустой файл дальше по процессу.

Деление на пачки удобно для ограничений почтовой системы или архива. Размер пачки задают в страницах, а не в мегабайтах: итоговый объём зависит от изображений и шрифтов. Если требуется предел по байтам, создайте кандидат в BytesIO, измерьте его, затем уменьшайте диапазон или применяйте стратегию двоичного поиска. Не оценивайте размер пропорционально числу страниц: одна страница со сканом может быть тяжелее сотни текстовых.

Объединение документов и вставка страниц

PdfWriter.append добавляет документ или выбранный диапазон в конец, а merge вставляет его в заданную позицию. Методы работают с путём или потоком и позволяют управлять импортом структуры навигации. Для простого набора файлов предварительно определите стабильный порядок сортировки: обычная строковая сортировка ставит file10 перед file2, поэтому номера нормализуют ведущими нулями или извлекают числовую часть отдельным ключом.

from pathlib import Path
from PyPDF2 import PdfWriter

writer = PdfWriter()
for source in sorted(Path("chapters").glob("*.pdf")):
    writer.append(source, import_outline=True)

with open("book.pdf", "wb") as stream:
    writer.write(stream)

Параметр pages позволяет взять не весь источник, а диапазон или список страниц. Это удобнее, чем сначала создавать временный PDF. Для управляемого оглавления можно отключить перенос исходных закладок и добавить собственный элемент outline_item с понятным названием главы. Не считайте, что закладка совпадает с печатным номером: она указывает на объект страницы, а после вставок её позицию нужно проверять в итоговом файле.

writer = PdfWriter()
writer.append("front.pdf", pages=(0, 2), import_outline=False)
writer.append("main.pdf", pages=(3, 20), outline_item="Основная часть")
writer.merge(position=2, fileobj="insert.pdf", pages=(0, 1), import_outline=False)

Результат простого объединения содержимого страниц в PyPDF2

При добавлении одной и той же страницы несколько раз библиотека может повторно использовать переведённые объекты исходного Reader. Если после первого добавления объект страницы изменяется, неожиданное изменение способно отразиться и на копии. Для сценариев, где нужны независимые варианты одного шаблона, создавайте отдельные источники либо сбрасывайте таблицу перевода Writer перед повторным клонированием. Результат обязательно открывайте заново и сравнивайте страницы, а не только число элементов.

Объединение не приводит все страницы к одному формату. A4, Letter и альбомные листы сохранят собственные размеры, если их явно не масштабировать. Это обычно правильно для архива, но может мешать печати или дальнейшему распознаванию. Перед сборкой вычислите ширину и высоту каждой страницы, сформируйте отчёт об отклонениях и применяйте преобразование только к тем элементам, для которых единый холст действительно обязателен.

Поворот, ориентация и прямоугольники страницы

Метод rotate изменяет свойство поворота страницы с шагом, кратным 90 градусам. Такой поворот поддерживают просмотрщики и он не обязан переписывать каждую команду содержимого. Если затем планируется объединять страницу с другой, добавлять штамп или вычислять координаты, полезно перенести поворот в содержимое через transfer_rotation_to_content. Это нормализует систему координат и снижает риск, что наложенный объект окажется сбоку или за пределами видимой области.

page = reader.pages[0]
page.rotate(90)
page.transfer_rotation_to_content()
writer.add_page(page)

Обрезка через cropbox скрывает область в просмотрщике, но не обязательно удаляет команды и данные за границей. Поэтому CropBox нельзя считать средством безвозвратного удаления конфиденциального текста. Для настоящей редактуры нужно удалить или заменить соответствующие объекты и проверить, что текст не извлекается и не возвращается при смене области просмотра. PyPDF2 подходит для геометрической обрезки листа, но не заменяет специализированную процедуру редактирования.

Координаты прямоугольника задаются нижним левым и верхним правым углами. Перед изменением сохраните исходные значения и убедитесь, что новая ширина и высота положительны. Не подставляйте координаты из экранных пикселей напрямую: в PDF начало обычно находится внизу слева, а в интерфейсах изображений — сверху слева. Для перевода требуется учитывать масштаб, поворот и высоту страницы.

Масштабирование и матрицы преобразования

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

Сравнение масштабирования содержимого и страницы в PyPDF2

Класс Transformation строит аффинную матрицу из масштабирования, переноса и поворота. Порядок операций важен: перенос после масштабирования не эквивалентен переносу до него. Чтобы избежать догадок, вычислите ожидаемые координаты углов и сделайте тестовый файл с рамкой. При объединении страниц следите за параметром expand: без расширения холста часть повёрнутого или перенесённого содержимого может быть корректно записана, но невидима за границей страницы.

from PyPDF2 import Transformation

page = reader.pages[0]
operation = Transformation().scale(0.75).translate(tx=72, ty=72)
page.add_transformation(operation)
page.mediabox.upper_right = (612, 792)
writer.add_page(page)

Объединение страницы с поворотом и расширением границ

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

Штампы, водяные знаки и фоновый слой

Штамп удобно хранить как одностраничный PDF с прозрачным фоном. Его страницу читают один раз, при необходимости масштабируют под размер целевой страницы и объединяют с каждой страницей документа. Параметр over определяет порядок слоёв: штамп поверх содержимого остаётся заметным, а фоновый водяной знак помещается под ним и может закрываться непрозрачными элементами. Для разного формата листов матрицу положения вычисляют отдельно для каждой страницы.

from PyPDF2 import PdfReader, PdfWriter

stamp = PdfReader("stamp.pdf").pages[0]
reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    page.transfer_rotation_to_content()
    page.merge_page(stamp, over=True)
    writer.add_page(page)

with open("stamped.pdf", "wb") as stream:
    writer.write(stream)

Штамп, наложенный на страницу средствами PyPDF2

Один и тот же объект штампа не всегда безопасно изменять внутри цикла: накопленная трансформация может перейти на следующую страницу. Надёжнее каждый раз получать независимую копию либо применять преобразование при объединении, не меняя шаблон. Для проверки создайте набор портретных, альбомных и повёрнутых страниц и убедитесь, что штамп не перекрывает номер, подпись или поле для печати. На документах с CropBox также проверяйте не только MediaBox.

Водяной знак в PDF остаётся частью содержимого и не является технической защитой от копирования. Его можно скрыть, изменить или удалить инструментами редактирования. Используйте знак для визуальной маркировки экземпляра, указания статуса или номера получателя, но не как единственный механизм контроля доступа. Для индивидуального знака сначала создают отдельную страницу-шаблон с нужным текстом, затем объединяют её с документом и сохраняют связь между экземпляром и получателем во внешнем журнале.

Чтение и добавление аннотаций

Аннотации хранятся в массиве /Annots страницы. У элемента можно прочитать подтип, прямоугольник, текст заметки, автора и другие поля, если они присутствуют. Не все записи являются комментариями пользователя: ссылки, виджеты формы и служебные элементы тоже относятся к аннотациям. При выгрузке сначала классифицируйте /Subtype, затем извлекайте только ожидаемые поля и сохраняйте номер страницы.

from PyPDF2 import PdfReader

reader = PdfReader("review.pdf")
for page_no, page in enumerate(reader.pages, start=1):
    annotations = page.get("/Annots") or []
    for ref in annotations:
        item = ref.get_object()
        print(page_no, item.get("/Subtype"), item.get("/Contents"))

AnnotationBuilder создаёт распространённые типы, включая свободный текст, заметку, линию, прямоугольник и ссылку. После создания объект передают writer.add_annotation вместе с номером страницы. Координаты прямоугольника должны находиться в системе координат целевой страницы, а цвет и оформление проверяются в нескольких просмотрщиках: внешний вид может зависеть от appearance stream и возможностей программы, которая открывает PDF.

Свободная текстовая аннотация в документе PyPDF2

Значок текстовой заметки в PDF

Свободный текст подходит для видимой подписи, а текстовая заметка обычно отображается значком и раскрывается по действию пользователя. Линия и прямоугольник полезны для визуального выделения области, но сами по себе не объясняют причину. Для аудита добавляйте /Contents и автора там, где формат это допускает, а во внешнем журнале фиксируйте идентификатор документа и координаты. Тогда замечание можно сопоставить с бизнес-процессом даже после экспорта.

Линейная аннотация, добавленная на страницу

Прямоугольная аннотация для выделения области

При удалении комментариев нельзя бездумно стирать весь /Annots: вместе с ними исчезнут ссылки и виджеты формы. Составьте список подтипов, которые разрешено удалять, и оставляйте остальные элементы. После изменения проверьте переходы по ссылкам, поля формы и визуальный вид в независимом просмотрщике. Для юридически значимых процессов храните исходный файл отдельно: изменение аннотаций меняет байты документа и может сделать существующую цифровую подпись недействительной.

Интерактивные формы

reader.get_fields() возвращает сведения о полях AcroForm, включая имена, типы и значения. Имя поля не всегда совпадает с подписью, которую видит пользователь, а несколько виджетов могут относиться к одному логическому полю. Перед заполнением выведите структуру полей в диагностический файл и сопоставьте имена с образцом. Не строьте автоматизацию только по позиции элемента на странице: координаты могут измениться при изменения бланка.

from PyPDF2 import PdfReader, PdfWriter

reader = PdfReader("form.pdf")
fields = reader.get_fields() or {}
for name, field in fields.items():
    print(name, field.get("/FT"), field.get("/V"))

writer = PdfWriter()
writer.append(reader)
writer.update_page_form_field_values(
    writer.pages[0],
    {"customer_name": "Иван Петров", "order_id": "A-1042"},
)
with open("filled.pdf", "wb") as stream:
    writer.write(stream)

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

Флажки, переключатели и выпадающие списки используют внутренние экспортные значения, которые могут отличаться от видимого текста. Сначала изучите словарь поля и допустимые состояния, затем записывайте одно из них. Для пакетного заполнения валидируйте обязательные поля до создания PDF и формируйте отчёт о пропусках. PyPDF2 не выполняет бизнес-правила формы и не знает, какое значение допустимо по смыслу, поэтому такую проверку реализует прикладной код.

Пароли и шифрование

Для создания защищённого документа страницы сначала добавляют в Writer, затем вызывают encrypt и только после этого write. Пользовательский пароль открывает документ, пароль владельца управляет разрешениями и изменением настроек. Не оставляйте одинаковый пароль для всех получателей и не формируйте его из легко угадываемых данных. В автоматизированной системе пароль передают по отдельному каналу, а журнал не должен содержать его значение или строку подключения к хранилищу секретов.

from PyPDF2 import PdfReader, PdfWriter

reader = PdfReader("statement.pdf")
writer = PdfWriter()
for page in reader.pages:
    writer.add_page(page)
writer.encrypt(user_password, owner_password)

with open("statement-protected.pdf", "wb") as stream:
    writer.write(stream)

Алгоритм шифрования и поддержка конкретного PDF-просмотрщика должны соответствовать требованиям получателя. Современные варианты AES могут потребовать дополнительную криптографическую зависимость. Если среда её не содержит, ошибка проявится при обработке, поэтому проверку выполняют в том же контейнере или виртуальном окружении, где работает задача. После записи откройте файл с правильным и неправильным паролем и убедитесь, что ожидаемая политика действительно действует.

Снятие пароля возможно только при наличии подходящего пароля и права на обработку документа. После decrypt страницы добавляют в новый Writer и записывают без вызова encrypt. Это создаёт новый незащищённый файл; исходный остаётся неизменным. Храните результат в каталоге с ограниченным доступом и не оставляйте временные копии в общей папке. Парольная защита не заменяет управление доступом к файловой системе, резервным копиям и журналам.

Вложения и пакет документов внутри PDF

PdfWriter.add_attachment добавляет файл как встроенное вложение. Это удобно для исходных таблиц, машинно читаемых данных или сопроводительного текста, которые должны путешествовать вместе с PDF. Имя вложения задавайте без каталогов и запрещённых символов, а байты считывайте в бинарном режиме. Получатель увидит вложение только в просмотрщике, который поддерживает панель файлов; оно не становится обычной страницей и не появится при печати.

from pathlib import Path
from PyPDF2 import PdfReader, PdfWriter

writer = PdfWriter()
writer.append("report.pdf")
data = Path("data.csv").read_bytes()
writer.add_attachment("data.csv", data)

with open("report-with-data.pdf", "wb") as stream:
    writer.write(stream)

Встроенный файл следует считать таким же недоверенным содержимым, как обычное вложение электронной почты. При извлечении используйте безопасный каталог, очищайте имя, ограничивайте размер и проверяйте тип по сигнатуре. Не запускайте извлечённый файл автоматически. Если документ проходит антивирусный контроль, убедитесь, что сканер анализирует вложения внутри PDF, иначе внешний контейнер может получить разрешённый статус, а опасное содержимое останется непросмотренным.

При повторной сборке документа проверяйте, сохранились ли исходные вложения и навигационные структуры. Операция добавления только страниц не обязана копировать все элементы каталога документа. Если вложения являются обязательной частью архива, составьте тест с известным файлом, после записи снова откройте PDF и перечислите embedded files. Такой контроль важнее визуального открытия первой страницы, где отсутствие вложения никак не заметно.

Работа с потоками памяти и облачными хранилищами

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

from io import BytesIO
from PyPDF2 import PdfReader, PdfWriter

input_stream = BytesIO(pdf_bytes)
reader = PdfReader(input_stream)
writer = PdfWriter()
writer.append(reader)

output_stream = BytesIO()
writer.write(output_stream)
result_bytes = output_stream.getvalue()

Для больших документов поток в памяти может удвоить или утроить потребление RAM: одновременно существуют входные байты, разобранные объекты и выходной буфер. На сервере задавайте предел размера до чтения, используйте временное хранилище для крупных файлов и ограничивайте число параллельных задач. Размер Content-Length не всегда доступен и не защищает от распакованных потоков внутри PDF, поэтому контролируйте и время обработки, и пиковую память процесса.

При передаче результата в облачное хранилище сначала завершите writer.write, затем вычислите хеш и только после этого публикуйте объект. Хеш позволяет сопоставить журнал обработки с конкретными байтами. Для защиты от частичных загрузок пишите под временным ключом, проверяйте размер и контрольную сумму, а затем перемещайте или обновляйте указатель на завершённый объект. Никогда не объявляйте задачу успешной только потому, что PdfWriter не выбросил исключение.

Уменьшение размера и нормализация потоков

page.compress_content_streams() объединяет и сжимает потоки команд содержимого страницы. Метод способен уменьшить файлы, где команды хранились без сжатия, но не гарантирует заметного эффекта для уже оптимизированного документа. Он также не понижает разрешение JPEG и не превращает цветной скан в серый. Перед массовым применением измеряйте размер до и после, время обработки и визуальное соответствие на файлах с текстом, векторной графикой и изображениями.

reader = PdfReader("input.pdf")
writer = PdfWriter()
for page in reader.pages:
    page.compress_content_streams()
    writer.add_page(page)
with open("compressed.pdf", "wb") as stream:
    writer.write(stream)

Не оценивайте качество оптимизации только по байтам. Повторная запись может убрать или изменить служебные структуры, которые нужны вашему процессу: вложения, JavaScript, закладки, формы, теги доступности или цифровые подписи. Составьте перечень обязательных свойств и проверяйте каждое после преобразования. Если задача допускает только побайтовое сохранение подписанного документа, вообще не перезаписывайте его: любое изменение может нарушить проверку подписи.

Для сканов основное уменьшение обычно достигается перекодированием изображений, выбором разумного DPI и удалением невидимых дублей. PyPDF2 может помочь разобрать страницы и собрать результат, но не заменяет полноценный оптимизатор изображений или PDF-движок. Практичный конвейер извлекает либо рендерит изображения специализированным компонентом, оптимизирует их с контролем качества, создаёт страницы и затем проверяет, что текстовый слой, ориентация и размеры сохранились.

Устойчивость, журналы и исключения

Ошибки чтения следует обрабатывать на уровне одного документа. PdfReadError указывает на проблему разбора, PdfStreamError — на поток, FileNotDecryptedError — на обращение к защищённым данным без успешной расшифровки, PageSizeNotDefinedError — на отсутствие размера там, где он нужен. Не перехватывайте Exception без регистрации типа и контекста: иначе разные причины превратятся в одно сообщение не удалось, а оператор не поймёт, нужен ли пароль, новый файл или исправление кода.

import logging
from PyPDF2 import PdfReader
from PyPDF2.errors import PdfReadError, PdfStreamError, FileNotDecryptedError

log = logging.getLogger("pdf-pipeline")

try:
    reader = PdfReader(path, strict=False)
    page_count = len(reader.pages)
except FileNotDecryptedError:
    log.warning("Нужен пароль: %s", path.name)
except (PdfReadError, PdfStreamError) as exc:
    log.error("Повреждённый PDF %s: %s", path.name, exc)

Предупреждения библиотеки поступают через logging. Настройте отдельный обработчик для имени PyPDF2, чтобы видеть исправленные ссылки, несоответствие таблицы xref и другие признаки дефекта. В рабочем процессе предупреждение не всегда означает отказ, но его полезно сохранить рядом с идентификатором файла. Если один и тот же тип предупреждения встречается массово от конкретного поставщика, это основание исправить генератор на входе, а не бессрочно полагаться на восстановление.

Повторная попытка должна менять условие, а не просто запускать тот же код. Например, после строгого чтения можно попробовать мягкий режим, после ошибки сетевого чтения — заново получить объект, после нехватки памяти — перевести задачу на дисковый поток. Бесконечный retry на повреждённом PDF расходует очередь и скрывает проблему. Ограничьте число попыток, помечайте причину и отправляйте файл в карантин, где его можно открыть независимым валидатором.

Типовые ошибки API и способы исправления

DeprecationError при старых именах классов и методов

Код с PdfFileReader, PdfFileWriter, getPage, getNumPages или extractText может завершиться DeprecationError. Исправление состоит не в подавлении исключения, а в переходе на PdfReader, PdfWriter, коллекцию pages, len и extract_text. Замена должна быть последовательной: если обновить только конструктор, следующий устаревший вызов остановит программу позже. Поиск по проекту выполняют по всем старым именам, после чего запускают тесты на реальных PDF.

Сообщение DeprecationError при устаревшем вызове PyPDF2

Исправленный пример чтения PDF через PdfReader

Успешный вывод текста после исправления кода

Соответствия для частых замен выглядят так: reader.getNumPages() превращается в len(reader.pages), reader.getPage(i) — в reader.pages[i], page.extractText() — в page.extract_text(), а writer.addPage(page) — в writer.add_page(page). Не выполняйте механическую замену без проверки аргументов: некоторые методы изменили не только написание, но и поведение. Тест должен сравнивать число страниц, текст контрольной страницы, метаданные и открытие результата.

Пустая строка при extract_text

Сначала определите, есть ли на странице текстовый слой. Выделяется ли текст в обычном просмотрщике, возвращает ли page.images крупное изображение размером со страницу, присутствуют ли шрифты в ресурсах? Если это скан, добавьте OCR до извлечения. Если текст выделяется, но результат странный, причиной могут быть нестандартное кодирование шрифта, отсутствующая ToUnicode-карта или порядок операторов. Сравните несколько страниц и сохраните проблемный образец для отдельного анализатора.

EOF marker not found и повреждённая таблица ссылок

Сообщение об отсутствии EOF или неверной xref часто появляется у незавершённой загрузки и некорректного генератора. Сначала сравните размер с источником и повторно скачайте файл; не пытайтесь чинить обрезанный документ в коде. Затем откройте его независимым просмотрщиком и валидатором. Мягкий режим способен восстановить некоторые ссылки, но восстановленный результат нужно записать в новый файл и полностью проверить, особенно количество страниц и вложения.

Страница выглядит пустой после merge_page

Проверьте порядок слоёв, прозрачность, CropBox, поворот и координаты. Наложенный объект может находиться под непрозрачным фоном либо за границей холста. Нормализуйте поворот, временно нарисуйте заметную рамку, примените expand при преобразовании и откройте результат в нескольких просмотрщиках. Если исходный шаблон использует необычные группы прозрачности, простой merge не всегда воспроизводит ожидаемый вид, и потребуется полноценный движок композиции.

Файл занят или результат имеет нулевой размер

На Windows выходной файл может оставаться занятым, если поток не закрыт. Используйте with и не держите Reader, Writer и внешний файловый дескриптор дольше необходимого. Нулевой размер обычно означает, что программа создала файл, но не дошла до writer.write или завершилась до закрытия буфера. Записывайте во временный путь, проверяйте размер и только затем заменяйте целевой файл. Для журналов используйте абсолютный путь, чтобы не искать результат в неожиданном рабочем каталоге.

Пакетная обработка каталога

Пакетный скрипт должен явно задавать входной и выходной каталоги, маску файлов и правило конфликта имён. Не сохраняйте результат рядом с исходником под именем, которое снова попадает под ту же маску: следующий запуск начнёт обрабатывать собственные результаты. Удобно использовать отдельные папки incoming, processed, failed и output, а перемещение выполнять только после успешной проверки. Для каждого входа храните статус и контрольную сумму.

from pathlib import Path
from PyPDF2 import PdfReader, PdfWriter

incoming = Path("incoming")
output = Path("output")
failed = Path("failed")
output.mkdir(exist_ok=True)
failed.mkdir(exist_ok=True)

for source in sorted(incoming.glob("*.pdf")):
    try:
        reader = PdfReader(source)
        writer = PdfWriter()
        for page in reader.pages:
            writer.add_page(page)
        temp = output / (source.stem + ".tmp")
        final = output / source.name
        with temp.open("wb") as stream:
            writer.write(stream)
        assert len(PdfReader(temp).pages) == len(reader.pages)
        temp.replace(final)
    except Exception:
        source.replace(failed / source.name)
        raise

Пример намеренно показывает общий каркас, но в реальной очереди нельзя прекращать всю партию из-за одного исключения. Регистрируйте ошибку, перемещайте проблемный документ и продолжайте. Перед перемещением убедитесь, что имя не конфликтует с уже существующим файлом; лучше добавлять идентификатор задания или хеш. Временное расширение должно отличаться от .pdf, чтобы незавершённый файл не подхватил следующий процесс или пользователь.

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

Проверка результата и автоматические тесты

Минимальная проверка подтверждает, что файл существует, имеет ненулевой размер, открывается новым PdfReader и содержит ожидаемое число страниц. Этого недостаточно для преобразования: дополнительно сравнивают размеры страниц, наличие текста на контрольных листах, метаданные, закладки, поля формы, аннотации, вложения и состояние шифрования. Набор проверок зависит от операции; например, после разбиения сумма страниц всех частей должна равняться исходному числу, а после удаления диапазона — ожидаемой разности.

Создайте небольшой корпус документов: текстовый PDF, скан, альбомная страница, файл с CropBox, форма, аннотации, пароль, вложение и повреждённый образец. Тесты должны включать не только успешные случаи, но и ожидаемые отказы. Не используйте конфиденциальные документы в репозитории; сгенерируйте синтетические файлы или обезличьте содержимое. Для визуальных операций храните эталонные изображения страниц и сравнивайте их с допустимым порогом, поскольку структурное открытие не гарантирует правильный вид.

После обновления окружения запускайте регрессионный набор до развёртывания. Особое внимание уделяйте API, который затрагивает шифрование, формы и трансформации: небольшое отличие способно проявиться только в отдельном просмотрщике или на необычном PDF. Закрепление точного номера зависимости делает сборку повторяемой, но не отменяет тестирования входных файлов. Если обработка доступна пользователям, сохраняйте идентификатор сборки вместе с отчётом, чтобы воспроизвести поведение.

Безопасная обработка недоверенных PDF

PDF — сложный контейнер с потоками, объектами, вложениями и ссылками, поэтому неизвестный файл нельзя считать безопасным только потому, что он заканчивается на .pdf. Запускайте обработку в отдельном процессе или контейнере без лишних прав, задавайте лимит времени, памяти, размера файла и числа страниц. Рабочий каталог должен быть временным, а сетевой доступ — отключён, если он не нужен. После превышения лимита процесс завершают, а файл помещают в карантин.

Не доверяйте именам вложений, метаданным и строкам аннотаций. При выводе в HTML или журнал экранируйте управляющие символы, при сохранении файла удаляйте пути и запрещайте переходы .., при формировании CSV защищайтесь от формул в начале ячейки. Ограничивайте общий объём извлечённых данных: небольшой контейнер может содержать сильно сжатый поток, который резко увеличится после декодирования. Любое внешнее приложение, вызываемое для OCR или рендеринга, получает те же ограничения.

Цифровую подпись PyPDF2 не создаёт и не валидирует как доверенный криптографический процесс. Для подписания и проверки применяют специализированный инструмент, который умеет работать с сертификатами, цепочками доверия, метками времени и инкрементальными обновлениями. Если подписанный PDF проходит через Writer, подпись может стать недействительной, даже когда страница визуально не изменилась. Поэтому подписание размещают после всех преобразований, а проверку подписи — до любых действий, способных переписать файл.

Практические сценарии

Сборка пакета договоров

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

Если приложения имеют разные ориентации, их не нужно насильно растягивать. Достаточно нормализовать поворот, чтобы штамп экземпляра и номер пакета попадали в ожидаемый угол. При наличии подписанных входов сборку проводят до подписания итогового документа либо хранят подписанные файлы как вложения, не переписывая их байты. Решение зависит от регламента, но автоматизация должна явно сообщать, что Writer создаёт новый PDF и не сохраняет криптографическую целостность старой подписи.

Разделение общего скана по штрихкоду или маркеру

PyPDF2 сам не распознаёт штрихкод, однако управляет страницами вокруг внешнего распознавателя. Сначала каждая страница рендерится или из неё извлекается крупное изображение, распознаватель возвращает идентификатор, а скрипт накапливает страницы до следующего маркера. Затем PdfWriter создаёт отдельный документ с безопасным именем. Не удаляйте маркерную страницу автоматически, пока не определено, является ли она титулом документа или техническим разделителем.

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

Подготовка документов к полнотекстовому поиску

Конвейер сначала пытается извлечь текст с каждой страницы и измеряет его длину. Страницы с достаточным текстовым слоем передаются индексатору, а страницы с пустым результатом отправляются в OCR. После распознавания текст сохраняется отдельно или добавляется в PDF специализированным компонентом, затем PyPDF2 повторно проверяет извлечение. Индекс должен хранить номер физической страницы, потому что печатная нумерация может начинаться позже и повторяться в приложениях.

Для колонтитулов применяют visitor_text и границы по координате, но правило подбирают по типу документа. Нельзя одним порогом обрабатывать A4, альбомные таблицы и маленькие квитанции. Сначала классифицируют размер и ориентацию, затем выбирают профиль. Исходный текст сохраняют для отладки, а нормализованный текст — для поиска. Это позволяет понять, потерялось ли слово при чтении PDF или на этапе очистки.

Массовое заполнение бланков

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

Визуальная проверка всё равно нужна: наличие значения в словаре не гарантирует корректный appearance. Для контрольной выборки страницы рендерят и сравнивают с эталоном, особенно для длинных строк, кириллицы, флажков и переключателей. Если получатель требует плоский документ, отдельный этап должен надёжно перенести внешний вид в содержимое. Простое удаление AcroForm после update_page_form_field_values не считается проверенным flatten и может оставить пустые области.

Маркировка экземпляров перед отправкой

Для каждого получателя создают персональный одностраничный штамп с идентификатором, датой и допустимым текстом. PyPDF2 накладывает его на все страницы или только на титульную, после чего в журнал записываются хеш результата и идентификатор получателя. Штамп не должен содержать секретов: любой человек, получивший PDF, увидит его. Координаты рассчитывают по размеру страницы, чтобы знак не выходил за CropBox на альбомных и нестандартных листах.

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

Проверка входящих PDF по техническому профилю

Организация может задать профиль: допустимый диапазон страниц, формат листа, наличие текста, запрет шифрования и обязательные метаданные. Скрипт читает каждый документ, формирует список нарушений и не меняет исходник. PyPDF2 хорошо подходит для таких структурных проверок, но не подтверждает полное соответствие стандарту PDF/A и не анализирует визуальный контент как человек. Для нормативной валидации его дополняют специализированным валидатором.

Отчёт должен отличать критическую ошибку от предупреждения. Невозможность открыть файл или несоответствие числа страниц блокирует импорт, пустой Title может лишь потребовать заполнения, а нестандартный размер листа — ручной проверки. Вместо одного статуса сохраняют код правила, фактическое значение и ожидаемый диапазон. Так поставщик сможет исправить генератор, а не угадывать причину отказа по общей фразе.

Сравнение PyPDF2 с аналогами

ПрограммаЛучше подходит дляГлавное ограничение
PyPDF2Скриптового объединения, разделения, чтения текста, форм, аннотаций и шифрования средствами PythonНет графического интерфейса и встроенного OCR
PDF CommanderРучного редактирования, сборки и оформления PDF пользователем без написания кодаНе предназначен как Python-библиотека для серверного конвейера
PyMuPDFБыстрого рендеринга страниц, извлечения, поиска и работы с графикойAPI и модель лицензирования требуют отдельной оценки для проекта
pikepdfНизкоуровневого изменения структуры PDF и надёжной пересборки на базе QPDFНе ориентирован на восстановление логического порядка текста
pdfplumberАнализа координат текста и извлечения таблиц из текстовых PDFНе является универсальным редактором и сборщиком документов
pdfminer.sixДетального разбора текстовой компоновки и объектов страницыСоздание и визуальное редактирование PDF не являются основной задачей

PyPDF2 выбирают, когда операции должны выполняться из Python и основной акцент сделан на страницах, структуре документа и повторяемом конвейере. PDF Commander удобнее для ручной работы, где пользователь ожидает визуальный редактор. PyMuPDF полезен, когда критичны скорость и рендеринг, pikepdf — для низкоуровневой структуры и пересборки, pdfplumber и pdfminer.six — для координатного анализа текста. На практике инструменты можно сочетать: один компонент распознаёт или рендерит, а PyPDF2 собирает, защищает и проверяет итоговый пакет.

Ограничения, которые важно учитывать

Библиотека не предлагает визуального редактирования: положение штампа, рамки или формы задаётся координатами и проверяется отдельным просмотрщиком. Это преимущество для воспроизводимого конвейера, но дополнительная сложность для разовой ручной правки. Если задача состоит в том, чтобы перетащить объект мышью, исправить опечатку прямо на странице или визуально переставить блоки, лучше использовать PDF-редактор, а PyPDF2 оставить для автоматизации повторяющихся операций.

Отсутствие OCR означает, что скан без текстового слоя не станет доступным для поиска от одного вызова extract_text. Распознавание выполняет внешний OCR, который возвращает текст или PDF с текстовым слоем. После этого PyPDF2 может разделить документ, объединить его с приложениями, добавить метаданные и пароль. Чёткое разделение этапов помогает диагностировать качество: ошибки букв относятся к OCR, а ошибки порядка страниц — к сборке.

Создание и доверенная проверка цифровых подписей требуют специализированного криптографического инструмента. Простая запись Writer изменяет структуру файла и способна нарушить существующую подпись. Поэтому все операции с содержимым выполняют до подписания, а после подписания документ передают без пересборки. Если нужно лишь парольное шифрование, PyPDF2 поддерживает соответствующий процесс, но пароль и цифровая подпись решают разные задачи и не заменяют друг друга.

Рекомендуемая структура собственного проекта

В небольшом скрипте функции удобно разделить по ответственности: load_document открывает и проверяет вход, transform_pages меняет страницы, build_output собирает Writer, validate_output повторно читает результат, а publish выполняет атомарную замену или загрузку. Конфигурация содержит диапазоны, каталоги и правила, но не секреты. Пароли поступают через переменные окружения или менеджер секретов. Такое разделение облегчает тестирование и не позволяет сетевой ошибке смешаться с ошибкой геометрии страницы.

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

Документация проекта должна фиксировать, какие свойства сохраняются при каждой операции. Например, функция split может переносить страницу, но не обязана сохранять каталог вложений; функция stamp меняет содержимое и делает подпись недействительной; функция extract_text не выполняет OCR. Такие контракты предотвращают ложные ожидания и помогают выбрать другой компонент, когда требование выходит за границы библиотеки.

Выбор страниц по содержимому

Иногда диапазон заранее неизвестен: нужно оставить страницы, на которых встречается номер договора, слово Приложение или другой маркер. Скрипт извлекает текст каждой страницы, нормализует пробелы и проверяет регулярное выражение, после чего добавляет подходящие страницы в Writer. Условие должно работать с пустой строкой и учитывать, что текст может быть разбит переводами строк. Для кириллицы обычно достаточно Unicode-строки, но регистр и дефисы лучше нормализовать явно. Результат проверки сохраняют вместе с физическим номером страницы.

import re
from PyPDF2 import PdfReader, PdfWriter

reader = PdfReader("register.pdf")
writer = PdfWriter()
pattern = re.compile(r"договор\s+№?\s*A-1042", re.IGNORECASE)
selected = []

for index, page in enumerate(reader.pages):
    text = " ".join((page.extract_text() or "").split())
    if pattern.search(text):
        writer.add_page(page)
        selected.append(index + 1)

if not selected:
    raise ValueError("Маркер не найден ни на одной странице")

Поиск по извлечённому тексту не подходит как единственный критерий для сканов и сложной кодировки шрифта. Если маркер критичен, добавьте второй канал: OCR для пустых страниц, штрихкод или проверку внешнего реестра. Не считайте отсутствие совпадения доказательством отсутствия текста на изображении. Также не объединяйте найденные страницы без контекста, если документ содержит продолжения: правило может включать страницу с заголовком и несколько следующих листов до нового разделителя. Такое состояние удобнее реализовать конечным автоматом, а не одним регулярным выражением.

Закладки, назначения и навигация

Закладки помогают перейти к разделу, но они не являются частью текста страницы. При append можно импортировать outline исходного файла, отключить его или добавить собственный элемент для вставляемого документа. Если несколько источников содержат одинаковые названия, итоговая панель навигации станет неоднозначной, поэтому заголовок формируют из типа документа и идентификатора. После сборки проверяют, что каждая закладка ведёт на существующую страницу и не сместилась из-за вставки титульного листа.

Именованные назначения могут использоваться ссылками и внешними процессами. Простое копирование страниц не гарантирует сохранение всей навигационной структуры, особенно когда ссылки указывают на объекты за пределами выбранного диапазона. Для сокращённого документа решите заранее, что делать с такой ссылкой: удалить, перенаправить на первую доступную страницу или считать ошибкой. Молчаливо оставленная ссылка на отсутствующую цель ухудшает документ, хотя страницы визуально открываются без проблем.

Печатные номера страниц и физические индексы также могут различаться. Если исходник использует римские цифры для введения и арабские для основной части, добавление или удаление страницы способно сделать подписи навигации неточными. PyPDF2 позволяет работать со страницами как с объектами, но прикладной код должен решить, нужно ли переносить PageLabels и как их проверить. Для строгого архива сохраняют карту физический индекс — отображаемая метка — название закладки.

Производительность и параллельная обработка

Обработка нескольких файлов хорошо распараллеливается на уровне документов, но один Reader и Writer не следует совместно изменять из разных потоков. Каждая задача получает собственные объекты и собственный временный файл. Для CPU-затратного декодирования и внешнего OCR чаще подходят процессы, а для ожидания сетевого хранилища — ограниченный пул потоков. Число работников выбирают по памяти: крупный PDF может занимать существенно больше своего размера на диске, поэтому запуск десятков задач одновременно приводит к обмену и аварийному завершению.

Измеряйте время отдельно для загрузки, разбора, преобразования, записи и повторной проверки. Общая цифра не показывает узкое место. Если медленно извлечение текста, уменьшение числа writer.write ничего не изменит; если тормозит сетевое хранилище, полезен временный файл на диске и пакетная публикация. Для длинных документов журналируйте прогресс по страницам с умеренной частотой, например каждые сто страниц, чтобы не превратить логирование в основную нагрузку.

Кэшировать PageObject между независимыми документами нельзя: косвенные ссылки принадлежат конкретному Reader. Безопасно кэшировать неизменяемые настройки, скомпилированные регулярные выражения и байты небольшого шаблона, но для каждого задания создавать новый Reader. Если один шаблон штампа применяется много раз, проверьте, что преобразования не изменяют его накопительно. Самый простой способ избежать скрытого состояния — читать шаблон заново из сохранённых байтов в BytesIO.

Сравнение двух PDF и поиск дубликатов

Побайтовый хеш определяет только точное совпадение. Два визуально одинаковых PDF могут иметь разные метаданные, порядок объектов или степень сжатия и поэтому разные SHA-256. Для структурного сравнения PyPDF2 помогает получить число страниц, размеры, нормализованный текст и список изображений. Эти признаки можно сравнить по страницам, но они не доказывают визуальную идентичность: текст может совпасть при разном расположении, а векторная графика не попадёт в список изображений.

Практичный детектор использует несколько уровней. Сначала проверяется точный хеш, затем число страниц и размеры, после этого хеш нормализованного текста каждой страницы. Для кандидатов с одинаковыми признаками страницы рендерятся внешним движком и сравниваются перцептивно. Такой порядок экономит ресурсы и даёт объяснимый результат. Не удаляйте документ автоматически только по совпадению извлечённого текста: пустые сканы дадут одинаковую пустую строку, хотя изображения могут быть совершенно разными.

При проверке изменений сохраняйте отчёт по страницам: добавлена, удалена, изменён текст, изменён размер или требуется визуальное сравнение. Если документ содержит персональные данные, не записывайте весь текст в журнал; храните хеш фрагмента и безопасное краткое описание. Для юридического аудита исходники остаются неизменными, а сравнение выполняется над копиями. PyPDF2 даёт структурные признаки, но окончательный статус равенства должен соответствовать цели: побайтовой, текстовой или визуальной.

Сохранение форм, аннотаций и служебных объектов

Операция writer.add_page переносит страницу, но документ состоит не только из страниц. Каталог может содержать AcroForm, Names, Outlines, Metadata, PageLabels, вложения и другие структуры. Если бизнес-требование говорит сохранить всё, простого цикла add_page недостаточно без проверки каждого типа объекта. append всего документа обычно лучше сохраняет связанную структуру, однако выбранные диапазоны и объединение нескольких источников всё равно требуют теста.

Для формы проверьте get_fields до и после, для аннотаций — число и подтипы /Annots по страницам, для вложений — имена и хеши байтов, для закладок — цели. Метаданные сравнивают отдельно, потому что Writer может получить новые поля. Такой контроль превращает абстрактное PDF открывается в конкретный контракт. Если структура не поддерживается выбранной операцией, это лучше сообщить до обработки и предложить другой маршрут, чем выпустить визуально правдоподобный, но неполный файл.

Особенно осторожно обращайтесь с действиями документа, JavaScript и внешними ссылками. Их сохранение может быть нежелательно по политике безопасности, а удаление — ломать рабочий сценарий формы. Составьте белый список разрешённых возможностей и проверяйте его после записи. PyPDF2 позволяет исследовать словари, но решение о том, что считать безопасным и обязательным, принимает прикладная система. Для входов из интернета разумно по умолчанию запрещать активные элементы и направлять исключения на ручную проверку.

Итоговый порядок действий

  1. Создать изолированное окружение и установить базовые либо необходимые дополнительные зависимости.
  2. Открыть вход через PdfReader, проверить шифрование, число страниц, размеры и обязательные метаданные.
  3. Выполнить одну чётко определённую операцию над страницами, текстом, формами, аннотациями или структурой.
  4. Записать результат PdfWriter во временный поток или файл, не перезаписывая читаемый источник.
  5. Повторно открыть результат и проверить свойства, важные для конкретного сценария.
  6. Опубликовать файл атомарно, сохранить хеш и журнал, а проблемный вход направить в карантин.

PyPDF2 особенно полезен там, где обработка PDF должна быть предсказуемой, повторяемой и связанной с остальным Python-кодом. Качественный результат зависит не от количества вызванных методов, а от ясного контракта: что извлекается, какие структуры сохраняются, как обрабатываются сканы и пароли, чем подтверждается итог. При таком подходе Reader, Writer и PageObject образуют понятный конвейер, который можно тестировать, масштабировать и безопасно включать в рабочую систему.