Производительность
PDF Oxide v0.3.11 обеспечивает среднее время извлечения текста 0.8 мс в Python на корпусе из 3 830 PDF-файлов — в 5 раз быстрее PyMuPDF и в 15 раз быстрее pypdf, при 100% успешной обработке валидных PDF.
Результаты benchmark
Корпус: 3 830 PDF-файлов
Объединены три независимых публичных набора тестов:
| Набор | PDF-файлов | Источник |
|---|---|---|
| veraPDF | 2 907 | Тестовый корпус соответствия PDF/A |
| Mozilla pdf.js | 897 | Тестовый набор рендеринга PDF в браузере |
| SafeDocs | 26 | Корпус некорректных PDF от DARPA SafeDocs |
Почему этот корпус надёжен
Это не подобранные вручную PDF. Каждый набор является признанным, прошедшим экспертную оценку тестовым корпусом, который поддерживается организациями по стандартизации или крупными проектами с открытым исходным кодом:
-
veraPDF — официальный валидатор соответствия PDF/A, используемый сообществом стандартов PDF более 10 лет. Его 2 907 тестовых файлов атомарны — каждый проверяет ровно одну особенность спецификации PDF — охватывая все версии PDF/A (1A/1B, 2A/2B/2U, 3A/3B/3U, 4/4E/4F), доступность PDF/UA (UA1, UA2), а также ISO 32000-1 (PDF 1.7) и ISO 32000-2 (PDF 2.0). Лицензия CC BY 4.0.
-
Mozilla pdf.js обеспечивает рендеринг PDF в Firefox, обрабатывая миллиарды PDF-файлов ежегодно. Его 897 тестовых файлов покрывают реальные граничные случаи рендеринга: сложные многостраничные документы, типы аннотаций (стили границ, выделения, каретки, вложенные файлы), виджеты форм, необычные кодировки шрифтов, объёмные стресс-тесты и граничные случаи потоков содержимого. 7 файлов намеренно повреждены для проверки корректной обработки ошибок.
-
DARPA SafeDocs — финансируемая правительством США программа исследований безопасности, направленная на устойчивость парсеров. Её 26 файлов нацелены на самые сложные граничные случаи: циклы потоков содержимого в шрифтах Type3, двойные трейлеры startxref, компактный синтаксис PDF, вариации диалектов, граничные случаи инлайн-изображений, рекурсивное вложение шрифтов и зашифрованные PDF с паролями Unicode (включая UTF-16 LE). Эти файлы спроектированы так, чтобы вызывать сбои, зависания или эксплуатировать уязвимые парсеры.
Что покрывает этот корпус
- Все основные версии PDF: PDF 1.0 — PDF 2.0
- Шифрование и пароли: AES-256, RC4, пароли Unicode, кодировка UTF-16 LE
- Граничные случаи безопасности: рекурсивные структуры, циклы потоков содержимого, некорректные трейлеры, повреждения от фаззинга
- Разнообразие шрифтов: TrueType, CIDFont, Type1, Type3, кодировки CJK, встроенные подмножества
- Сложность документов: от одностраничных фикстур до документов с 10 000+ страниц, инлайн-изображения, вложенные Form XObject
- Корректное отклонение: 7 намеренно повреждённых файлов (отсутствующие заголовки PDF, невалидные потоки xref) — библиотека, которая «парсит» их, менее безопасна, а не более надёжна
Из 3 830 файлов 3 823 являются валидными PDF. 7 невалидных файлов — это тестовые фикстуры для обработки ошибок. PDF Oxide корректно отклоняет все 7.
Сравнение Python-библиотек
Среднее время извлечения текста из одного PDF на полном корпусе из 3 830 файлов:
| Библиотека | Среднее | p99 | Успешность | Лицензия |
|---|---|---|---|---|
| PDF Oxide | 0.8 мс | 9 мс | 100% | MIT |
| PyMuPDF | 4.6 мс | 28 мс | 99.3% | AGPL-3.0 |
| pypdfium2 | 4.1 мс | 42 мс | 99.2% | Apache-2.0 |
| pymupdf4llm | 55.5 мс | 280 мс | 99.1% | AGPL-3.0 |
| pdftext | 7.3 мс | 82 мс | 99.0% | GPL-3.0 |
| pdfminer | 16.8 мс | 124 мс | 98.8% | MIT |
| pdfplumber | 23.2 мс | 189 мс | 98.8% | MIT |
| markitdown | 108.8 мс | 378 мс | 98.6% | MIT |
| pypdf | 12.1 мс | 97 мс | 98.4% | BSD-3 |
PDF Oxide — самая быстрая из доступных Python-библиотек для работы с PDF. В отличие от PyMuPDF, она использует лицензию MIT — без ограничений AGPL для коммерческого использования.
Сравнение Rust-библиотек
| Библиотека | Среднее | p99 | Успешность | Извлечение текста |
|---|---|---|---|---|
| PDF Oxide | 0.8 мс | 9 мс | 100% | Встроенное, production-уровня |
| oxidize_pdf | 13.5 мс | 11 мс | 99.1% | Базовое |
| unpdf | 2.8 мс | 10 мс | 95.1% | Базовое |
| pdf_extract | 4.08 мс | 37 мс | 91.5% | Базовое |
| lopdf | 0.3 мс | 2 мс | 80.2% | Без встроенного извлечения |
lopdf быстрее на тех PDF, которые он способен обработать, но отказывает на 20% корпуса. pdf_oxide — единственный Rust-крейт, который сочетает 100% надёжность со встроенным извлечением текста. Обратите внимание, что lopdf не предоставляет извлечения текста — декодирование шрифтов и анализ межсимвольных интервалов придётся реализовывать самостоятельно.
Качество текста
PDF Oxide достигает 99.5% совпадения текста по сравнению с PyMuPDF и pypdfium2 на полном корпусе. Качество измерялось посимвольным сравнением извлечённого текста по всем 3 823 валидным PDF.
Детализация по корпусам
| Корпус | PDF-файлов | Среднее PDF Oxide | Среднее pypdfium2 | Среднее PyMuPDF |
|---|---|---|---|---|
| veraPDF | 2 907 | 0.7 мс | 3.6 мс | 4.1 мс |
| Mozilla pdf.js | 897 | 1.1 мс | 5.8 мс | 6.2 мс |
| SafeDocs | 26 | 0.9 мс | 4.0 мс | 4.3 мс |
История оптимизаций: v0.3.5 -> v0.3.8
В версии 0.3.8 были устранены два критических узких места O(n), которые позволили снизить среднее время с 23.3 мс до 0.8 мс (Python) на том же корпусе.
1. Массовый кеш дерева страниц
До: get_page() обходил дерево страниц от корня для каждой некешированной страницы. При последовательном извлечении всех страниц это давало O(n) на страницу и O(n^2) суммарно.
После: При первом обращении к странице всё дерево страниц обходится один раз, и все страницы кешируются в HashMap<usize, Object>. Каждый последующий доступ выполняется за O(1).
Именно эта оптимизация позволила обработать 10 000-страничный PDF за 332 мс вместо 55 секунд.
2. Кеш смещений объектов при сканировании
До: Когда объекты отсутствовали в таблице xref, scan_for_object() считывал весь PDF-файл для каждого отсутствующего объекта. PDF с тегами и сотнями элементов дерева структуры, отсутствующих в xref, провоцировали сотни полных считываний файла.
После: Файл сканируется один раз, и все смещения объектов кешируются в HashMap. Последующие обращения выполняются за O(1).
3. Однопроходное извлечение текста
До: extract_spans() выполнял два прохода по содержимому страницы — сначала классификацию типа документа (научная статья, газета, форма и т.д.), затем извлечение текста.
После: Проход классификации полностью исключён. Адаптивные пороги с учётом шрифтов теперь дают такие же или лучшие результаты за один проход.
4. Предварительное выделение памяти для потока содержимого
До: parse_content_stream() начинал формирование Vec операторов с ёмкости по умолчанию, что вызывало многократные перевыделения памяти на больших потоках содержимого.
После: Vec создаётся с предварительно рассчитанной ёмкостью на основе размера потока (data.len() / 20), что соответствует примерно одному оператору на 20 байт.
Методология
Все benchmark используют единую методологию:
- Каждая библиотека обрабатывает все 3 830 PDF с помощью Python multiprocessing (один PDF на процесс)
- Таймаут 60 секунд на PDF — превышение засчитывается как ошибка
- Извлечённый текст сохраняется на диск для каждой библиотеки для сравнения качества
- Измеряется реальное время от открытия файла до завершения извлечения текста
- Без прогревочных запусков, без кеширования между файлами
- Один поток на PDF
Тестовый стенд запускает все 18 библиотек (3 на Rust, 15 на Python) на одной машине, одном корпусе, в одинаковых условиях.
Воспроизведение benchmark
Публичные тестовые корпуса находятся в свободном доступе:
- veraPDF: github.com/veraPDF/veraPDF-corpus
- Mozilla pdf.js: github.com/mozilla/pdf.js/tree/master/test/pdfs
- SafeDocs: github.com/pdf-association/safedocs
Запуск верификации:
cargo run --release --example verify_corpus -- \
/path/to/veraPDF-corpus \
/path/to/pdfjs-test \
/path/to/safedocs \
--csv results.csv
Характеристики производительности
В чём PDF Oxide быстр
- Извлечение текста: основная цель оптимизации. Субмиллисекундное время для типичных документов.
- Последовательное извлечение из многостраничных документов: кеш дерева страниц делает извлечение всех страниц из объёмного документа почти таким же быстрым, как извлечение одной.
- PDF с тегами: обход дерева структуры и разрешение объектов теперь кешируются.
- Некорректные PDF: мягкий парсинг с резервными стратегиями позволяет избежать дорогостоящих повторных попыток.
Что масштабируется линейно
- Количество страниц: каждая страница обрабатывается независимо. 100 страниц занимают примерно в 100 раз больше, чем одна.
- Размер потока содержимого: парсинг операторов линеен по длине потока.
- Извлечение изображений: пропорционально количеству и размеру изображений.
Когда ожидать замедления
- Отсканированные PDF с OCR: обработка OCR (при включении) значительно медленнее извлечения текста.
- Рендеринг: рендеринг страниц в изображения зависит от сложности содержимого и целевого DPI.
- Сильно зашифрованные PDF: дешифрование AES-256 добавляет накладные расходы на каждый поток.
- PDF с тысячами шрифтов: парсинг шрифтов кешируется в рамках документа, но начальный парсинг масштабируется с количеством шрифтов.
Дальнейшие шаги
- Журнал изменений – полная история версий
- Сравнение Python-библиотек – подробное сравнение с PyMuPDF, pypdf, pdfplumber, pdfminer
- Сравнение Rust-библиотек – подробное сравнение с lopdf, pdf_extract, pdf-rs
- Начало работы с Rust – установка и первое извлечение
- Справочник Rust API – полная документация API