Видобути текст із 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
- Бенчмарки продуктивності — методологія та результати тестування