pdfmux/blog
pdf-extraction

Extracting structured data from SEC 10-K filings in Python

TL;DRHow to pull financial tables and narrative sections out of SEC 10-K PDFs with pdfmux — CPU-only, table-aware extraction, and where to use EDGAR's XBRL data instead.

Direct answer: Don’t use PDF extraction to re-derive numbers the SEC already publishes as structured data — EDGAR’s XBRL feed covers the income statement, balance sheet, and cash flow statement directly, and it’s more reliable than any OCR pipeline for those fields. Use pdfmux on the 10-K PDF for the parts XBRL doesn’t cover: MD&A, risk factors, legal proceedings, and footnotes. process(path, quality="standard", format="markdown") handles the narrative sections; process(path, quality="high", format="csv") pulls any tables embedded in those sections (unit breakdowns, segment data) at 91%+ table structure accuracy (TEDS). CPU-only, no per-page API cost on the digital-native filings that make up the vast majority of recent EDGAR submissions.


What’s actually hard about a 10-K

A 10-K isn’t one document — it’s 80 to 300+ pages covering business description, risk factors, legal proceedings, management’s discussion and analysis (MD&A), and full financial statements with footnotes, each written and formatted by a different team inside the company. The financial statements are dense multi-column tables. The risk factors and MD&A are long-form prose, sometimes with embedded tables of their own (segment revenue, geographic breakdowns). Legal proceedings sections vary wildly in length and structure depending on how much litigation the company is managing.

Most recent 10-Ks filed to EDGAR are digital-native PDFs or HTML renders — companies generate them from Word or XBRL-tagging software, not from a scanner — so the extraction problem usually isn’t bad-OCR-on-a-fax. It’s structural: hundreds of pages with mixed tables and prose, where a extractor tuned for “give me the words” misses which numbers belong to which line item, and one tuned for “give me every table” flattens the risk-factor prose into noise.

The second thing that trips people up is duplicated effort: the SEC already requires companies to tag their financial statements in XBRL (eXtensible Business Reporting Language) — a machine-readable, structured format covering the core financial statements. If you’re building a pipeline to pull revenue, net income, or total assets out of a 10-K, pulling those numbers from the PDF with OCR is strictly worse than pulling the same numbers from EDGAR’s XBRL API, which is exact by construction. PDF extraction earns its keep on the sections XBRL doesn’t tag — MD&A, risk factors, legal proceedings, and footnote prose — which is most of what a RAG pipeline over 10-Ks actually needs, since those sections are where the qualitative signal lives.

Setup

pip install pdfmux

No OCR dependency needed for the common case — a digital-native 10-K PDF routes through pdfmux’s fast backend (PyMuPDF-based) with no GPU or API key required. Install pdfmux[ocr] only if you’re processing filings old enough to be scanned (pre-mid-2000s EDGAR submissions are the usual case).

Extracting the narrative sections

For MD&A, risk factors, and legal proceedings, Markdown output preserves heading structure, which matters for downstream chunking:

from pdfmux import process

result = process("aapl-10k-2026.pdf", quality="standard", format="markdown")

print(f"confidence: {result.confidence:.0%}")
print(f"pages: {result.page_count}")
print(result.text[:2000])

On a digital-native filing, expect confidence in the high 90s on nearly every page — these PDFs were generated from structured source documents, not scanned, so there’s little of the garbled-OCR risk that shows up in invoice extraction or other scanned-document workflows. The self-healing routing still matters at the margins — cover pages, exhibit indexes, and the occasional scanned signature page.

Pulling the financial tables

The balance sheet, income statement, and cash flow statement are dense tables, and quality="high" routes table-heavy pages through pdfmux’s table-aware backends (Docling scores 97.9% TEDS — tree edit distance similarity — on table structure in pdfmux’s benchmark suite):

result = process("aapl-10k-2026.pdf", quality="high", format="csv")

for table in result.tables:
    print(f"page {table.page}: {table.rows}×{table.cols}")
    table.to_csv(f"table_p{table.page}.csv")

This is genuinely useful for the footnote tables and segment breakdowns that XBRL doesn’t structure the same way the primary statements are — a revenue-by-geography table buried in a footnote, for instance. For the headline numbers (total revenue, net income, EPS), cross-check against EDGAR’s XBRL API before trusting a PDF-extracted figure in anything that matters.

Chunking for RAG over a 10-K corpus

A single 10-K can run 100,000+ words. If the goal is a RAG pipeline that answers questions across a company’s filing history, chunk by section rather than by fixed token count — “what did the risk factors say about supply chain in fiscal 2024 vs 2026” is a section-scoped question, and heading-aware chunks keep each chunk’s context self-contained:

result = process(
    "aapl-10k-2026.pdf",
    quality="standard",
    format="llm",   # section-aware chunked JSON with token estimates
    chunk=True,
)

for chunk in result.chunks:
    print(f"{chunk.heading}: ~{chunk.token_estimate} tokens")

See PDF chunking strategies for RAG for the heading-based approach this builds on, and PDF to pgvector for loading the resulting chunks into a vector store you can query across a whole filing history, not just one document.

Cost at scale

A mid-size research team tracking 500 companies’ annual 10-Ks is processing roughly 500 PDFs a year, each 100-300 pages. On digital-native filings, pdfmux’s fast and standard tiers are CPU-only and free — there’s no per-page API cost the way there is with cloud document AI services:

ApproachCost per filing (≈150 pages)GPU requiredHandles scanned legacy filings
pdfmux (standard, digital-native)$0NoNo (needs [ocr] extra)
pdfmux (high, table-heavy)$0–$1.50 (if LLM fallback triggers)NoYes (with [ocr])
AWS Textract~$22.50 (Tables API, $0.15/page)NoYes
GPT-4o vision, every page~$1.50NoYes

The gap widens fast at 500+ filings a year — Textract’s per-page table pricing doesn’t change with volume, while pdfmux’s digital-native path stays free regardless of how many filings you process. See the pdfmux vs AWS Textract comparison for the full breakdown including scanned-document accuracy.

FAQ

Are 10-K PDFs usually scanned or digital-native? Overwhelmingly digital-native for anything filed in the last 15+ years — EDGAR’s filing system requires electronic submission, and filers generate the PDF from Word or XBRL-tagging software rather than scanning paper. Expect high extraction confidence by default; treat [ocr] as the exception path, not the main one.

Should I extract the financial statement numbers from the PDF at all? Generally no. EDGAR’s XBRL feed gives you the primary financial statements as structured, machine-readable data with no extraction risk. Reserve PDF extraction for MD&A, risk factors, legal proceedings, and footnote tables — the sections XBRL doesn’t cover and that a RAG pipeline over qualitative disclosure actually needs.

How do I know a table extraction is trustworthy before I use it downstream? Check result.confidence and spot-check against the filing’s HTML render (EDGAR serves both PDF and HTML for most filings) before trusting a footnote table in anything automated. See the confidence scoring deep-dive for exactly what the per-page score is measuring and when it’s worth a second look.

Does this work for other SEC forms — 10-Q, 8-K, S-1? Yes, structurally — they’re the same mix of narrative sections and financial tables, just shorter (10-Q) or more event-specific (8-K). The same quality="standard" for narrative / quality="high" for tables split applies.