Skip to content

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

Извлечение текста из PDF — одна из самых распространённых задач в конвейерах обработки документов: от построения поисковых индексов и подачи данных в RAG-системы до интеллектуального анализа данных и процессов обеспечения соответствия требованиям. В этом руководстве рассмотрено всё необходимое для извлечения текста из PDF на Python, JavaScript и Rust с использованием PDF Oxide: извлечение простого текста, позиционирование на уровне символов, форматированные спаны, OCR для сканированных документов, работа с зашифрованными файлами и настройка производительности для пакетных конвейеров.

Извлеките текст из любого PDF в три строки:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("document.pdf")
text = doc.extract_text(0)  # page 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); // page 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) // page 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); // page 0
Console.WriteLine(text);

Java

import fyi.oxide.pdf.PdfDocument;
import java.nio.file.Path;

try (PdfDocument doc = PdfDocument.open(Path.of("document.pdf"))) {
    String text = doc.extractText(0); // page 0
    System.out.println(text);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import java.nio.file.Path

PdfDocument.open(Path.of("document.pdf")).use { doc ->
    val text = doc.extractText(0) // page 0
    println(text)
}

Scala

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open("document.pdf")) { doc =>
  val text = doc.extractText(0) // page 0
  println(text)
}

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "document.pdf")]
  (println (pdf/extract-text doc 0))) ; page 0

PHP

use PdfOxide\PdfDocument;

$doc  = PdfDocument::open('document.pdf');
$text = $doc->extractText(0); // page 0
echo $text;
$doc->close();

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('document.pdf') do |doc|
  text = doc.extract_text(0) # page 0
  puts text
end

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>

auto doc  = pdf_oxide::Document::open("document.pdf");
auto text = doc.extract_text(0); // page 0
std::cout << text << '\n';

Swift

import PdfOxide

let doc  = try Document.open("document.pdf")
let text = try doc.extractText(0) // page 0
print(text)

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc  = PdfDocument.open('document.pdf');
final text = doc.extractText(0); // page 0
print(text);
doc.close();

R

library(pdfoxide)

doc  <- pdf_open("document.pdf")
text <- pdf_extract_text(doc, 0) # page 0
cat(text)

Julia

using PdfOxide

doc  = open_document("document.pdf")
text = extract_text(doc, 0) # page 0
println(text)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("document.pdf");
const text = try doc.extractText(a, 0); // page 0
std.debug.print("{s}\n", .{text});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];
NSString *text = [doc extractText:0 error:&err]; // page 0
NSLog(@"%@", text);

Elixir

{:ok, doc}  = PdfOxide.open("document.pdf")
{:ok, text} = PdfOxide.extract_text(doc, 0) # page 0
IO.puts(text)

PDF Oxide извлекает текст со средней скоростью 0,8 мс на страницу — в 5 раз быстрее PyMuPDF и в 15 раз быстрее pypdf — при 100% успешности на 3 830 тестовых 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-библиотеки дают разный текст для одного файла — и почему некоторые полностью не справляются со сложными документами. PDF Oxide обрабатывает все эти случаи с помощью парсера на Rust, протестированного на 3 830 реальных PDF с 100% успешностью.

Установка

Python (PyPI):

pip install pdf_oxide

Предсобранные колёса для Linux (x86_64, aarch64), macOS (Intel и Apple Silicon) и Windows (x86_64). Python 3.8+. Никаких системных зависимостей — ядро на Rust скомпилировано в колесо, поэтому устанавливать 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));

Java

try (PdfDocument doc = PdfDocument.open(Path.of("report.pdf"))) {
    StringBuilder all = new StringBuilder();
    for (int i = 0; i < doc.pageCount(); i++)
        all.append(doc.extractText(i));
    System.out.println(all);
}

Kotlin

PdfDocument.open(Path.of("report.pdf")).use { doc ->
    val all = (0 until doc.pageCount()).joinToString("") { doc.extractText(it) }
    println(all)
}

Scala

Using.resource(PdfDocument.open("report.pdf")) { doc =>
  val all = (0 until doc.pageCount()).map(doc.extractText).mkString
  println(all)
}

Clojure

(with-open [doc (pdf/open "report.pdf")]
  (println (apply str (map #(pdf/extract-text doc %)
                           (range (pdf/page-count doc))))))

PHP

$doc = PdfDocument::open('report.pdf');
$all = '';
for ($i = 0; $i < $doc->pageCount(); $i++) { $all .= $doc->extractText($i); }
echo $all;
$doc->close();

Ruby

PdfOxide::PdfDocument.open('report.pdf') do |doc|
  all = (0...doc.page_count).map { |i| doc.extract_text(i) }.join
  puts all
end

C++

auto doc = pdf_oxide::Document::open("report.pdf");
auto all = doc.extract_all_text();
std::cout << all << '\n';

Swift

let doc = try Document.open("report.pdf")
let all = try doc.extractAllText()
print(all)

Dart

final doc = PdfDocument.open('report.pdf');
final all = doc.extractAllText();
print(all);
doc.close();

R

doc <- pdf_open("report.pdf")
all <- pdf_extract_all_text(doc)
cat(all)

Julia

doc = open_document("report.pdf")
all = extract_all_text(doc)
println(all)

Zig

var doc = try pdf_oxide.Document.open("report.pdf");
const all = try doc.extractAllText(a);
std.debug.print("{s}\n", .{all});

Objective-C

POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
NSString *all = [doc extractAllTextWithError:&err];
NSLog(@"%@", all);

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")
{:ok, n}   = PdfOxide.page_count(doc)
all = 0..(n - 1)
      |> Enum.map(fn i -> {:ok, t} = PdfOxide.extract_text(doc, i); t end)
      |> Enum.join()
IO.puts(all)

Извлечение текста с позициями символов

Получите точные координаты, название шрифта и размер для каждого символа:

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}");

Java

import fyi.oxide.pdf.text.TextChar;

try (PdfDocument doc = PdfDocument.open(Path.of("paper.pdf"))) {
    for (TextChar ch : doc.page(0).chars().subList(0, 20)) {
        System.out.printf("'%s' at (%.1f, %.1f)%n",
            ch.asString(), ch.bbox().x0(), ch.bbox().y0());
    }
}

Kotlin

PdfDocument.open(Path.of("paper.pdf")).use { doc ->
    doc.page(0).chars().take(20).forEach { ch ->
        println("'${ch.asString()}' at (${ch.bbox().x0()}, ${ch.bbox().y0()})")
    }
}

Scala

import fyi.oxide.pdf.charsSeq

Using.resource(PdfDocument.open("paper.pdf")) { doc =>
  doc.page(0).charsSeq.take(20).foreach { ch =>
    println(f"'${ch.asString}' at (${ch.bbox.x0}%.1f, ${ch.bbox.y0}%.1f)")
  }
}

Clojure

(with-open [doc (pdf/open "paper.pdf")]
  (doseq [ch (take 20 (pdf/chars (pdf/page doc 0)))]
    (let [b (.bbox ch)]
      (println (format "'%s' at (%.1f, %.1f)"
                       (.asString ch) (.x0 b) (.y0 b))))))

C++

auto doc   = pdf_oxide::Document::open("paper.pdf");
auto chars = doc.extract_chars(0);
int shown = 0;
for (const auto& ch : chars) {
    if (shown++ >= 20) break;
    std::printf("U+%04X at (%.1f, %.1f) font=%s size=%.1f\n",
        ch.character, ch.bbox.x, ch.bbox.y,
        ch.font_name.c_str(), ch.font_size);
}

Swift

let doc   = try Document.open("paper.pdf")
let chars = try doc.extractChars(0)
for ch in chars.prefix(20) {
    let s = String(UnicodeScalar(ch.character) ?? " ")
    print("'\(s)' at (\(ch.bbox.x), \(ch.bbox.y)) font=\(ch.fontName) size=\(ch.fontSize)")
}

Dart

final doc   = PdfDocument.open('paper.pdf');
final chars = doc.extractChars(0);
for (final ch in chars.take(20)) {
  final s = String.fromCharCode(ch.character);
  print("'$s' at (${ch.bbox.x}, ${ch.bbox.y}) "
      "font=${ch.fontName} size=${ch.fontSize}");
}
doc.close();

R

doc   <- pdf_open("paper.pdf")
chars <- pdf_extract_chars(doc, 0)
for (ch in head(chars, 20)) {
  cat(sprintf("'%s' at (%.1f, %.1f) font=%s size=%.1f\n",
              intToUtf8(ch$character), ch$bbox$x, ch$bbox$y,
              ch$font_name, ch$font_size))
}

Julia

doc   = open_document("paper.pdf")
chars = extract_chars(doc, 0)
for ch in chars[1:min(20, end)]
    println("'$(Char(ch.character))' at ($(ch.bbox.x), $(ch.bbox.y)) ",
            "font=$(ch.font_name) size=$(ch.font_size)")
end

Zig

var doc = try pdf_oxide.Document.open("paper.pdf");
const chars = try doc.extractChars(a, 0);
defer pdf_oxide.Document.freeChars(a, chars);
for (chars[0..@min(20, chars.len)]) |ch| {
    std.debug.print("U+{X:0>4} at ({d:.1}, {d:.1}) font={s} size={d:.1}\n",
        .{ ch.character, ch.bbox.x, ch.bbox.y, ch.fontName, ch.fontSize });
}

Objective-C

POXDocument *doc = [POXDocument openPath:@"paper.pdf" error:&err];
NSArray<POXChar*> *chars = [doc extractChars:0 error:&err];
for (POXChar *ch in [chars subarrayWithRange:NSMakeRange(0, MIN(20, chars.count))]) {
    NSLog(@"U+%04X at (%.1f, %.1f) font=%@ size=%.1f",
        ch.character, ch.bbox.x, ch.bbox.y, ch.fontName, ch.fontSize);
}

Elixir

{:ok, doc}   = PdfOxide.open("paper.pdf")
{:ok, chars} = PdfOxide.extract_chars(doc, 0)
chars
|> Enum.take(20)
|> Enum.each(fn ch ->
  IO.puts("'#{<<ch.character::utf8>>}' at (#{ch.bbox.x}, #{ch.bbox.y}) " <>
          "font=#{ch.font_name} size=#{ch.font_size}")
end)

Каждый символ содержит:

Поле Тип Описание
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)
            # Process text...
    except PdfError as e:
        print(f"Skipped {pdf_path.name}: {e}")

При скорости 0,8 мс на страницу обработка 3 830 PDF занимает около 3,1 секунды. Для производственных конвейеров см. руководство по пакетной обработке с паттернами параллельной обработки через multiprocessing и async I/O.

Работа со сканированными PDF (OCR)

Если PDF содержит сканированные изображения вместо текста, extract_text() вернёт пустой или минимальный результат. Используйте встроенный OCR PDF Oxide:

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")
text = doc.extract_text(0)

if not text.strip():
    # Page is likely scanned — use 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));

Java

try (PdfDocument doc = PdfDocument.open("protected.pdf", "secret")) {
    System.out.println(doc.extractText(0));
}

Kotlin

PdfDocument.open("protected.pdf", "secret").use { doc ->
    println(doc.extractText(0))
}

Scala

Using.resource(PdfDocument.open("protected.pdf", "secret")) { doc =>
  println(doc.extractText(0))
}

Clojure

(with-open [doc (pdf/open "protected.pdf" "secret")]
  (println (pdf/extract-text doc 0)))

Ruby

PdfOxide::PdfDocument.open('protected.pdf', password: 'secret') do |doc|
  puts doc.extract_text(0)
end

C++

auto doc = pdf_oxide::Document::open_with_password("protected.pdf", "secret");
std::cout << doc.extract_text(0) << '\n';

Swift

let doc = try Document.openWithPassword("protected.pdf", password: "secret")
print(try doc.extractText(0))

Dart

final doc = PdfDocument.openWithPassword('protected.pdf', 'secret');
print(doc.extractText(0));
doc.close();

R

doc <- pdf_open_with_password("protected.pdf", "secret")
cat(pdf_extract_text(doc, 0))

Julia

doc = open_with_password("protected.pdf", "secret")
println(extract_text(doc, 0))

Zig

var doc = try pdf_oxide.Document.openWithPassword("protected.pdf", "secret");
const text = try doc.extractText(a, 0);
std.debug.print("{s}\n", .{text});

Objective-C

POXDocument *doc = [POXDocument openWithPassword:@"protected.pdf"
                                       password:@"secret" error:&err];
NSLog(@"%@", [doc extractText:0 error:&err]);

Elixir

{:ok, doc}  = PdfOxide.open_with_password("protected.pdf", "secret")
{:ok, text} = PdfOxide.extract_text(doc, 0)
IO.puts(text)

Поддерживаются 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));

Java

try (PdfDocument doc = PdfDocument.open(Path.of("paper.pdf"))) {
    System.out.println(doc.toMarkdown(0));
}

Kotlin

PdfDocument.open(Path.of("paper.pdf")).use { doc ->
    println(doc.toMarkdown(0))
}

Scala

Using.resource(PdfDocument.open("paper.pdf")) { doc =>
  println(doc.toMarkdown(0))
}

Clojure

(with-open [doc (pdf/open "paper.pdf")]
  (println (pdf/to-markdown doc 0)))

PHP

$doc = PdfDocument::open('paper.pdf');
echo $doc->toMarkdown(0);
$doc->close();

Ruby

PdfOxide::PdfDocument.open('paper.pdf') do |doc|
  puts doc.to_markdown(0)
end

C++

auto doc = pdf_oxide::Document::open("paper.pdf");
std::cout << doc.to_markdown(0) << '\n';

Swift

let doc = try Document.open("paper.pdf")
print(try doc.toMarkdown(0))

Dart

final doc = PdfDocument.open('paper.pdf');
print(doc.toMarkdown(0));
doc.close();

R

doc <- pdf_open("paper.pdf")
cat(pdf_to_markdown(doc, 0))

Julia

doc = open_document("paper.pdf")
println(to_markdown(doc, 0))

Zig

var doc = try pdf_oxide.Document.open("paper.pdf");
const md = try doc.toMarkdown(a, 0);
std.debug.print("{s}\n", .{md});

Objective-C

POXDocument *doc = [POXDocument openPath:@"paper.pdf" error:&err];
NSLog(@"%@", [doc toMarkdown:0 error:&err]);

Elixir

{:ok, doc} = PdfOxide.open("paper.pdf")
{:ok, md}  = PdfOxide.to_markdown(doc, 0)
IO.puts(md)

Паттерны интеграции с 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})");

Java

import fyi.oxide.pdf.search.SearchMatch;

try (PdfDocument doc = PdfDocument.open(Path.of("manual.pdf"))) {
    for (SearchMatch m : doc.search("configuration")) {
        System.out.printf("Page %d: '%s' at (%.0f, %.0f)%n",
            m.pageIndex(), m.text(), m.bbox().x0(), m.bbox().y0());
    }
}

Kotlin

PdfDocument.open(Path.of("manual.pdf")).use { doc ->
    for (m in doc.search("configuration")) {
        println("Page ${m.pageIndex()}: '${m.text()}' at (${m.bbox().x0()}, ${m.bbox().y0()})")
    }
}

Scala

import fyi.oxide.pdf.searchSeq

Using.resource(PdfDocument.open("manual.pdf")) { doc =>
  for (m <- doc.searchSeq("configuration"))
    println(f"Page ${m.pageIndex}: '${m.text}' at (${m.bbox.x0}%.0f, ${m.bbox.y0}%.0f)")
}

Clojure

(with-open [doc (pdf/open "manual.pdf")]
  (doseq [m (pdf/search doc "configuration")]
    (let [b (.bbox m)]
      (println (format "Page %d: '%s' at (%.0f, %.0f)"
                       (.pageIndex m) (.text m) (.x0 b) (.y0 b))))))

Ruby

PdfOxide::PdfDocument.open('manual.pdf') do |doc|
  doc.search('configuration').each do |r|
    puts "Page #{r[:page]}: '#{r[:text]}' at (#{r[:bbox][:x].round}, #{r[:bbox][:y].round})"
  end
end

C++

auto doc = pdf_oxide::Document::open("manual.pdf");
for (const auto& r : doc.search_all("configuration", /*case_sensitive=*/false)) {
    std::printf("Page %d: '%s' at (%.0f, %.0f)\n",
        r.page, r.text.c_str(), r.bbox.x, r.bbox.y);
}

Swift

let doc = try Document.open("manual.pdf")
for r in try doc.searchAll("configuration", false) {
    print("Page \(r.page): '\(r.text)' at (\(r.bbox.x), \(r.bbox.y))")
}

Dart

final doc = PdfDocument.open('manual.pdf');
for (final r in doc.searchAll('configuration', false)) {
  print("Page ${r.page}: '${r.text}' at (${r.bbox.x}, ${r.bbox.y})");
}
doc.close();

R

doc <- pdf_open("manual.pdf")
for (r in pdf_search_all(doc, "configuration", case_sensitive = FALSE)) {
  cat(sprintf("Page %d: '%s' at (%.0f, %.0f)\n",
              r$page, r$text, r$bbox$x, r$bbox$y))
}

Julia

doc = open_document("manual.pdf")
for r in search_all(doc, "configuration", false)
    println("Page $(r.page): '$(r.text)' at ($(r.bbox.x), $(r.bbox.y))")
end

Zig

var doc = try pdf_oxide.Document.open("manual.pdf");
const hits = try doc.searchAll(a, "configuration", false);
defer pdf_oxide.Document.freeSearchResults(a, hits);
for (hits) |r| {
    std.debug.print("Page {d}: '{s}' at ({d:.0}, {d:.0})\n",
        .{ r.page, r.text, r.bbox.x, r.bbox.y });
}

Objective-C

POXDocument *doc = [POXDocument openPath:@"manual.pdf" error:&err];
for (POXSearchResult *r in [doc searchAll:@"configuration" caseSensitive:NO error:&err]) {
    NSLog(@"Page %ld: '%@' at (%.0f, %.0f)",
        (long)r.page, r.text, r.bbox.x, r.bbox.y);
}

Elixir

{:ok, doc}  = PdfOxide.open("manual.pdf")
{:ok, hits} = PdfOxide.search_all(doc, "configuration", false)
Enum.each(hits, fn r ->
  IO.puts("Page #{r.page}: '#{r.text}' at (#{r.bbox.x}, #{r.bbox.y})")
end)

Сравнение с другими Python-библиотеками для PDF

Существует несколько Python-библиотек для извлечения текста из PDF. Вот как они сравниваются:

  • 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 с детальным анализом макета. Очень медленный и не поддерживается. Не справляется с PDF с шифрованием AES-256. В основном вытеснен pdfplumber.
  • PDF Oxide — На базе Rust с Python-привязками через PyO3. Самый быстрый вариант (0,8 мс на страницу), 100% успешности, поддерживает все методы шифрования, включает встроенный OCR. Лицензия MIT, без системных зависимостей.

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

Производительность: насколько быстр PDF Oxide?

Тестирование на 3 830 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.

Искажённые или неправильные символы

Обычно это означает, что шрифт использует нестандартный вектор кодировки или отсутствует ToUnicode CMap. PDF Oxide обрабатывает большинство нестандартных кодировок, но некоторые намеренно запутанные PDF (контент с защитой DRM) могут давать неправильный вывод.

Отсутствующие пробелы или слитные слова

Текстовые операторы PDF размещают символы по одному. Вывод пробелов зависит от промежутка между позициями символов относительно ширины пробела в шрифте. Если слова сливаются, попробуйте extract_chars() и реализуйте собственную логику расстановки пробелов на основе позиций символов.

Вывод отличается от других библиотек

Разные библиотеки используют разные эвристики для вывода пробелов, переносов строк и порядка чтения. PDF Oxide достигает 99,5% совпадения текста с PyMuPDF на 3 830 PDF. Разница в 0,5% связана с нормализацией пробелов и обработкой лигатур.

Реальные сценарии использования

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

RAG-конвейеры (retrieval-augmented generation) — Извлекайте и разбивайте на чанки текст PDF для создания эмбеддингов с помощью OpenAI, Cohere или open-source моделей. Используйте extract_spans() для сохранения структуры заголовков, чтобы чанки соответствовали разделам документа. Для вывода, оптимизированного под LLM, см. руководство по конвертации PDF в Markdown.

Комплаенс и аудит — Сканируйте контракты, счета-фактуры и регуляторные документы на предмет конкретных положений или ключевых слов. Используйте doc.search() для поиска терминов по всем страницам с точными позициями или извлекайте полный текст для NLP-обнаружения положений.

Извлечение данных — Получайте структурированные данные из счетов-фактур, чеков, банковских выписок и форм. Комбинируйте extract_chars() для позиционных данных с предметно-специфическими правилами для поиска таких полей, как «Итоговая сумма» или «Дата счёта», и извлечения соседних значений.

Академические исследования — Обрабатывайте тысячи научных статей для обзора литературы, извлечения цитат или мета-анализа. PDF Oxide поддерживает весь спектр программ для создания PDF (LaTeX, Word, InDesign, Quark) и кодировок шрифтов, встречающихся в академических публикациях.

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