Skip to content

Извлечение текста из 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) и кодировок шрифтов, встречающихся в научных публикациях.

Связанные страницы