Skip to content

Python으로 PDF에서 텍스트 추출하기

PDF 텍스트 추출은 문서 처리 파이프라인에서 가장 흔한 작업 중 하나입니다. 검색 인덱스 구축, RAG 시스템 데이터 공급부터 데이터 마이닝, 컴플라이언스 워크플로까지 다양한 분야에서 활용됩니다. 이 가이드에서는 PDF Oxide를 사용해 Python, JavaScript, Rust로 PDF에서 텍스트를 추출하는 방법을 전반적으로 다룹니다. 일반 텍스트 추출, 문자 단위 위치 정보, 스타일이 적용된 스팬, 스캔 문서의 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.8ms로 텍스트를 추출합니다. 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단 레이아웃, 머리글, 바닥글, 표, 사이드바를 공간적으로 분석해 선형 텍스트 흐름을 만들어야 합니다.
  • 인코딩 엣지 케이스 — 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.8ms로 3,830개 PDF를 처리하는 데 약 3.1초가 걸립니다. 프로덕션 파이프라인의 multiprocessing 및 async 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():
    # Page is likely scanned — use 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));

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)

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));

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 라이브러리와 비교

PDF 텍스트 추출을 위한 Python 라이브러리가 여러 가지 있습니다. 각 라이브러리를 비교하면 다음과 같습니다:

  • pypdf — 순수 Python, C 의존성 없음. 설치가 쉽지만 느리고(페이지당 12ms), 폰트 및 인코딩 지원이 제한적이어서 1.6%의 PDF에서 실패합니다. 문자 위치 데이터 없음. 속도가 중요하지 않은 단순한 PDF에 적합합니다.
  • pdfplumber — pdfminer 기반으로 상세한 문자 및 표 추출 기능을 제공합니다. 매우 느리고(페이지당 23ms) 암호화 PDF를 열 수 없습니다. 셀 수준 데이터가 필요하고 성능이 중요하지 않은 표 추출에 적합합니다.
  • PyMuPDF (fitz) — MuPDF C 라이브러리의 Python 바인딩. 빠르고(페이지당 4.6ms) 신뢰성이 높습니다(99.3% 성공률). C 라이브러리 설치가 필요하며 AGPL 라이선스입니다. 라이선스가 맞다면 안정적인 선택입니다.
  • pypdfium2 — Google PDFium 엔진의 Python 바인딩. 빠르지만(페이지당 4.1ms) 복잡한 문서의 p99 지연 시간이 높습니다(42ms). PyMuPDF에 비해 API 범위가 제한적입니다.
  • pdfminer.six — 상세한 레이아웃 분석을 제공하는 순수 Python. 매우 느리고 유지보수가 중단되었습니다. AES-256 암호화 PDF에서 실패합니다. 대부분 pdfplumber로 대체되었습니다.
  • PDF Oxide — PyO3를 통한 Python 바인딩을 가진 Rust 기반 라이브러리. 가장 빠른 옵션(페이지당 0.8ms), 100% 성공률, 모든 암호화 방식 지원, 내장 OCR 포함. 시스템 의존성 없는 MIT 라이선스.

PDF Oxide는 기존 라이브러리의 격차를 해소하기 위해 개발되었습니다: 순수 Python 파서의 속도 제한, MuPDF의 라이선스 제약, 그리고 비정상적인 폰트, 손상된 상호 참조 테이블, 비표준 인코딩이 있는 실제 PDF에서 라이브러리가 실패하는 신뢰성 문제를 해결합니다.

성능: PDF Oxide는 얼마나 빠른가?

세 개의 독립적인 공개 테스트 스위트에서 가져온 3,830개 PDF에 대한 벤치마크:

라이브러리 평균 p99 성공률
PDF Oxide 0.8ms 9ms 100%
PyMuPDF 4.6ms 28ms 99.3%
pypdfium2 4.1ms 42ms 99.2%
pypdf 12.1ms 97ms 98.4%
pdfplumber 23.2ms 189ms 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 텍스트 연산자는 문자를 개별적으로 배치합니다. 공백 추론은 폰트의 공백 너비에 대한 문자 위치 간격에 따라 달라집니다. 단어가 붙어 보이면 extract_chars()를 사용하고 문자 위치를 기반으로 커스텀 간격 로직을 적용해보세요.

다른 라이브러리와 다른 출력

라이브러리마다 공백 추론, 줄 바꿈, 읽기 순서에 다른 휴리스틱을 사용합니다. PDF Oxide는 3,830개 PDF에서 PyMuPDF와 99.5%의 텍스트 일치를 달성합니다. 0.5%의 차이는 공백 정규화 및 합자 처리에서 발생합니다.

실제 활용 사례

검색 인덱싱 — 문서 저장소의 모든 PDF의 모든 페이지에서 텍스트를 추출한 다음 Elasticsearch, Typesense 또는 벡터 데이터베이스에 텍스트를 입력해 전문 검색을 구축합니다. PDF Oxide의 속도 덕분에 수천 개의 문서를 온디맨드로 재인덱싱하는 것이 실용적입니다.

RAG 파이프라인 (검색 증강 생성) — OpenAI, Cohere 또는 오픈소스 모델로 임베딩하기 위해 PDF 텍스트를 추출하고 청크로 분할합니다. extract_spans()를 사용해 제목 구조를 보존하면 청크가 문서 섹션과 일치하도록 할 수 있습니다. LLM에 최적화된 출력은 PDF를 Markdown으로 변환하는 가이드를 참고하세요.

컴플라이언스 및 감사 — 계약서, 청구서, 규제 문서에서 특정 조항이나 키워드를 스캔합니다. doc.search()를 사용해 모든 페이지에서 정확한 위치와 함께 용어를 찾거나, NLP 기반 조항 감지를 위해 전체 텍스트를 추출합니다.

데이터 추출 — 청구서, 영수증, 은행 명세서, 양식에서 구조화된 데이터를 추출합니다. 위치 데이터를 위한 extract_chars()와 도메인별 규칙을 결합해 “총 금액” 또는 “청구 날짜” 같은 필드를 찾고 인접한 값을 추출합니다.

학술 연구 — 문헌 리뷰, 인용 추출 또는 메타 분석을 위해 수천 개의 연구 논문을 처리합니다. PDF Oxide는 학술 출판물에서 발견되는 전체 범위의 PDF 생성 도구(LaTeX, Word, InDesign, Quark)와 폰트 인코딩을 처리합니다.

관련 페이지