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

Пов’язані сторінки