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” を意味し、別のフォントでは “α” を意味することがあります。 - テキストストリームの解析 —
Tj・TJ・'・"などの演算子が文字をページ上に配置します。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 プロデューサーと、そこで見られるフォントエンコーディングを一通りカバーしています。
関連ページ
- テキスト抽出 API — 完全な API リファレンス
- PDF から Markdown — 構造化変換
- バッチ処理 — 並列処理のパターン
- スキャン PDF の OCR — OCR のセットアップと使い方
- パフォーマンスベンチマーク — ベンチマーク手法と結果