Извлечение текста из PDF в Python
Извлечение текста из PDF — одна из самых частых задач в конвейерах обработки документов: построение поисковых индексов, подача данных в RAG-системы, data mining, комплаенс. В этом руководстве собрано всё, что нужно, чтобы извлекать текст из PDF в Python, JavaScript и Rust с помощью PDF Oxide: от обычного текста и покоординатного извлечения символов до стилизованных спанов, OCR для сканов, работы с зашифрованными файлами и настройки производительности для пакетных конвейеров.
Извлеките текст из любого PDF в три строки:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("document.pdf")
text = doc.extract_text(0) # страница 0
print(text)
WASM
import { WasmPdfDocument } from "pdf-oxide-wasm";
const bytes = new Uint8Array(buffer);
const doc = new WasmPdfDocument(bytes);
const text = doc.extractText(0); // страница 0
console.log(text);
doc.free();
Rust
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("document.pdf")?;
let text = doc.extract_text(0)?;
println!("{}", text);
Go
package main
import (
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
doc, err := pdfoxide.Open("document.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()
text, err := doc.ExtractText(0) // страница 0
if err != nil { log.Fatal(err) }
fmt.Println(text)
}
C#
using PdfOxide;
using var doc = PdfDocument.Open("document.pdf");
var text = doc.ExtractText(0); // страница 0
Console.WriteLine(text);
PDF Oxide извлекает текст в среднем за 0,8 мс на страницу — это в 5 раз быстрее PyMuPDF и в 15 раз быстрее pypdf, при 100 % успешной обработке 3830 тестовых PDF.
Почему извлечение текста из PDF — непростая задача
PDF — это визуальный формат, а не текстовый. В отличие от HTML или Markdown, PDF-файл не хранит ни «абзацев», ни «предложений»: внутри лежат отдельные символы, расставленные в конкретных координатах страницы. Чтобы из них получился читаемый текст, нужно:
- Раскодировать шрифты — шрифты в PDF сопоставляют коды символов глифам через таблицы кодировок (WinAnsi, MacRoman, Unicode CMap, Type 1, TrueType, CIDFont). Один и тот же код
0x41в одном шрифте означает «A», а в другом — «α». - Разобрать поток текста — операторы
Tj,TJ,',"расставляют символы по странице. Поправки кернинга в массивахTJсдвигают символы на доли пункта, а пропущенные пробелы приходится выводить из расстояний между позициями символов. - Восстановить вёрстку — у символов на странице нет явного порядка чтения. Двухколоночные макеты, колонтитулы, таблицы и боковые панели нужно разбирать пространственно, чтобы получить линейный текстовый поток.
- Закрывать крайние случаи кодировок — CJK-текст (китайский, японский, корейский) использует кодировки CIDFont/CMap с тысячами глифов. Для арабского и иврита требуется разворот справа налево. Лигатуры (fi, fl, ffi) нужно раскладывать на части.
- Учитывать встроенные подмножества — многие PDF встраивают только реально используемые глифы, с собственными векторами кодировки. Шрифт может отображать глиф с индексом 1→«T», 2→«h», 3→«e», не опираясь ни на какой стандарт.
Именно поэтому разные библиотеки выдают для одного и того же файла разный результат, а некоторые и вовсе отказываются работать со сложными документами. PDF Oxide закрывает все эти случаи Rust-парсером, проверенным на 3830 реальных PDF с показателем успеха 100 %.
Установка
Python (PyPI):
pip install pdf_oxide
Готовые wheel-пакеты для Linux (x86_64, aarch64), macOS (Intel и Apple Silicon) и Windows (x86_64). Python 3.8 и новее. Без системных зависимостей: Rust-ядро компилируется прямо в wheel, поэтому ставить Poppler, MuPDF или другие C-библиотеки не нужно.
JavaScript (npm):
npm install pdf-oxide-wasm
Работает в Node.js 18 и новее, а также в современных браузерах. WASM-бинарь уже лежит в пакете.
Rust (Cargo):
cargo add pdf_oxide
Нужен Rust 1.70 или новее. Ничего, кроме стандартного тулчейна Rust, ставить не придётся.
Извлечение всех страниц
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("report.pdf")
full_text = []
for i in range(doc.page_count()):
text = doc.extract_text(i)
full_text.append(text)
print("\n".join(full_text))
WASM
const doc = new WasmPdfDocument(bytes);
const fullText = doc.extractAllText();
console.log(fullText);
doc.free();
Rust
let mut doc = PdfDocument::open("report.pdf")?;
let mut full_text = Vec::new();
for i in 0..doc.page_count()? {
full_text.push(doc.extract_text(i)?);
}
println!("{}", full_text.join("\n"));
Go
doc, err := pdfoxide.Open("report.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()
full, err := doc.ExtractAllText()
if err != nil { log.Fatal(err) }
fmt.Println(full)
C#
using var doc = PdfDocument.Open("report.pdf");
var parts = new List<string>();
for (int i = 0; i < doc.PageCount; i++)
parts.Add(doc.ExtractText(i));
Console.WriteLine(string.Join("\n", parts));
Извлечение текста с позициями символов
Получите точные координаты, имена шрифтов и размеры для каждого символа:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
chars = doc.extract_chars(0)
for ch in chars[:20]:
print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) "
f"font={ch.font_name} size={ch.font_size:.1f}")
WASM
const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);
for (const ch of chars.slice(0, 20)) {
console.log(`'${ch.char}' at (${ch.x.toFixed(1)}, ${ch.y.toFixed(1)}) font=${ch.fontName} size=${ch.fontSize.toFixed(1)}`);
}
doc.free();
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
let chars = doc.extract_chars(0)?;
for ch in chars.iter().take(20) {
println!("'{}' at ({:.1}, {:.1}) font={} size={:.1}",
ch.char, ch.x, ch.y, ch.font_name, ch.font_size);
}
Go
doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
chars, _ := doc.ExtractChars(0)
for _, ch := range chars[:20] {
fmt.Printf("%q at (%.1f, %.1f) font=%s size=%.1f\n",
ch.Char, ch.X, ch.Y, ch.FontName, ch.FontSize)
}
C#
using var doc = PdfDocument.Open("paper.pdf");
var chars = doc.ExtractChars(0);
foreach (var ch in chars.Take(20))
Console.WriteLine($"'{ch.Char}' at ({ch.X:F1}, {ch.Y:F1}) font={ch.FontName} size={ch.FontSize:F1}");
В каждом символе возвращаются:
| Поле | Тип | Описание |
|---|---|---|
char |
str |
Символ Unicode |
x, y |
float |
Позиция в пунктах |
font_size |
float |
Размер шрифта в пунктах |
font_name |
str |
PostScript-имя шрифта |
bbox |
tuple |
Ограничивающий прямоугольник (x0, y0, x1, y1) |
Покоординатное извлечение удобно, когда нужно восстановить таблицы, выявить заголовки по размеру шрифта или построить ограничивающие прямоугольники вокруг текстовых областей. Например, символы можно сгруппировать в строки по координате y, а границы колонок определить по разрывам в x.
Извлечение стилизованных спанов
Сгруппируйте подряд идущие символы по шрифту и размеру:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
spans = doc.extract_spans(0)
for span in spans:
print(f"'{span.text}' font={span.font_name} size={span.font_size:.1f}")
WASM
const doc = new WasmPdfDocument(bytes);
const spans = doc.extractSpans(0);
for (const span of spans) {
console.log(`'${span.text}' font=${span.fontName} size=${span.fontSize.toFixed(1)}`);
}
doc.free();
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
let spans = doc.extract_spans(0)?;
for span in &spans {
println!("'{}' font={} size={:.1}", span.text, span.font_name, span.font_size);
}
Пригодится для распознавания заголовков, жирного текста и построения структурированного вывода.
Пакетная обработка
Обрабатывайте сотни и тысячи PDF:
from pdf_oxide import PdfDocument, PdfError
from pathlib import Path
pdf_dir = Path("documents/")
for pdf_path in pdf_dir.glob("*.pdf"):
try:
doc = PdfDocument(str(pdf_path))
for i in range(doc.page_count()):
text = doc.extract_text(i)
# Обрабатываем текст...
except PdfError as e:
print(f"Пропущен {pdf_path.name}: {e}")
При 0,8 мс на страницу обработка 3830 PDF занимает около 3,1 секунды. Для продакшен-конвейеров смотрите руководство по пакетной обработке с паттернами параллельной обработки через multiprocessing и асинхронный ввод-вывод.
Обработка отсканированных PDF (OCR)
Если PDF содержит не текст, а сканы, extract_text() вернёт пустую или почти пустую строку. В этом случае используйте встроенный в PDF Oxide OCR:
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
text = doc.extract_text(0)
if not text.strip():
# Страница, похоже, отсканирована — используем OCR
text = doc.extract_text_ocr(0)
print(text)
PDF Oxide запускает PaddleOCR через ONNX Runtime — ставить Tesseract не нужно. Про выбор модели и настройку читайте в руководстве по OCR.
Обработка зашифрованных PDF
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("protected.pdf", password="secret")
text = doc.extract_text(0)
print(text)
WASM
const doc = new WasmPdfDocument(bytes);
doc.authenticate("secret");
const text = doc.extractText(0);
console.log(text);
doc.free();
Rust
let mut doc = PdfDocument::open_with_password("protected.pdf", "secret")?;
let text = doc.extract_text(0)?;
println!("{}", text);
Go
doc, _ := pdfoxide.Open("protected.pdf")
defer doc.Close()
if _, err := doc.Authenticate("secret"); err != nil { log.Fatal(err) }
text, _ := doc.ExtractText(0)
fmt.Println(text)
C#
using var doc = PdfDocument.OpenWithPassword("protected.pdf", "secret");
Console.WriteLine(doc.ExtractText(0));
Поддерживаются PDF, зашифрованные AES-256, AES-128 и RC4. В отличие от pdfplumber (который вообще не открывает зашифрованные файлы) и pdfminer (который падает на AES-256), PDF Oxide прозрачно работает со всеми стандартными способами шифрования PDF.
Вывод в Markdown
Если нужен структурированный вывод с заголовками и форматированием:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)
WASM
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
doc.free();
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);
Go
doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
md, _ := doc.ToMarkdown(0)
fmt.Println(md)
C#
using var doc = PdfDocument.Open("paper.pdf");
Console.WriteLine(doc.ToMarkdown(0));
Сценарии интеграции с RAG и LLM смотрите в руководстве PDF → Markdown.
Поиск по PDF
Найдите текст на всех страницах вместе с координатами:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("manual.pdf")
results = doc.search("configuration")
for r in results:
print(f"Page {r.page}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})")
WASM
const doc = new WasmPdfDocument(bytes);
const results = doc.search("configuration", false);
for (const r of results) {
console.log(`Page ${r.page}: '${r.text}' at (${r.x.toFixed(0)}, ${r.y.toFixed(0)})`);
}
doc.free();
Rust
let mut pdf = Pdf::open("manual.pdf")?;
let results = pdf.search("configuration")?;
for r in &results {
println!("Page {}: '{}' at ({:.0}, {:.0})", r.page, r.text, r.bbox.x, r.bbox.y);
}
Go
doc, _ := pdfoxide.Open("manual.pdf")
defer doc.Close()
results, _ := doc.SearchAll("configuration", false)
for _, r := range results {
fmt.Printf("Page %d: %q at (%.0f, %.0f)\n", r.PageIndex, r.Text, r.X, r.Y)
}
C#
using var doc = PdfDocument.Open("manual.pdf");
foreach (var r in doc.SearchAll("configuration", caseSensitive: false))
Console.WriteLine($"Page {r.PageIndex}: '{r.Text}' at ({r.X:F0}, {r.Y:F0})");
Сравнение с другими Python-библиотеками для PDF
Для извлечения текста из PDF в Python есть несколько библиотек. Коротко сравним их:
- pypdf — чистый Python без C-зависимостей. Легко ставится, но медленная (12 мс на страницу) и падает примерно на 1,6 % PDF из-за ограниченной поддержки шрифтов и кодировок. Координат символов не даёт. Подойдёт для простых PDF, где скорость не принципиальна.
- pdfplumber — построена на pdfminer, умеет детально разбирать символы и таблицы. Очень медленная (23 мс на страницу) и не открывает зашифрованные PDF. Хороша для работы с таблицами, когда нужны данные по ячейкам, а производительность не важна.
- PyMuPDF (fitz) — Python-обёртка для C-библиотеки MuPDF. Быстрая (4,6 мс на страницу) и стабильная (99,3 % успеха). Требует установки C-библиотеки и распространяется под AGPL. Хороший выбор, если лицензия подходит проекту.
- pypdfium2 — Python-обёртка для движка PDFium от Google. Быстрая (4,1 мс на страницу), но на сложных документах p99 достигает 42 мс. API заметно уже, чем у PyMuPDF.
- pdfminer.six — чистый Python с подробным анализом вёрстки. Очень медленная и фактически не поддерживается. Падает на AES-256. В большинстве сценариев её вытеснила pdfplumber.
- PDF Oxide — Rust-ядро с Python-биндингами через PyO3. Самая быстрая (0,8 мс на страницу), 100 % успеха, поддержка всех методов шифрования, встроенный OCR. Лицензия MIT, без системных зависимостей.
PDF Oxide и задумывалась как ответ на слабости имеющихся библиотек: ограничения скорости чистых Python-парсеров, лицензионные оговорки MuPDF и проблемы со стабильностью, из-за которых другие библиотеки спотыкаются о реальные PDF с нестандартными шрифтами, сломанными таблицами перекрёстных ссылок или необычными кодировками.
Производительность: насколько быстр PDF Oxide
Измерения проведены на 3830 PDF из трёх независимых публичных тестовых наборов:
| Библиотека | Среднее | p99 | Успех |
|---|---|---|---|
| PDF Oxide | 0,8 мс | 9 мс | 100 % |
| PyMuPDF | 4,6 мс | 28 мс | 99,3 % |
| pypdfium2 | 4,1 мс | 42 мс | 99,2 % |
| pypdf | 12,1 мс | 97 мс | 98,4 % |
| pdfplumber | 23,2 мс | 189 мс | 98,8 % |
Для конвейера из 10 000 PDF:
- PDF Oxide: 8 секунд
- PyMuPDF: 46 секунд
- pypdf: 2 минуты
- pdfplumber: 3,9 минуты
Методологию и шаги воспроизведения ищите в полных бенчмарках.
Частые проблемы и их решение
Пустой текстовый вывод
Если extract_text() возвращает пустую строку, вероятнее всего на странице сканы, а не текст. Используйте extract_text_ocr(). Инструкции по настройке — в OCR для отсканированных PDF.
Крабазяки или неверные символы
Обычно это шрифт с нестандартным вектором кодировки или без CMap ToUnicode. PDF Oxide закрывает большинство таких случаев, но на сознательно запутанных PDF (контент с DRM) вывод всё равно может получиться некорректным.
Пропущенные пробелы и «слипшиеся» слова
Операторы текста в PDF расставляют символы по одному. Вывод пробелов опирается на промежутки между позициями относительно ширины пробела шрифта. Если слова слипаются, попробуйте extract_chars() и реализуйте собственную логику расстановки пробелов по координатам.
Отличия от других библиотек
Разные библиотеки применяют разные эвристики для пробелов, переносов строк и порядка чтения. PDF Oxide показывает 99,5 % совпадения текста с PyMuPDF на 3830 PDF. Оставшиеся 0,5 % приходятся на нормализацию пробелов и обработку лигатур.
Реальные сценарии
Индексация для поиска — Извлеките текст каждой страницы каждого PDF в репозитории документов и отправьте его в Elasticsearch, Typesense или векторную БД для полнотекстового поиска. Скорость PDF Oxide делает практичной переиндексацию тысяч документов по требованию.
RAG-конвейеры (retrieval-augmented generation) — Извлекайте и режьте PDF на чанки для эмбеддингов через OpenAI, Cohere или опенсорсные модели. Через extract_spans() вы сохраняете структуру заголовков, и чанки естественно совпадают с разделами документа. Оптимизированный под LLM вывод описан в руководстве PDF → Markdown.
Комплаенс и аудит — Прогоняйте договоры, счета и отчётность по ключевым словам и формулировкам. doc.search() находит термины по всем страницам вместе с точными позициями, а полный текст можно прогнать через NLP для поиска оговорок.
Извлечение данных — Доставайте структурированные данные из счетов-фактур, чеков, банковских выписок и анкет. Сочетайте extract_chars() с правилами предметной области, чтобы находить поля вроде «Итого» или «Дата счёта» и извлекать значения рядом.
Академические исследования — Обрабатывайте тысячи статей для обзоров литературы, извлечения цитат или мета-анализа. PDF Oxide справляется со всем спектром производителей PDF (LaTeX, Word, InDesign, Quark) и кодировок шрифтов, встречающихся в научных публикациях.
Связанные страницы
- API извлечения текста — полный справочник API
- PDF → Markdown — структурированное преобразование
- Пакетная обработка — паттерны параллельной обработки
- OCR для отсканированных PDF — настройка и использование OCR
- Бенчмарки производительности — методология и результаты