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)  # 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); // 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) // 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); // 0 페이지
Console.WriteLine(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 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 유니코드 문자
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}")

페이지당 0.8ms이니까, 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 의존성 없음. 설치는 쉽지만 페이지당 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까지 올라갑니다. API 범위는 PyMuPDF보다 좁습니다.
  • 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%

PDF 10,000개짜리 파이프라인이라면:

  • 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, 혹은 벡터 DB로 넣어 전문 검색을 만듭니다. PDF Oxide의 속도 덕분에 수천 개 문서를 필요할 때마다 재인덱싱하는 것도 현실적입니다.

RAG 파이프라인(검색 증강 생성) — PDF 텍스트를 추출해 청크로 나눈 뒤, OpenAI, Cohere, 오픈소스 모델로 임베딩을 만듭니다. extract_spans()로 제목 구조를 유지하면 청크가 문서 섹션과 잘 맞춰지죠. LLM에 최적화된 출력은 PDF to Markdown 가이드를 참고하세요.

컴플라이언스와 감사 — 계약서, 인보이스, 규제 제출 자료를 특정 조항이나 키워드로 스캔합니다. doc.search()로 모든 페이지에서 정확한 위치와 함께 용어를 찾아내거나, 전체 텍스트를 뽑아 NLP 기반 조항 검출에 활용할 수 있어요.

데이터 추출 — 인보이스, 영수증, 은행 명세서, 양식에서 구조화된 데이터를 꺼냅니다. extract_chars()의 위치 정보에 도메인 규칙을 결합하면 "총액"이나 “발행일” 같은 필드를 찾아 옆의 값을 함께 추출할 수 있어요.

학술 연구 — 문헌 리뷰, 인용 추출, 메타 분석을 위해 수천 편의 논문을 처리합니다. PDF Oxide는 학술 출판에서 흔한 LaTeX, Word, InDesign, Quark 등 다양한 PDF 생성 도구와 그들이 쓰는 폰트 인코딩을 모두 처리합니다.

관련 페이지