Skip to content

Python で PDF テキスト抽出

PDF からのテキスト抽出は、検索インデックスの構築、RAG システムへの取り込み、データマイニング、コンプライアンス対応など、ドキュメント処理パイプラインで最もよく出てくる作業の 1 つです。本ガイドでは、PDF Oxide を使って Python・JavaScript・Rust から PDF のテキストを抽出するために必要なことをひととおり解説します。プレーンテキストの抽出、文字単位の座標情報、スタイル付きスパン、スキャン PDF 向けの OCR、暗号化 PDF の扱い、バッチパイプライン向けのパフォーマンスチューニングまで含みます。

3 行でどんな 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);

PDF Oxide は 1 ページ平均 0.8 ms でテキストを抽出します。PyMuPDF の 5 倍、pypdf の 15 倍の速さで、3,830 件のテスト PDF に対して成功率 100 % を記録しています。

PDF テキスト抽出が難しい理由

PDF は視覚表現のフォーマットであって、テキストのフォーマットではありません。HTML や Markdown とは違い、PDF ファイルには「段落」や「文」といった情報は保存されておらず、ページ上の特定の座標に配置された個々の文字が記録されているだけです。そこから読めるテキストを取り出すには、次のような処理が必要になります。

  • フォントのデコード — PDF のフォントは、エンコーディングテーブル(WinAnsi、MacRoman、Unicode CMap、Type 1、TrueType、CIDFont)を使って文字コードをグリフに対応付けます。同じコード 0x41 でも、あるフォントでは “A” を意味し、別のフォントでは “α” を意味することがあります。
  • テキストストリームの解析TjTJ'" などの演算子が文字をページ上に配置します。TJ 配列内のカーニング調整は文字をポイント単位の小数で移動させ、欠落したスペースは文字位置の間隔から推定しなければなりません。
  • レイアウトの復元 — ページ上の文字には明示的な読み順がありません。2 段組、ヘッダー、フッター、表、サイドバーなどを空間的に解析し、1 本のテキストフローに整えてやる必要があります。
  • エンコーディングの例外 — 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));

文字位置付きでテキストを抽出する

各文字の正確な座標、フォント名、サイズをまとめて取得できます。

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
chars = doc.extract_chars(0)

for ch in chars[:20]:
    print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) "
          f"font={ch.font_name} size={ch.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);
for (const ch of chars.slice(0, 20)) {
    console.log(`'${ch.char}' at (${ch.x.toFixed(1)}, ${ch.y.toFixed(1)}) font=${ch.fontName} size=${ch.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let chars = doc.extract_chars(0)?;
for ch in chars.iter().take(20) {
    println!("'{}' at ({:.1}, {:.1}) font={} size={:.1}",
        ch.char, ch.x, ch.y, ch.font_name, ch.font_size);
}

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

chars, _ := doc.ExtractChars(0)
for _, ch := range chars[:20] {
    fmt.Printf("%q at (%.1f, %.1f) font=%s size=%.1f\n",
        ch.Char, ch.X, ch.Y, ch.FontName, ch.FontSize)
}

C#

using var doc = PdfDocument.Open("paper.pdf");
var chars = doc.ExtractChars(0);
foreach (var ch in chars.Take(20))
    Console.WriteLine($"'{ch.Char}' at ({ch.X:F1}, {ch.Y:F1}) font={ch.FontName} size={ch.FontSize:F1}");

各文字に含まれる情報は以下のとおりです。

フィールド 説明
char str Unicode 文字
x, y float ポイント単位の位置
font_size float ポイント単位のフォントサイズ
font_name str PostScript フォント名
bbox tuple バウンディングボックス (x0, y0, x1, y1)

文字単位の抽出は、表の復元、フォントサイズによる見出し検出、テキスト領域のバウンディングボックス生成などで役立ちます。たとえば y 座標で文字を行にまとめたり、x 位置の隙間から段組の境界を判定したりできます。

スタイル付きテキストスパンを抽出する

同じフォントとサイズが続く文字をまとめてスパンにします。

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
spans = doc.extract_spans(0)

for span in spans:
    print(f"'{span.text}' font={span.font_name} size={span.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const spans = doc.extractSpans(0);
for (const span of spans) {
    console.log(`'${span.text}' font=${span.fontName} size=${span.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let spans = doc.extract_spans(0)?;
for span in &spans {
    println!("'{}' font={} size={:.1}", span.text, span.font_name, span.font_size);
}

見出しや太字の検出、構造化された出力の組み立てに便利です。

バッチ処理

数百〜数千の PDF をまとめて処理します。

from pdf_oxide import PdfDocument, PdfError
from pathlib import Path

pdf_dir = Path("documents/")
for pdf_path in pdf_dir.glob("*.pdf"):
    try:
        doc = PdfDocument(str(pdf_path))
        for i in range(doc.page_count()):
            text = doc.extract_text(i)
            # テキストを処理する...
    except PdfError as e:
        print(f"Skipped {pdf_path.name}: {e}")

1 ページ 0.8 ms なので、3,830 件の PDF を処理しても 3.1 秒程度です。本番パイプラインでの並列化パターンについては、バッチ処理ガイド で multiprocessing と非同期 I/O を使った構成を紹介しています。

スキャン PDF の扱い(OCR)

PDF がテキストではなくスキャン画像で構成されている場合、extract_text() は空文字や最小限の出力を返します。その際は PDF Oxide に組み込まれた OCR を使います。

from pdf_oxide import PdfDocument

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

if not text.strip():
    # ページはスキャンと思われる。OCR を使う
    text = doc.extract_text_ocr(0)
    print(text)

PDF Oxide は ONNX Runtime 経由で PaddleOCR を呼び出すので、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));

AES-256、AES-128、RC4 で暗号化された PDF に対応しています。暗号化ファイルをそもそも開けない pdfplumber や AES-256 で失敗する pdfminer と違い、PDF Oxide は PDF の標準的な暗号化方式をすべて透過的に処理します。

Markdown として出力する

見出しや書式付きの構造化された出力が欲しい場合は次のようにします。

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

md, _ := doc.ToMarkdown(0)
fmt.Println(md)

C#

using var doc = PdfDocument.Open("paper.pdf");
Console.WriteLine(doc.ToMarkdown(0));

RAG や LLM と組み合わせるパターンは PDF から Markdown への変換ガイド を参照してください。

PDF 内を検索する

位置情報付きで全ページからテキストを検索します。

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("manual.pdf")
results = doc.search("configuration")
for r in results:
    print(f"Page {r.page}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})")

WASM

const doc = new WasmPdfDocument(bytes);
const results = doc.search("configuration", false);
for (const r of results) {
    console.log(`Page ${r.page}: '${r.text}' at (${r.x.toFixed(0)}, ${r.y.toFixed(0)})`);
}
doc.free();

Rust

let mut pdf = Pdf::open("manual.pdf")?;
let results = pdf.search("configuration")?;
for r in &results {
    println!("Page {}: '{}' at ({:.0}, {:.0})", r.page, r.text, r.bbox.x, r.bbox.y);
}

Go

doc, _ := pdfoxide.Open("manual.pdf")
defer doc.Close()

results, _ := doc.SearchAll("configuration", false)
for _, r := range results {
    fmt.Printf("Page %d: %q at (%.0f, %.0f)\n", r.PageIndex, r.Text, r.X, r.Y)
}

C#

using var doc = PdfDocument.Open("manual.pdf");
foreach (var r in doc.SearchAll("configuration", caseSensitive: false))
    Console.WriteLine($"Page {r.PageIndex}: '{r.Text}' at ({r.X:F0}, {r.Y:F0})");

Python の他の PDF ライブラリとの比較

Python で PDF テキストを抽出できるライブラリはいくつかありますが、比較すると次のようになります。

  • pypdf — 純 Python、C 依存なし。導入は簡単ですが遅く(1 ページ 12 ms)、フォントとエンコーディングのサポートが限定的で 1.6 % の PDF で失敗します。文字位置情報は取得できません。速度が問われないシンプルな PDF 向け。
  • pdfplumber — pdfminer をベースに、細かい文字情報と表の抽出を提供します。非常に遅く(1 ページ 23 ms)、暗号化 PDF を開けません。性能よりもセル単位の表情報が欲しいときに向きます。
  • PyMuPDF(fitz) — C ライブラリ MuPDF の Python バインディング。高速(1 ページ 4.6 ms)で安定(成功率 99.3 %)ですが、C ライブラリの導入が必要で AGPL ライセンスです。ライセンスが許容できるなら堅実な選択肢です。
  • pypdfium2 — Google の PDFium エンジンへの Python バインディング。高速(1 ページ 4.1 ms)ですが、複雑なドキュメントでは p99 レイテンシが 42 ms と高めです。PyMuPDF と比べ API の幅は狭いです。
  • pdfminer.six — 純 Python で詳細なレイアウト解析ができますが、非常に遅く、メンテナンスもほぼ止まっています。AES-256 で失敗し、大部分の用途は pdfplumber に置き換わりました。
  • PDF Oxide — PyO3 を介した Python バインディング付きの Rust コア。最速(1 ページ 0.8 ms)、成功率 100 %、全暗号化方式と組み込み OCR に対応。MIT ライセンスでシステム依存はありません。

PDF Oxide は、既存ライブラリの弱点をまとめて解消するために作られました。純 Python パーサーの速度的な限界、MuPDF のライセンス上の制約、そして特殊なフォント・破損したクロスリファレンステーブル・非標準のエンコーディングを持つ実際の PDF で他のライブラリが落ちる信頼性の問題です。

パフォーマンス: PDF Oxide はどのくらい速いか

独立した 3 つの公開テストスイートから集めた 3,830 件の PDF で計測しました。

ライブラリ 平均 p99 成功率
PDF Oxide 0.8 ms 9 ms 100 %
PyMuPDF 4.6 ms 28 ms 99.3 %
pypdfium2 4.1 ms 42 ms 99.2 %
pypdf 12.1 ms 97 ms 98.4 %
pdfplumber 23.2 ms 189 ms 98.8 %

10,000 件の PDF を処理するパイプラインでは次のようになります。

  • PDF Oxide: 8 秒
  • PyMuPDF: 46 秒
  • pypdf: 2 分
  • pdfplumber: 3.9 分

計測方法と再現手順は 詳細ベンチマーク を参照してください。

よくある問題とトラブルシューティング

テキスト出力が空になる

extract_text() が空文字を返す場合、ページはテキストではなくスキャン画像で構成されている可能性が高いです。代わりに extract_text_ocr() を使ってください。セットアップ手順は スキャン PDF の OCR にまとめています。

文字化けや誤った文字が出る

これは多くの場合、エンコーディングベクトルが非標準のフォントや ToUnicode CMap が欠けているフォントが原因です。PDF Oxide はほとんどのエッジケースを処理しますが、意図的に難読化された PDF(DRM 保護コンテンツ)では誤った出力になることがあります。

スペースが抜ける/単語がくっつく

PDF のテキスト演算子は文字を 1 つずつ配置します。スペースの推定はフォントのスペース幅に対する文字間隔に依存するため、単語がくっついて見えるときは extract_chars() を使い、文字位置をもとに独自の区切りロジックを適用してください。

他ライブラリと出力が違う

ライブラリごとにスペース推定、改行、読み順のヒューリスティックは異なります。PDF Oxide は 3,830 件の PDF で PyMuPDF と 99.5 % のテキスト一致を達成しています。残り 0.5 % の差は主に空白の正規化と合字の扱いに関するものです。

実際の活用例

検索インデックス — 文書リポジトリにあるすべての PDF の全ページからテキストを抽出し、Elasticsearch、Typesense、あるいはベクトル DB に流し込んで全文検索を作ります。PDF Oxide の速度なら、数千件のドキュメントをオンデマンドで再インデックスするのも現実的です。

RAG パイプライン(検索拡張生成) — PDF のテキストを抽出してチャンク化し、OpenAI、Cohere、OSS のモデルで埋め込みにかけます。extract_spans() を使えば見出し構造を保てるので、チャンクをドキュメントのセクションに揃えやすくなります。LLM 向けに最適化された出力は PDF から Markdown への変換ガイド を参照してください。

コンプライアンスと監査 — 契約書、請求書、規制当局への提出書類を特定の条項やキーワードで走査します。doc.search() で全ページから正確な位置付きで用語を特定したり、全文を抽出して NLP で条項検出を回したりできます。

データ抽出 — 請求書、領収書、銀行明細、フォームから構造化データを取り出します。extract_chars() による位置情報と業務ルールを組み合わせて、「合計金額」や「請求日」といったフィールドを見つけ、隣接する値を取り出します。

学術研究 — 文献レビュー、引用抽出、メタアナリシスのために大量の論文を処理します。PDF Oxide は LaTeX、Word、InDesign、Quark など学術出版で使われる PDF プロデューサーと、そこで見られるフォントエンコーディングを一通りカバーしています。

関連ページ