Извлечение текста из 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) и кодировок шрифтов, встречающихся в академических публикациях.
Связанные страницы
- API извлечения текста — полный справочник API
- PDF в Markdown — структурированное преобразование
- Пакетная обработка — паттерны параллельной обработки
- OCR сканированных PDF — настройка и использование OCR
- Бенчмарки производительности — методология и результаты тестирования