<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felipe Cardona</title>
    <description>The latest articles on DEV Community by Felipe Cardona (@felipe_anyformat).</description>
    <link>https://dev.to/felipe_anyformat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4114113%2Fc529dc1a-df38-484a-aef0-0d954581ce7e.png</url>
      <title>DEV Community: Felipe Cardona</title>
      <link>https://dev.to/felipe_anyformat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felipe_anyformat"/>
    <language>en</language>
    <item>
      <title>The 8 Best Google Document AI Alternatives in 2026</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:07:37 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/the-8-best-google-document-ai-alternatives-in-2026-5bf7</link>
      <guid>https://dev.to/felipe_anyformat/the-8-best-google-document-ai-alternatives-in-2026-5bf7</guid>
      <description>&lt;p&gt;The best Google Document AI alternatives in 2026 are anyformat, AWS Textract, Azure Document Intelligence, ABBYY, Nanonets, Reducto, LlamaParse, and Unstructured. Teams switch for four main reasons: European data sovereignty, on-premise deployment, the 15-page cap on online processing, and the operations layer (classification, routing, validation, human review, accuracy monitoring) that Google's extraction API leaves engineers to build and maintain.&lt;/p&gt;

&lt;p&gt;We compared each tool on five criteria: zero-shot custom field extraction, on-premise deployment, EU data sovereignty, pricing model, and workflow orchestration. Facts come from vendor documentation, published pricing, and a benchmark run on 1,000+ real documents. Last evaluated: October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Zero-shot custom fields&lt;/th&gt;
&lt;th&gt;On-premise&lt;/th&gt;
&lt;th&gt;EU sovereignty&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;anyformat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Custom schemas on complex documents, with accuracy you can measure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, incl. air-gapped&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;EU-native, ISO 27001&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Per page, per operator ($25/1,000 pages parsing)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Textract&lt;/td&gt;
&lt;td&gt;Forms and tables in AWS stacks&lt;/td&gt;
&lt;td&gt;No (Queries: English only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;US jurisdiction&lt;/td&gt;
&lt;td&gt;Per page ($65/1,000 for forms + tables)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Document Intelligence&lt;/td&gt;
&lt;td&gt;Microsoft-stack teams&lt;/td&gt;
&lt;td&gt;No (5+ labeled docs)&lt;/td&gt;
&lt;td&gt;Partial (containers)&lt;/td&gt;
&lt;td&gt;US jurisdiction&lt;/td&gt;
&lt;td&gt;Per page ($10/1,000 prebuilt, $30 custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABBYY&lt;/td&gt;
&lt;td&gt;Traditional enterprise IDP&lt;/td&gt;
&lt;td&gt;No (Skills + training)&lt;/td&gt;
&lt;td&gt;Yes (FlexiCapture)&lt;/td&gt;
&lt;td&gt;US-headquartered&lt;/td&gt;
&lt;td&gt;License + professional services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nanonets&lt;/td&gt;
&lt;td&gt;Mid-market no-code automation&lt;/td&gt;
&lt;td&gt;Partial (HITL annotation)&lt;/td&gt;
&lt;td&gt;Enterprise contract only&lt;/td&gt;
&lt;td&gt;US multi-tenant&lt;/td&gt;
&lt;td&gt;Per block run ($0.02–$0.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reducto&lt;/td&gt;
&lt;td&gt;Engineering teams wanting a parsing API&lt;/td&gt;
&lt;td&gt;Yes (API schemas)&lt;/td&gt;
&lt;td&gt;Yes (VPC, air-gapped)&lt;/td&gt;
&lt;td&gt;US company, EU endpoints&lt;/td&gt;
&lt;td&gt;Usage-based API ($10 parse, $20 extract per 1,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaParse&lt;/td&gt;
&lt;td&gt;Parsing documents into LLM-ready Markdown&lt;/td&gt;
&lt;td&gt;Partial (parsing-first)&lt;/td&gt;
&lt;td&gt;Enterprise self-hosted&lt;/td&gt;
&lt;td&gt;US cloud API&lt;/td&gt;
&lt;td&gt;Credit-based per page ($1.25–$56.25/1,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unstructured&lt;/td&gt;
&lt;td&gt;RAG preprocessing with connectors&lt;/td&gt;
&lt;td&gt;No (elements, not fields)&lt;/td&gt;
&lt;td&gt;Yes (self-hosted)&lt;/td&gt;
&lt;td&gt;US company&lt;/td&gt;
&lt;td&gt;Apache-2.0 library; platform &lt;a href="https://unstructured.io/pricing" rel="noopener noreferrer"&gt;$0.015/page&lt;/a&gt; after 10,000 free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. anyformat
&lt;/h2&gt;

&lt;p&gt;anyformat is an agentic document intelligence platform built in the EU, and the strongest fit when the reason you are leaving Google Document AI is sovereignty, deployment, or the operations and measurement layer around extraction. It extracts any user-defined schema zero-shot, with no labeled samples or retraining cycles, and links every extracted value to its exact position on the page through visual citations. It deploys on-premise, including air-gapped environments, is ISO 27001 certified, and offers zero data retention as an Enterprise option. In its published July 2026 benchmark on 1,000+ real documents, anyformat scored 78.1% on parsing quality versus 55.0% for Google Document AI, and held roughly 99% row recovery on 50-page tables.&lt;/p&gt;

&lt;p&gt;Two things separate it from the raw-API options in this list. Classification, routing and human review are configured in a visual Studio instead of being assembled from cloud functions, queues and a review UI your team writes and maintains. And every Extract and Classify workflow ships a Health tab for evaluation: build a dataset with verified ground truth (promoted from reviewed production files or uploaded as labelled data), tag sub-datasets to slice accuracy by provider, document type or difficulty, and run numbered, immutable evaluations that score any workflow version field by field against that ground truth, with result-versus-expected comparisons. The Health Overview puts the dataset benchmark next to live production accuracy, confidence and through rate. anyformat has also announced Optimizer, a workflow that tunes itself against its own dataset using your evals as the target; it is not shipped yet.&lt;/p&gt;

&lt;p&gt;Pricing is credit-based, charged per page and per operator (10 credits = €0.01): Parse 25 credits per page, Extract 35, Classify 10, Split 25, with agentic variants at 100 and 150. The $25 per 1,000 pages in the benchmark is the parsing rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Custom schemas on complex documents, with accuracy you can measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. AWS Textract
&lt;/h2&gt;

&lt;p&gt;AWS Textract is Amazon's OCR service, launched in 2019, and the natural pick for engineering teams already running on AWS. It extracts text, forms, tables, and signatures from PDFs and images, with table detection that ranks among the strongest of the cloud providers, and integrates directly with S3, Lambda, SNS, and SQS. It returns raw OCR output rather than schema-shaped data, so field mapping, validation, and orchestration are custom engineering work. AWS's published pricing is $65 per 1,000 pages for the combined forms and tables analysis, rising to $70 when queries are added. Textract is cloud-only on AWS and governed under US jurisdiction regardless of region selection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; AWS-native engineering teams extracting structured forms and tables who can build their own pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Azure Document Intelligence
&lt;/h2&gt;

&lt;p&gt;Azure Document Intelligence, formerly Form Recognizer (renamed in 2023), is Microsoft's document processing service and the path of least resistance for organizations deep in the Azure ecosystem. Its pretrained models cover invoices, receipts, IDs, and tax forms, and it handles multi-column tables and nested structures better than most cloud rivals; independent benchmarks have reported around 96% accuracy on printed text. Custom fields require labeling documents in Document Studio, five examples minimum per Microsoft's documentation and usually more, plus a training cycle, and schema changes mean relabeling. Container deployment is genuinely partial: the current v4.0 GA API ships containers for Read and Layout only, and custom extraction models have no container path; the full platform is cloud-only under US jurisdiction. Prebuilt models cost $10 per 1,000 pages (invoices, receipts, IDs and others), custom extraction $30 and Read $1.50 (&lt;a href="https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" rel="noopener noreferrer"&gt;Azure pricing page&lt;/a&gt;, read October 2026).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Microsoft-stack teams whose documents match a pretrained model.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ABBYY
&lt;/h2&gt;

&lt;p&gt;ABBYY is the longest-standing vendor in this list, founded in 1989 and now headquartered in Austin, Texas, with two products: Vantage (cloud IDP) and FlexiCapture (on-premise capture). Its 150+ pre-built extraction Skills and two decades of enterprise deployments translate into genuinely strong accuracy on supported document types, including handwriting. The trade-off is the delivery model: implementations typically run weeks to months with third-party integrators, professional services budgets range from $15K to over $200K, and contracts commonly require 3-year terms with annual prepayment. FlexiCapture remains one of the few mature, fully on-premise options in the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Large enterprises with stable document portfolios and the budget for a traditional IDP deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Nanonets
&lt;/h2&gt;

&lt;p&gt;Nanonets is a mid-market document automation platform, founded in 2017, whose block-based no-code workflow builder is its clearest differentiator: import, data-action, and export blocks with conditional logic that non-technical users can assemble. Extraction is template-free, though Nanonets' own documentation notes that non-standard documents may need human-in-the-loop annotation before reaching production accuracy. Pricing is per block run, between $0.02 and $0.30 depending on complexity, and Nanonets' own pricing page puts a typical invoice workflow at four to six block runs per document, so the per-run figure is not the per-document cost. Data sits on US multi-tenant infrastructure by default, with retention of roughly 30 days after termination; on-premise deployment is available only via enterprise contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mid-market teams automating standard document types without engineering involvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Reducto
&lt;/h2&gt;

&lt;p&gt;Reducto is a US document parsing API, founded around 2023, that has raised $108M in total, including a $75M Series B led by a16z in October 2025 (&lt;a href="https://reducto.ai/blog/reducto-series-b-funding" rel="noopener noreferrer"&gt;Reducto announcement&lt;/a&gt;). Pricing is usage-based: Parse r-1 is $10 per 1,000 pages flat and Extract is $20 per 1,000 pages (&lt;a href="https://reducto.ai/pricing" rel="noopener noreferrer"&gt;Reducto pricing&lt;/a&gt;, September 2026). Its agentic OCR combines multi-pass vision-language models with error correction, and it publishes an open table-extraction benchmark, RD-TableBench, which is more transparency than most vendors offer. Extraction is schema-driven and zero-shot through the API, but there is no UI: every schema change goes through a developer, and classification, routing, human review and accuracy monitoring are yours to build around the Parse, Split, Extract, and Edit endpoints. Deployment spans cloud, VPC, and air-gapped on-premise. It holds SOC 2 Type II and offers HIPAA processing with a BAA on higher tiers; ISO 27001 is not listed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Engineering teams that want a high-accuracy parsing primitive and will build the platform around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. LlamaParse
&lt;/h2&gt;

&lt;p&gt;LlamaParse is LlamaIndex's document parsing API, launched in February 2024, purpose-built to convert documents into LLM-ready Markdown for retrieval-augmented generation. Its speed is real: an independent Procycons benchmark measured about 6 seconds for both 1-page and 50-page documents in batch mode. The constraint is the output format. Markdown flattens merged cells, row spans, and nested table relationships, which limits downstream structured extraction, and while confidence scores are &lt;a href="https://developers.llamaindex.ai/llamaparse/extract/guides/extensions/#confidence-scores" rel="noopener noreferrer"&gt;documented on its upper tiers&lt;/a&gt;, the calibration is self-reported and has no published evaluation, and there is no review interface in the product. It holds SOC 2 Type 2; ISO 27001 is not listed, and the standard offering is a US cloud API with enterprise self-hosting via Kubernetes. Pricing is credit-based: parsing runs from $1.25 (Fast) to $56.25 (Agentic Plus) per 1,000 pages, plus 10 credits per page containing a form (&lt;a href="https://developers.llamaindex.ai/llamaparse/general/pricing/" rel="noopener noreferrer"&gt;LlamaParse pricing&lt;/a&gt;, September 2026).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Fast, cost-effective RAG ingestion inside the LlamaIndex ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Unstructured
&lt;/h2&gt;

&lt;p&gt;Unstructured is a partially open-source preprocessing platform that converts documents into element arrays (text blocks, tables, images) for LLM pipelines, with the widest connector ecosystem in the category: &lt;a href="https://unstructured.io/pricing" rel="noopener noreferrer"&gt;40+ connectors&lt;/a&gt; including Databricks, Elasticsearch, S3, and Google Drive. Its commercial platform holds SOC 2 Type II, ISO 27001, and HIPAA certifications, plus &lt;a href="https://unstructured.io/blog/unstructured-secures-fed-ramp-high-authorization" rel="noopener noreferrer"&gt;FedRAMP High authorization&lt;/a&gt; (announced December 2025) (the open-source library is not in scope), and self-hosted deployment gives full data control. What it deliberately does not do is field-level structured extraction: you get chunks for retrieval, not schema-defined values for an ERP or database. Teams that need both often pair it with an extraction platform rather than replacing one with the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; AI teams building RAG pipelines that need broad connector coverage rather than field extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data sovereignty.&lt;/strong&gt; If GDPR, DORA, or sector rules govern your data, distinguish between EU regions on a US-governed platform and EU-native governance. Google Document AI remains US-governed regardless of region selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom schemas.&lt;/strong&gt; Google's Custom Extractor now runs on a foundation model that extracts from a schema alone, with no labeled documents; few-shot accepts up to 5 examples and fine-tuning needs 50 training plus 50 test documents to reach production accuracy. If schema flexibility is your only complaint, Google may already solve it. anyformat and Reducto are the other zero-shot options; ABBYY, Azure and Nanonets still involve labeling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline around the API.&lt;/strong&gt; A raw OCR API returns text and blocks. Post-processing, schema validation, retry and routing logic, a review UI, accuracy monitoring and re-tuning after every layout or model change are yours to build and keep running. Price that engineering alongside the per-page rate, because it is usually the larger number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How you will measure accuracy.&lt;/strong&gt; Per-field confidence is a starting point; a dataset with verified ground truth and repeatable, versioned evaluations is what lets you change a workflow without guessing. Google offers evaluation in Workbench for custom processors, anyformat ships it as the Health tab on every Extract and Classify workflow, and the parsing APIs leave it to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing shape.&lt;/strong&gt; Per-page, per-operator and per-run models compound differently; model your real monthly volume, including reprocessing and multi-step workflows, before comparing list prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment.&lt;/strong&gt; Cloud-only rules out many regulated environments. On-premise options in this list: anyformat, ABBYY FlexiCapture, Reducto, and self-hosted Unstructured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope.&lt;/strong&gt; Decide whether you need a parsing API you build around (Reducto, LlamaParse) or an operations platform with workflows, review, and orchestration included (anyformat, Nanonets).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Google Document AI free?&lt;/strong&gt;&lt;br&gt;
No. Google Document AI is pay-per-use on Google Cloud. Enterprise Document OCR is $1.50 per 1,000 pages up to 5 million; Form Parser and Custom Extractor are $30 per 1,000 pages, falling to $20 above a million; Layout Parser is $10 per 1,000; and the pretrained invoice parser bills $0.10 per document of up to 10 pages (&lt;a href="https://cloud.google.com/document-ai/pricing" rel="noopener noreferrer"&gt;Google Cloud pricing&lt;/a&gt;, read October 2026). There is no free tier for production volumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best Google Document AI alternative for EU companies?&lt;/strong&gt;&lt;br&gt;
anyformat is the strongest fit for EU companies: it is EU-native, ISO 27001 certified, GDPR-compliant by architecture, and deploys on-premise including air-gapped environments with zero-retention processing. ABBYY FlexiCapture also keeps data on your own infrastructure, though with a traditional implementation model measured in weeks to months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I extract custom fields without training data?&lt;/strong&gt;&lt;br&gt;
Yes, and Google itself can do this now. Its Custom Extractor runs on a foundation model that extracts user-defined fields from a schema alone; few-shot prompting accepts up to 5 labeled examples, and fine-tuning requires at least 50 training and 50 test documents to reach production accuracy (Google Cloud documentation, 2026). anyformat and Reducto are also zero-shot from a schema definition. Azure Document Intelligence still requires at least 5 labeled documents plus a training cycle in Document Studio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Google Document AI alternatives support on-premise deployment?&lt;/strong&gt;&lt;br&gt;
anyformat (including air-gapped environments), ABBYY FlexiCapture, Reducto (VPC and air-gapped), and Unstructured (self-hosted) all run on your own infrastructure. Google Document AI is cloud-only on GCP, Nanonets offers on-premise only through enterprise contracts, and Azure's containers cover a subset of features.&lt;/p&gt;




&lt;p&gt;If Google Document AI is the specific tool you are replacing, the head-to-head comparison goes deeper: &lt;a href="https://anyformat.ai/vs/anyformat-vs-google-document-ai" rel="noopener noreferrer"&gt;anyformat vs Google Document AI&lt;/a&gt; covers zero-shot extraction, the 15-page online processing limit, European sovereignty, and workflow orchestration in detail.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/best-google-document-ai-alternatives" rel="noopener noreferrer"&gt;anyformat.ai/blog/best-google-document-ai-alternatives&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ocr</category>
      <category>ai</category>
      <category>api</category>
    </item>
    <item>
      <title>OCR for poor-quality scanned PDFs</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:14:49 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/ocr-for-poor-quality-scanned-pdfs-3i97</link>
      <guid>https://dev.to/felipe_anyformat/ocr-for-poor-quality-scanned-pdfs-3i97</guid>
      <description>&lt;p&gt;You need data out of a 200-page PDF because you want to feed a RAG pipeline, fill a database from contracts or filings, or ask questions that cross hundreds of pages.&lt;/p&gt;

&lt;p&gt;If you tried putting the whole file into an LLM, we get it: the output was not good. Rows were missing, a total did not match, the answer was confident and wrong. That is not your prompt. These models were built to predict the next piece of text inside a limited window, not to read a long, structured document page by page and hand it back complete. Last evaluated: October 2026.&lt;/p&gt;

&lt;p&gt;The short answer to how to do it properly: read the document page by page, join the pieces that belong together (above all tables that run over several pages), extract the fields from the joined result against a schema, and check that the output is complete. The rest of this guide shows why the shortcuts fail and what works.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you put it all in an LLM, we get it: the output was not good
&lt;/h2&gt;

&lt;p&gt;It is the first thing almost everyone tries, and it breaks for four reasons that have nothing to do with how well you wrote the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The answer does not fit in the reply.&lt;/strong&gt; Models accept far more input than they can write back in one response, so a long table cannot be returned in a single call, however large the input window is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The middle of the document gets read worse.&lt;/strong&gt; Research on long contexts (Liu et al., 2023) shows that accuracy drops for information in the middle of a long input compared with the start and the end. The table that begins on page 87 sits in that zone. &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and latency grow with the page count&lt;/strong&gt;, and a failed attempt repeats the whole bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing tells you what is missing.&lt;/strong&gt; The model answers fluently whether it saw every row or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The story does not end there for most teams, because the next idea is the obvious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  So you split it into chunks, and that did not work either
&lt;/h2&gt;

&lt;p&gt;Cutting the PDF into pages or fixed-size chunks solves the window problem and creates new ones. A table that runs over eight pages becomes eight unrelated fragments. Column headers that repeat on every page are read as data. A field that is printed once on page 1, such as the supplier or the contract number, is invisible to the chunk that reads page 40. Soon the code that stitches results back together is bigger than the extraction itself, and it breaks with every new layout.&lt;/p&gt;

&lt;p&gt;The more careful variants (splitting by sections, or extracting each table row in parallel) help, but each leaves something open: a table that crosses a section boundary, or document-level fields living in a different call. We go through these trade-offs in &lt;a href="https://anyformat.ai/blog/anyformat-long-documents-extraction" rel="noopener noreferrer"&gt;our long-documents essay&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approaches, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Keeps multi-page tables whole&lt;/th&gt;
&lt;th&gt;Document-level fields next to table rows&lt;/th&gt;
&lt;th&gt;Tells you if rows are missing&lt;/th&gt;
&lt;th&gt;Typical limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whole document in one prompt&lt;/td&gt;
&lt;td&gt;Only if it fits the reply&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Output cap, middle-of-context loss, cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size or page chunks&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Stitching code becomes the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split by sections, extract in parallel&lt;/td&gt;
&lt;td&gt;Not across a section boundary&lt;/td&gt;
&lt;td&gt;Needs a separate pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Tables that cross a boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One extraction per table row, in parallel&lt;/td&gt;
&lt;td&gt;Row by row&lt;/td&gt;
&lt;td&gt;Separate call&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;Document context lives in another call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic IDP (OCR + parse per page, extract across pages)&lt;/td&gt;
&lt;td&gt;Yes, if table continuity is detected&lt;/td&gt;
&lt;td&gt;Yes, same schema&lt;/td&gt;
&lt;td&gt;Yes, if you add checks&lt;/td&gt;
&lt;td&gt;Needs a parser that keeps layout and structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the pattern that holds up, and it is what an agentic IDP does. For tools that implement these patterns, see &lt;a href="https://anyformat.ai/blog/best-document-parsing-apis" rel="noopener noreferrer"&gt;the best document parsing APIs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: an agentic IDP with OCR
&lt;/h2&gt;

&lt;p&gt;The pattern that holds up is what an intelligent document processing (IDP) platform does: OCR and layout analysis first, then an agent that reasons over the whole document. anyformat works like this, in two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse every page on its own, in parallel.&lt;/strong&gt; OCR and layout analysis split each page into blocks (text, tables, figures, headers, footers) and fix the reading order. Each page is independent, so 300 pages are 300 small jobs and not one huge one. Scans go through OCR first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract across the whole set of parsed blocks.&lt;/strong&gt; An agentic extractor looks at the blocks of every page, recognizes which table fragments belong to the same table and joins them in order, and reads document-level fields from wherever they sit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You describe the result once, as a schema with two kinds of fields: values that appear once per document (supplier, contract reference, filing date, total) and a table field with its columns. A single run returns both together, so you do not need a second pass or a join afterwards. In anyformat this is a Parse node followed by an Extract node, and the Extract node has an Agentic mode intended for dense tables and values that span several pages.&lt;/p&gt;

&lt;p&gt;The table field is an &lt;code&gt;object&lt;/code&gt; field with nested columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"One row per line of the table, across all pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nested_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Quantity ordered"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Price per unit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line_total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Row total amount"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a list with one entry per row, and every value carries a confidence score and the text and page it was read from. For the full flow with code, see &lt;a href="https://anyformat.ai/blog/extract-structured-data-from-pdf-with-llm" rel="noopener noreferrer"&gt;how to extract structured data from a PDF with an LLM&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it on your own long document:&lt;/strong&gt; the &lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;Free tier&lt;/a&gt; includes 50,000 credits with no credit card, enough to run a long file through this workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It also works for RAG.&lt;/strong&gt; If the goal is retrieval and not fields, the same parse step gives you the document as structured text with tables kept whole, ready to index. See &lt;a href="https://anyformat.ai/blog/pdf-to-markdown-for-llms" rel="noopener noreferrer"&gt;PDF to markdown for LLMs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check nothing is missing when you extract data from a long PDF
&lt;/h2&gt;

&lt;p&gt;A long document makes silent loss easy, so build the check into the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count the rows.&lt;/strong&gt; If the document says "42 items", the extraction should have 42 rows. A different count flags the document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the totals close.&lt;/strong&gt; The sum of the line totals should match the printed subtotal, and subtotal plus tax should match the grand total. A validation step can run these rules on every document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the confidence of the rows that matter.&lt;/strong&gt; Set a &lt;a href="https://anyformat.ai/blog/anyformat-confidence" rel="noopener noreferrer"&gt;threshold&lt;/a&gt; and route what falls below it to a person who sees the source page next to the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the seams.&lt;/strong&gt; Open the page breaks of one test document by hand: the last row of a page and the first of the next are where a table is most likely to be cut or duplicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Evidence on each value is what makes the manual check fast: the reviewer clicks a value and lands on the page it came from. See &lt;a href="https://anyformat.ai/blog/anyformat-visual-grounding-auditable-document-ai" rel="noopener noreferrer"&gt;why grounding makes extraction auditable&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs with anyformat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;anyformat's pricing&lt;/a&gt; is credit-based, per page and per operator. At list rates, Standard Parse is 25 credits and Standard Extract 35 credits per page, so 60 credits per page; Agentic is 100 and 150 credits, so 250 credits per page. A credit costs €0.001 on Business and €0.0015 on Free and pay-as-you-go. For a 100-page document that is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;100-page document&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;th&gt;Business (€0.001 per credit)&lt;/th&gt;
&lt;th&gt;Free / pay-as-you-go (€0.0015)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Parse + Standard Extract&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;€6.00&lt;/td&gt;
&lt;td&gt;€9.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic Parse + Agentic Extract&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;€25.00&lt;/td&gt;
&lt;td&gt;€37.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Validation rules are billed separately, per rule and once per extraction rather than per page; check the &lt;a href="https://docs.anyformat.ai/concepts/how-credits-work" rel="noopener noreferrer"&gt;credits page&lt;/a&gt; for the current rate. Start with Standard and move to Agentic only for the documents where Standard gets a table wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scans and photos&lt;/strong&gt; lower accuracy for every method. Use a confidence threshold and a review step for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic mode does not support multi-file documents yet&lt;/strong&gt; (several files treated as one document). Combine files before upload or process them separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very irregular tables&lt;/strong&gt; (merged cells, tables that change columns halfway) need a test on your own files before you trust them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy depends on your documents.&lt;/strong&gt; Before you rely on any vendor's headline figure, ours included, test fifty of your own, count rows against the source and measure field-level accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;If your documents are short and clean, a single prompt is enough. If they are long, ask each tool three questions: does a table that spans pages come back as one table, do document-level fields come back in the same result, and what tells me a row is missing? A tool that cannot answer the third one will fail you quietly. For a comparison of vendors, see &lt;a href="https://anyformat.ai/vs" rel="noopener noreferrer"&gt;our comparisons&lt;/a&gt; and, for a specific one, &lt;a href="https://anyformat.ai/vs/anyformat-vs-docsumo" rel="noopener noreferrer"&gt;anyformat vs Docsumo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can an LLM extract data from a 200-page PDF?&lt;/strong&gt; Yes, but not reliably in one prompt. The reply cannot hold a long table, accuracy drops in the middle of long inputs and nothing flags what was missed. Parse the pages separately and extract across them against a schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why was the output of my long PDF not good?&lt;/strong&gt; These models were built to predict text inside a limited window, not to reproduce a long structured document. They also give no signal about what they missed, so a wrong answer looks the same as a right one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I extract a table that spans many pages?&lt;/strong&gt; Detect which page-level table fragments belong together, join them in order, and ignore repeated column headers. Then compare the row count and totals with the document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get the supplier name and the line items in one result?&lt;/strong&gt; Declare both in one schema: single fields for the document-level values and an object field with nested columns for the table. One run returns them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know no rows were dropped?&lt;/strong&gt; Count the rows against the document, make the totals close, and review low-confidence values with their source page open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this for RAG?&lt;/strong&gt; Yes: the parse step returns the document as structured text with tables kept whole, ready to index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does it cost per page?&lt;/strong&gt; At Standard rates, 60 credits per page (€0.06 on Business); at Agentic rates, 250 credits per page (€0.25 on Business).&lt;/p&gt;




&lt;p&gt;Start with your longest documents. Upload the file that breaks your current pipeline, define the fields you need, and see the values with their confidence and their evidence on the page. The Free tier includes 50,000 credits with no credit card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: anyformat sells document extraction; the comparison criteria and dates are stated.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/extract-data-from-long-pdfs-llm" rel="noopener noreferrer"&gt;anyformat.ai/blog/extract-data-from-long-pdfs-llm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ocr</category>
      <category>pdf</category>
      <category>python</category>
    </item>
    <item>
      <title>How to extract data from long PDFs with an LLM</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:05:48 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/how-to-extract-data-from-long-pdfs-with-an-llm-3mnd</link>
      <guid>https://dev.to/felipe_anyformat/how-to-extract-data-from-long-pdfs-with-an-llm-3mnd</guid>
      <description>&lt;p&gt;You need data out of a 200-page PDF because you want to feed a RAG pipeline, fill a database from contracts or filings, or ask questions that cross hundreds of pages.&lt;/p&gt;

&lt;p&gt;If you tried putting the whole file into an LLM, we get it: the output was not good. Rows were missing, a total did not match, the answer was confident and wrong. That is not your prompt. These models were built to predict the next piece of text inside a limited window, not to read a long, structured document page by page and hand it back complete. Last evaluated: October 2026.&lt;/p&gt;

&lt;p&gt;The short answer to how to do it properly: read the document page by page, join the pieces that belong together (above all tables that run over several pages), extract the fields from the joined result against a schema, and check that the output is complete. The rest of this guide shows why the shortcuts fail and what works.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you put it all in an LLM, we get it: the output was not good
&lt;/h2&gt;

&lt;p&gt;It is the first thing almost everyone tries, and it breaks for four reasons that have nothing to do with how well you wrote the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The answer does not fit in the reply.&lt;/strong&gt; Models accept far more input than they can write back in one response, so a long table cannot be returned in a single call, however large the input window is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The middle of the document gets read worse.&lt;/strong&gt; Research on long contexts (Liu et al., 2023) shows that accuracy drops for information in the middle of a long input compared with the start and the end. The table that begins on page 87 sits in that zone. &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and latency grow with the page count&lt;/strong&gt;, and a failed attempt repeats the whole bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing tells you what is missing.&lt;/strong&gt; The model answers fluently whether it saw every row or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The story does not end there for most teams, because the next idea is the obvious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  So you split it into chunks, and that did not work either
&lt;/h2&gt;

&lt;p&gt;Cutting the PDF into pages or fixed-size chunks solves the window problem and creates new ones. A table that runs over eight pages becomes eight unrelated fragments. Column headers that repeat on every page are read as data. A field that is printed once on page 1, such as the supplier or the contract number, is invisible to the chunk that reads page 40. Soon the code that stitches results back together is bigger than the extraction itself, and it breaks with every new layout.&lt;/p&gt;

&lt;p&gt;The more careful variants (splitting by sections, or extracting each table row in parallel) help, but each leaves something open: a table that crosses a section boundary, or document-level fields living in a different call. We go through these trade-offs in &lt;a href="https://anyformat.ai/blog/anyformat-long-documents-extraction" rel="noopener noreferrer"&gt;our long-documents essay&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approaches, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Keeps multi-page tables whole&lt;/th&gt;
&lt;th&gt;Document-level fields next to table rows&lt;/th&gt;
&lt;th&gt;Tells you if rows are missing&lt;/th&gt;
&lt;th&gt;Typical limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whole document in one prompt&lt;/td&gt;
&lt;td&gt;Only if it fits the reply&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Output cap, middle-of-context loss, cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size or page chunks&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Stitching code becomes the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split by sections, extract in parallel&lt;/td&gt;
&lt;td&gt;Not across a section boundary&lt;/td&gt;
&lt;td&gt;Needs a separate pass&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Tables that cross a boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One extraction per table row, in parallel&lt;/td&gt;
&lt;td&gt;Row by row&lt;/td&gt;
&lt;td&gt;Separate call&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;Document context lives in another call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic IDP (OCR + parse per page, extract across pages)&lt;/td&gt;
&lt;td&gt;Yes, if table continuity is detected&lt;/td&gt;
&lt;td&gt;Yes, same schema&lt;/td&gt;
&lt;td&gt;Yes, if you add checks&lt;/td&gt;
&lt;td&gt;Needs a parser that keeps layout and structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the pattern that holds up, and it is what an agentic IDP does. For tools that implement these patterns, see &lt;a href="https://anyformat.ai/blog/best-document-parsing-apis" rel="noopener noreferrer"&gt;the best document parsing APIs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: an agentic IDP with OCR
&lt;/h2&gt;

&lt;p&gt;The pattern that holds up is what an intelligent document processing (IDP) platform does: OCR and layout analysis first, then an agent that reasons over the whole document. anyformat works like this, in two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse every page on its own, in parallel.&lt;/strong&gt; OCR and layout analysis split each page into blocks (text, tables, figures, headers, footers) and fix the reading order. Each page is independent, so 300 pages are 300 small jobs and not one huge one. Scans go through OCR first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract across the whole set of parsed blocks.&lt;/strong&gt; An agentic extractor looks at the blocks of every page, recognizes which table fragments belong to the same table and joins them in order, and reads document-level fields from wherever they sit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You describe the result once, as a schema with two kinds of fields: values that appear once per document (supplier, contract reference, filing date, total) and a table field with its columns. A single run returns both together, so you do not need a second pass or a join afterwards. In anyformat this is a Parse node followed by an Extract node, and the Extract node has an Agentic mode intended for dense tables and values that span several pages.&lt;/p&gt;

&lt;p&gt;The table field is an &lt;code&gt;object&lt;/code&gt; field with nested columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"One row per line of the table, across all pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nested_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Quantity ordered"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Price per unit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line_total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Row total amount"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a list with one entry per row, and every value carries a confidence score and the text and page it was read from. For the full flow with code, see &lt;a href="https://anyformat.ai/blog/extract-structured-data-from-pdf-with-llm" rel="noopener noreferrer"&gt;how to extract structured data from a PDF with an LLM&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it on your own long document:&lt;/strong&gt; the &lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;Free tier&lt;/a&gt; includes 50,000 credits with no credit card, enough to run a long file through this workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It also works for RAG.&lt;/strong&gt; If the goal is retrieval and not fields, the same parse step gives you the document as structured text with tables kept whole, ready to index. See &lt;a href="https://anyformat.ai/blog/pdf-to-markdown-for-llms" rel="noopener noreferrer"&gt;PDF to markdown for LLMs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check nothing is missing when you extract data from a long PDF
&lt;/h2&gt;

&lt;p&gt;A long document makes silent loss easy, so build the check into the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count the rows.&lt;/strong&gt; If the document says "42 items", the extraction should have 42 rows. A different count flags the document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the totals close.&lt;/strong&gt; The sum of the line totals should match the printed subtotal, and subtotal plus tax should match the grand total. A validation step can run these rules on every document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the confidence of the rows that matter.&lt;/strong&gt; Set a &lt;a href="https://anyformat.ai/blog/anyformat-confidence" rel="noopener noreferrer"&gt;threshold&lt;/a&gt; and route what falls below it to a person who sees the source page next to the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the seams.&lt;/strong&gt; Open the page breaks of one test document by hand: the last row of a page and the first of the next are where a table is most likely to be cut or duplicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Evidence on each value is what makes the manual check fast: the reviewer clicks a value and lands on the page it came from. See &lt;a href="https://anyformat.ai/blog/anyformat-visual-grounding-auditable-document-ai" rel="noopener noreferrer"&gt;why grounding makes extraction auditable&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs with anyformat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;anyformat's pricing&lt;/a&gt; is credit-based, per page and per operator. At list rates, Standard Parse is 25 credits and Standard Extract 35 credits per page, so 60 credits per page; Agentic is 100 and 150 credits, so 250 credits per page. A credit costs €0.001 on Business and €0.0015 on Free and pay-as-you-go. For a 100-page document that is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;100-page document&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;th&gt;Business (€0.001 per credit)&lt;/th&gt;
&lt;th&gt;Free / pay-as-you-go (€0.0015)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Parse + Standard Extract&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;€6.00&lt;/td&gt;
&lt;td&gt;€9.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic Parse + Agentic Extract&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;€25.00&lt;/td&gt;
&lt;td&gt;€37.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Validation rules are billed separately, per rule and once per extraction rather than per page; check the &lt;a href="https://docs.anyformat.ai/concepts/how-credits-work" rel="noopener noreferrer"&gt;credits page&lt;/a&gt; for the current rate. Start with Standard and move to Agentic only for the documents where Standard gets a table wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scans and photos&lt;/strong&gt; lower accuracy for every method. Use a confidence threshold and a review step for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic mode does not support multi-file documents yet&lt;/strong&gt; (several files treated as one document). Combine files before upload or process them separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very irregular tables&lt;/strong&gt; (merged cells, tables that change columns halfway) need a test on your own files before you trust them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy depends on your documents.&lt;/strong&gt; Before you rely on any vendor's headline figure, ours included, test fifty of your own, count rows against the source and measure field-level accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;If your documents are short and clean, a single prompt is enough. If they are long, ask each tool three questions: does a table that spans pages come back as one table, do document-level fields come back in the same result, and what tells me a row is missing? A tool that cannot answer the third one will fail you quietly. For a comparison of vendors, see &lt;a href="https://anyformat.ai/vs" rel="noopener noreferrer"&gt;our comparisons&lt;/a&gt; and, for a specific one, &lt;a href="https://anyformat.ai/vs/anyformat-vs-docsumo" rel="noopener noreferrer"&gt;anyformat vs Docsumo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can an LLM extract data from a 200-page PDF?&lt;/strong&gt; Yes, but not reliably in one prompt. The reply cannot hold a long table, accuracy drops in the middle of long inputs and nothing flags what was missed. Parse the pages separately and extract across them against a schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why was the output of my long PDF not good?&lt;/strong&gt; These models were built to predict text inside a limited window, not to reproduce a long structured document. They also give no signal about what they missed, so a wrong answer looks the same as a right one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I extract a table that spans many pages?&lt;/strong&gt; Detect which page-level table fragments belong together, join them in order, and ignore repeated column headers. Then compare the row count and totals with the document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get the supplier name and the line items in one result?&lt;/strong&gt; Declare both in one schema: single fields for the document-level values and an object field with nested columns for the table. One run returns them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know no rows were dropped?&lt;/strong&gt; Count the rows against the document, make the totals close, and review low-confidence values with their source page open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this for RAG?&lt;/strong&gt; Yes: the parse step returns the document as structured text with tables kept whole, ready to index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does it cost per page?&lt;/strong&gt; At Standard rates, 60 credits per page (€0.06 on Business); at Agentic rates, 250 credits per page (€0.25 on Business).&lt;/p&gt;




&lt;p&gt;Start with your longest documents. Upload the file that breaks your current pipeline, define the fields you need, and see the values with their confidence and their evidence on the page. The Free tier includes 50,000 credits with no credit card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: anyformat sells document extraction; the comparison criteria and dates are stated.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/extract-data-from-long-pdfs-llm" rel="noopener noreferrer"&gt;anyformat.ai/blog/extract-data-from-long-pdfs-llm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>python</category>
      <category>pdf</category>
    </item>
    <item>
      <title>Invoice data extraction API: how to extract invoices to JSON</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:00:19 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/invoice-data-extraction-api-how-to-extract-invoices-to-json-3ebd</link>
      <guid>https://dev.to/felipe_anyformat/invoice-data-extraction-api-how-to-extract-invoices-to-json-3ebd</guid>
      <description>&lt;p&gt;An invoice data extraction API takes an invoice as a PDF or image and returns structured JSON: header fields such as invoice number, vendor, dates and totals, plus the line items, each value with a confidence score and, in the better ones, the place on the page it was read from. With anyformat you describe the fields once as a schema, run a workflow, and read the result. This guide shows the full flow with code, what comes back, what it costs, and how it compares with the other ways of doing it. Last evaluated: October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an invoice extraction API should return
&lt;/h2&gt;

&lt;p&gt;For accounts payable the useful output is not text, it is a record you can post to an ERP or accounting system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header fields:&lt;/strong&gt; invoice number, vendor name, issue date, due date, currency, subtotal, tax amount, total amount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line items:&lt;/strong&gt; description, quantity, unit price and amount for each row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A confidence score per value&lt;/strong&gt;, so you can decide what is posted automatically and what a person checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; the text and page each value came from, so a reviewer can verify it in seconds instead of rereading the invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No invented values:&lt;/strong&gt; when a field is not on the document, the right answer is empty, not a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two are what separate an extraction API from OCR with a script on top. See &lt;a href="https://anyformat.ai/blog/extract-structured-data-from-pdf-with-llm" rel="noopener noreferrer"&gt;how to extract structured data from a PDF with an LLM&lt;/a&gt; for the general case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow: three calls
&lt;/h2&gt;

&lt;p&gt;The API has three steps. Create a workflow with a schema, upload an invoice against it, and fetch the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the workflow&lt;/strong&gt; (a parse node and an extract node, connected):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://api.anyformat.ai/v3/workflows/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "Invoice extractor",
    "nodes": [
      { "id": "parse_1", "type": "parse" },
      {
        "id": "extract_1",
        "type": "extract",
        "extraction_schema": {
          "fields": [
            { "name": "invoice_number", "description": "Invoice ID printed on the document", "data_type": "string" },
            { "name": "vendor_name", "description": "Company that issued the invoice", "data_type": "string" },
            { "name": "issue_date", "description": "Date the invoice was issued", "data_type": "date" },
            { "name": "due_date", "description": "Payment due date", "data_type": "date" },
            { "name": "subtotal", "description": "Total before tax", "data_type": "float" },
            { "name": "tax_amount", "description": "Total tax", "data_type": "float" },
            { "name": "total_amount", "description": "Grand total to pay", "data_type": "float" },
            { "name": "currency", "description": "Currency of the amounts", "data_type": "enum",
              "enum_options": [
                { "name": "USD", "description": "US dollar" },
                { "name": "EUR", "description": "Euro" },
                { "name": "GBP", "description": "Pound sterling" }
              ] },
            {
              "name": "line_items",
              "description": "One row per line on the invoice",
              "data_type": "object",
              "nested_fields": [
                { "name": "description", "data_type": "string" },
                { "name": "quantity", "data_type": "integer" },
                { "name": "unit_price", "data_type": "float" },
                { "name": "amount", "data_type": "float" }
              ]
            }
          ]
        }
      }
    ],
    "edges": [ { "source": "parse_1", "target": "extract_1" } ]
  }'&lt;/span&gt;  &lt;span class="c"&gt;# returns { "id": "&amp;lt;workflow_id&amp;gt;", ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Upload an invoice and run it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.anyformat.ai/v3/workflows/&amp;lt;workflow_id&amp;gt;/upload/run/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"files=@invoice.pdf"&lt;/span&gt;  &lt;span class="c"&gt;# returns 202 { "run_id": "&amp;lt;run_id&amp;gt;", "document_packet_id": "...", "workflow_id": "...", "status": "queued" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Fetch the result.&lt;/strong&gt; The endpoint always answers &lt;code&gt;200&lt;/code&gt;; the &lt;code&gt;status&lt;/code&gt; field tells you where the run is (&lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;in_progress&lt;/code&gt;, &lt;code&gt;processed&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt; or &lt;code&gt;cancelled&lt;/code&gt;), and the results appear once it is &lt;code&gt;processed&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.anyformat.ai/v3/runs/&amp;lt;run_id&amp;gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same flow as a Python function, with polling and backoff (install the library first with &lt;code&gt;pip install requests&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.anyformat.ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v3/workflows/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/upload/run/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v3/runs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extractions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ended with status &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# queued or in_progress
&lt;/span&gt;        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice still processing after 60 attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;workflow_id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submissions are limited to 60 requests per minute and the other endpoints to 600. Send an &lt;code&gt;Idempotency-Key&lt;/code&gt; header on POST requests so a retry does not duplicate work. See the &lt;a href="https://docs.anyformat.ai/api-reference-v3/introduction" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes back
&lt;/h2&gt;

&lt;p&gt;Every field in &lt;code&gt;results.extractions[0].fields&lt;/code&gt; returns its value, a confidence score, a verification status and the evidence it was read from. Real output for a fictional invoice, trimmed to two header fields and the first line item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2026-0417"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value_override"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verification_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;91.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice number: INV-2026-0417&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Issue date: 2026-09-15&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Due date: 2026-10-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"page_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2141.7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value_override"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verification_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;92.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Total due (EUR) 2,141.70"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"page_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cloud hosting, September"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value_override"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"verification_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;98.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cloud hosting, September | 1 | 1,200.00 | 1,200.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"page_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1200.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value_override"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"verification_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;98.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cloud hosting, September | 1 | 1,200.00 | 1,200.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"page_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that save debugging time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Values arrive as strings&lt;/strong&gt;, even for numbers and dates. Cast them with the data type you declared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;null&lt;/code&gt; means not found.&lt;/strong&gt; The model does not invent a value when the document does not contain it; use the confidence score to decide what to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line items come back as a list of rows.&lt;/strong&gt; &lt;code&gt;fields["line_items"]&lt;/code&gt; is a list with one entry per row of the invoice, and each entry maps a column name (&lt;code&gt;description&lt;/code&gt;, &lt;code&gt;quantity&lt;/code&gt;, &lt;code&gt;unit_price&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;) to a cell with the same shape as any other field: &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt; and &lt;code&gt;evidence&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A confidence score is only useful if you act on it: set a &lt;a href="https://anyformat.ai/blog/anyformat-confidence" rel="noopener noreferrer"&gt;threshold&lt;/a&gt;, post the invoices above it automatically, and send the rest to a person who sees the evidence next to the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to extract invoice data, compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Invoice fields&lt;/th&gt;
&lt;th&gt;Line items&lt;/th&gt;
&lt;th&gt;Custom fields&lt;/th&gt;
&lt;th&gt;Confidence and evidence&lt;/th&gt;
&lt;th&gt;Price (list, date checked)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OCR plus regex or templates&lt;/td&gt;
&lt;td&gt;Per vendor layout&lt;/td&gt;
&lt;td&gt;Fragile&lt;/td&gt;
&lt;td&gt;New template each time&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Your engineering time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Textract AnalyzeExpense&lt;/td&gt;
&lt;td&gt;Prebuilt&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Per-field confidence&lt;/td&gt;
&lt;td&gt;$10 per 1,000 pages, $0.01 per page up to 1 million pages, US West (Oregon) (AWS pricing page, read 2026-10-07, &lt;a href="https://anyformat.ai/blog/best-aws-textract-alternatives" rel="noopener noreferrer"&gt;our Textract comparison&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Document AI invoice parser&lt;/td&gt;
&lt;td&gt;Prebuilt&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Custom extractor&lt;/td&gt;
&lt;td&gt;Per-field confidence&lt;/td&gt;
&lt;td&gt;$0.10 per document of up to 10 pages, so $0.10 for a one-page invoice (Google Cloud pricing page, read 2026-10-07, &lt;a href="https://anyformat.ai/blog/best-google-document-ai-alternatives" rel="noopener noreferrer"&gt;our Google comparison&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Document Intelligence prebuilt invoice&lt;/td&gt;
&lt;td&gt;Prebuilt&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Needs labeled training data&lt;/td&gt;
&lt;td&gt;Per-field confidence&lt;/td&gt;
&lt;td&gt;$10 per 1,000 pages for all prebuilt models, invoices included (Azure pricing page, read 2026-10-07, &lt;a href="https://anyformat.ai/blog/best-azure-document-intelligence-alternatives" rel="noopener noreferrer"&gt;our Azure comparison&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema-based extraction (anyformat)&lt;/td&gt;
&lt;td&gt;Any schema you define&lt;/td&gt;
&lt;td&gt;Object field with nested columns&lt;/td&gt;
&lt;td&gt;A schema change, no retraining&lt;/td&gt;
&lt;td&gt;Per value, with evidence&lt;/td&gt;
&lt;td&gt;Credits per page and per operator (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS Textract and Azure list their prebuilt invoice models at $10 per 1,000 pages, which is cheaper per page than a schema-based API, and they are a good fit when your invoices match their model and you stay on one cloud. Google bills by document, $0.10 for up to 10 pages, so one-page invoices cost more per page there than anywhere else in this table. The trade-off for all three is custom fields: a field they did not anticipate means training or a second tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt; is credit-based, per page and per operator. At list rates, Standard Parse is 25 credits and Standard Extract 35 credits per page, so one invoice page is 60 credits. A credit costs €0.001 on Business and €0.0015 on Free and pay-as-you-go: that is €0.06 per page on Business, or €60 per 1,000 pages, and €0.09 per page on pay-as-you-go. Agentic mode, for dense or complex invoices, costs 100 credits for Parse and 150 for Extract. The Free tier includes 50,000 one-time credits with no credit card, which covers about 800 single-page invoices at Standard rates.&lt;/p&gt;

&lt;p&gt;That is more per page than the AWS and Azure prebuilt invoice models at $10 per 1,000 pages. The difference buys a schema you control, evidence for every value, and the same API for other document types. Run the numbers on your own volume before choosing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scans and photos:&lt;/strong&gt; accuracy drops with skew, noise and handwriting. Use a confidence threshold and a review step for those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-page invoices:&lt;/strong&gt; test one long invoice from each supplier you handle; dense tables across pages are what Agentic mode is for, and Agentic mode does not support multi-file documents yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several invoices in one PDF:&lt;/strong&gt; add a split step so each invoice is extracted separately, and read &lt;code&gt;extractions&lt;/code&gt;, which lists one entry per detected document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoices are business and sometimes personal data.&lt;/strong&gt; Zero data retention is an Enterprise option that has to be enabled for your organization, so on the Free and Business plans use fictional invoices for tests and read &lt;a href="https://anyformat.ai/blog/gdpr-compliant-document-ai" rel="noopener noreferrer"&gt;our GDPR guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Accuracy depends on your documents. Before you rely on any vendor's headline figure, ours included, test fifty of your own invoices and measure field-level accuracy and the share that pass with no human touch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose an invoice extraction API
&lt;/h2&gt;

&lt;p&gt;If your invoices match a prebuilt model and you live in one cloud, start with that cloud's invoice parser. If you need custom fields, several suppliers with different layouts, or evidence a reviewer can check, a schema-based API is the better fit. Ask every vendor the same question: when a value is wrong, what does my team see? For the broader process, see &lt;a href="https://anyformat.ai/use-cases/invoice-processing" rel="noopener noreferrer"&gt;invoice processing with anyformat&lt;/a&gt; and the &lt;a href="https://anyformat.ai/industries/accounts-payable" rel="noopener noreferrer"&gt;accounts payable page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is an invoice data extraction API?&lt;/strong&gt;&lt;br&gt;
An API that receives an invoice (PDF or image) and returns structured data, usually JSON with header fields, line items and a confidence score per value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I extract line items from an invoice?&lt;/strong&gt;&lt;br&gt;
Define a &lt;code&gt;line_items&lt;/code&gt; field of type &lt;code&gt;object&lt;/code&gt; with nested fields (description, quantity, unit price, amount). The API returns one row per line, each cell with its own confidence and evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work with scanned invoices?&lt;/strong&gt;&lt;br&gt;
Yes, with OCR before extraction. Scans and photos are the weak spot of every approach, so set a confidence threshold and review what falls below it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What format does the API return?&lt;/strong&gt;&lt;br&gt;
JSON. Values arrive as strings and you cast them using the data type you declared; a value that is not on the invoice comes back as &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is it priced?&lt;/strong&gt;&lt;br&gt;
Per page and per operator in credits. At Standard rates one invoice page is 60 credits, or €0.06 on Business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I process many invoices at once?&lt;/strong&gt;&lt;br&gt;
Yes: run the workflow once per file and poll each run. Submissions are limited to 60 requests per minute.&lt;/p&gt;




&lt;p&gt;Start with your hardest documents. Upload the invoices that break your current pipeline, define the fields you need, and see the values with their confidence and their evidence on the page. The Free tier includes 50,000 credits with no credit card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: anyformat sells document extraction; the example is fictional and the comparison criteria and dates are stated.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/invoice-data-extraction-api" rel="noopener noreferrer"&gt;anyformat.ai/blog/invoice-data-extraction-api&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>json</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to extract data from bank statements automatically</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:00:27 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/how-to-extract-data-from-bank-statements-automatically-2c8o</link>
      <guid>https://dev.to/felipe_anyformat/how-to-extract-data-from-bank-statements-automatically-2c8o</guid>
      <description>&lt;p&gt;You extract data from bank statements automatically by reading each PDF or scan into a structured schema (account holder, account number, period, opening and closing balance, and one row per transaction) and then checking that the arithmetic closes: opening balance plus credits minus debits must equal the closing balance. The first step is the easy part. The check is what tells you whether to trust the output, because a bank statement is a long table where one dropped row looks exactly like a correct one.&lt;/p&gt;

&lt;p&gt;This guide walks through the problem, the main ways people solve it, and a worked example with a fictional statement. Last evaluated: October 2026. For long statements, see how we handle &lt;a href="https://anyformat.ai/blog/anyformat-long-documents-extraction" rel="noopener noreferrer"&gt;multi-page documents&lt;/a&gt;; for finance teams, see &lt;a href="https://anyformat.ai/industries/financial-services" rel="noopener noreferrer"&gt;anyformat for financial services&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bank statements are harder than they look
&lt;/h2&gt;

&lt;p&gt;A bank statement looks like a clean table, and that is the trap. Five things make it hard to extract reliably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every bank lays it out differently.&lt;/strong&gt; Column order, date formats, whether debits and credits are separate columns or one signed column, and where the opening balance sits all change from bank to bank, and often between a bank's own products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions run across pages.&lt;/strong&gt; The table breaks at the page boundary, headers repeat or disappear, and a row can be cut in two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions wrap onto several lines.&lt;/strong&gt; A naive parser reads the second line of a description as a new, empty transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scans and photos.&lt;/strong&gt; Many statements arrive as scans or phone photos, with skew, noise and stamps over the numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several accounts or statements in one file.&lt;/strong&gt; A single PDF can hold more than one account, or more than one month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a reason to avoid automation. It is the reason the output needs a check built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you want out: a schema
&lt;/h2&gt;

&lt;p&gt;Start from the output, not from the tool. For a typical current-account statement:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;account_holder&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;As printed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;account_number&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;IBAN or local format, as printed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;period_start, period_end&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opening_balance, closing_balance&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;With currency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions[]&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;One entry per row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions[].date&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;Booking date; keep the value date if both exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions[].description&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Joined across wrapped lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions[].debit, .credit&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;Or one signed amount, pick one convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions[].balance&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;Running balance, when the statement prints it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fictional example of the result for one page (all values invented):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_holder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Northwind Trading Ltd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES00 0000 0000 0000 0000 0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"period_start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"period_end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"opening_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;12480.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"closing_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11902.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transfer from ACME SA, invoice 1043"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"credit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3200.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15680.15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Card payment, Office Supplies Co"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;84.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15595.25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Direct debit, Utilities Ltd, September"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;312.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15282.65&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ways to extract bank statement data, compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Handles different banks&lt;/th&gt;
&lt;th&gt;Handles scans&lt;/th&gt;
&lt;th&gt;Catches wrong rows&lt;/th&gt;
&lt;th&gt;Typical limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy and paste into a spreadsheet&lt;/td&gt;
&lt;td&gt;Yes, by hand&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Only if you check&lt;/td&gt;
&lt;td&gt;Hours per batch, error-prone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template or rules per bank&lt;/td&gt;
&lt;td&gt;One template per layout&lt;/td&gt;
&lt;td&gt;Poorly&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Breaks when a bank changes its PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic OCR plus a table library&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Tables with merged or wrapped cells fail silently; see &lt;a href="https://anyformat.ai/blog/extract-tables-from-pdf" rel="noopener noreferrer"&gt;our table-library test&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompting a general LLM with the file&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Fluent output with no signal for a wrong row; see &lt;a href="https://anyformat.ai/blog/reduce-llm-hallucinations-document-extraction" rel="noopener noreferrer"&gt;how to reduce hallucinations&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bank-statement specialist (for example DocuClipper)&lt;/td&gt;
&lt;td&gt;Yes, built for financial formats&lt;/td&gt;
&lt;td&gt;Not stated in our comparison&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;td&gt;Narrow to financial documents; flat monthly tiers (Starter $20 for 60 pages, Business $111 for 640, Enterprise $360 for 2,000), no on-premise or EU residency in its published docs (as-of 2026-09-14, &lt;a href="https://anyformat.ai/blog/best-nanonets-alternatives" rel="noopener noreferrer"&gt;our Nanonets comparison&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A pretrained document-AI platform (for example Docsumo)&lt;/td&gt;
&lt;td&gt;Pretrained bank-statement model; custom types need about 20 training samples&lt;/td&gt;
&lt;td&gt;Not stated in our comparison&lt;/td&gt;
&lt;td&gt;Not stated; 99%+ on bank statement data is vendor-reported and unaudited&lt;/td&gt;
&lt;td&gt;Pricing on quote (&lt;a href="https://anyformat.ai/vs/anyformat-vs-docsumo" rel="noopener noreferrer"&gt;anyformat vs Docsumo&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A document platform with validation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, if you configure the check&lt;/td&gt;
&lt;td&gt;You define the schema and the rules once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The column that matters is "Catches wrong rows". Every method on this table reads a statement. Few tell you which rows to distrust.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to extract data from a bank statement with anyformat, step by step
&lt;/h2&gt;

&lt;p&gt;The same statement runs through four steps in an anyformat workflow, built in Studio as a three-operator flow: Parse, Extract and Validate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F944tyfz564li8tjomdkq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F944tyfz564li8tjomdkq.webp" alt="The Bank Statement Extraction workflow in anyformat Studio: Parse in Agentic mode, Extract in Agentic mode with 8 fields, and Validate with 6 rules" width="799" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse.&lt;/strong&gt; The file becomes structured pages and tables, with each block tied to its position on the page and a confidence score on each table. Scans go through OCR first. In this workflow Parse runs in Agentic mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ah5pqim9g9eafdrush2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ah5pqim9g9eafdrush2.webp" alt="The Parse view in anyformat: the source page on the left and the structured table with its confidence on the right" width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract.&lt;/strong&gt; You describe the schema above in plain language. Header fields come out as single values, and the transactions come out as a list with one entry per row. Extract runs in Agentic mode here, with the 8 fields of the schema. Each field shows its type, whether it is verified, and the review threshold you set. Agentic mode does not support multi-file documents yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdype05iclp7abbtw9lgl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdype05iclp7abbtw9lgl.webp" alt="The Extraction view: each extracted field with its type, a verified status and the review threshold" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate.&lt;/strong&gt; Rules check the result against itself: opening balance plus credits minus debits must equal closing balance; each running balance must follow from the previous one; every transaction date must fall inside the statement period; transactions must be in chronological order; a row cannot hold both a debit and a credit; and values under the confidence threshold are flagged for review. The Validation view shows each rule with its result and the values it checked, and you can test an expression before you rely on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favvw9rk55be1azelacuf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favvw9rk55be1azelacuf.webp" alt="The Validation view: each rule with its result, pass or warning, and the values it checked" width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Review what is doubtful.&lt;/strong&gt; Every extracted value carries a &lt;a href="https://anyformat.ai/blog/anyformat-confidence" rel="noopener noreferrer"&gt;confidence score you can set a threshold on&lt;/a&gt; and a link to the place on the page it was read from. Rows under the threshold, or any statement whose balance check fails, go to a person who sees the source next to the value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What this changes in practice: you do not read every row. You read the rows the check or the confidence flags, and everything else flows through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it on your own statements:&lt;/strong&gt; the &lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;Free tier&lt;/a&gt; includes 50,000 credits, enough to run this workflow on a batch of fictional or consented statements without a credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to convert a bank statement to Excel or CSV
&lt;/h2&gt;

&lt;p&gt;Once the transactions are structured, the Excel or CSV export is the easy part: each transaction becomes a row, with date, description, debit, credit and balance as columns, and the header fields (account holder, account number, period, opening and closing balance) go in a summary sheet or repeat on every row, whichever your accounting tool expects. The step that decides whether the spreadsheet is usable is the one before it: a spreadsheet built from an unchecked extraction hides its errors in plain sight. Run the balance check first, then export only the statements that pass, and send the rest to review.&lt;/p&gt;

&lt;h3&gt;
  
  
  A balance check, spelled out
&lt;/h3&gt;

&lt;p&gt;If the opening balance is 12,480.15, credits total 3,200.00 and debits total 3,777.75, the closing balance must be 11,902.40. If the extraction returns 11,818.00, one row is wrong or missing and the statement is flagged, instead of the error reaching your ledger. The same logic catches a dropped row, a misread digit and a duplicated line. (The figures are the fictional ones from the JSON above, extended to a full month.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Statements are personal data
&lt;/h2&gt;

&lt;p&gt;Bank statements carry personal and financial data. Use fictional or consented documents for tests, keep production data inside a deployment you control, and check where each tool processes it. anyformat is ISO 27001:2022 certified, offers EU data residency and can run self-hosted or air-gapped. Zero data retention is an Enterprise option that has to be enabled for your organization, so on the Free and Business plans treat uploaded documents as stored and use fictional statements for tests. More on residency in &lt;a href="https://anyformat.ai/blog/gdpr-compliant-document-ai" rel="noopener noreferrer"&gt;our GDPR guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt; is credit-based, per page and per operator. At list rates, Parse costs 25 credits per page in Standard mode and 100 in Agentic; Extract costs 35 in Standard and 150 in Agentic; and each AI validation rule costs 5 credits, charged once per extraction and never per page. A credit costs €0.001 on Business and €0.0015 on Free and pay-as-you-go.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;10-page statement, 6 AI validation rules&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;th&gt;Business (€0.001 per credit)&lt;/th&gt;
&lt;th&gt;Free / pay-as-you-go (€0.0015)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Parse + Standard Extract&lt;/td&gt;
&lt;td&gt;630&lt;/td&gt;
&lt;td&gt;€0.63&lt;/td&gt;
&lt;td&gt;€0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic Parse + Agentic Extract&lt;/td&gt;
&lt;td&gt;2,530&lt;/td&gt;
&lt;td&gt;€2.53&lt;/td&gt;
&lt;td&gt;€3.80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Free tier includes 50,000 one-time credits with no credit card, and Business includes 500,000 credits a month. Standard mode is the right starting point for clean digital statements; Agentic is for scans and layouts that Standard gets wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handwriting and very low-quality photos&lt;/strong&gt; are the weak spot of every approach. Set a confidence threshold and let a person review those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statements with unusual structures&lt;/strong&gt; (multi-currency accounts on one page, summary pages that repeat totals) need a test on your own files before you trust them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The balance check only works when the statement prints balances.&lt;/strong&gt; Some formats print only movements; then the check compares totals, not running balances.&lt;/li&gt;
&lt;li&gt;We have not published an accuracy figure for bank statements. Test fifty of your own statements, measure field-level accuracy and the share that pass with no human touch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;If you process a handful of statements a month in one layout, a template or a converter is enough. If layouts vary, scans are common, or an error in a ledger is expensive, choose a tool where a failed check is visible: the question to ask every vendor is "when a balance does not close, what does my team see?". For broader comparisons see &lt;a href="https://anyformat.ai/blog/best-document-parsing-apis" rel="noopener noreferrer"&gt;the best document parsing APIs&lt;/a&gt; and &lt;a href="https://anyformat.ai/blog/best-nanonets-alternatives" rel="noopener noreferrer"&gt;alternatives to Nanonets&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I extract data from a bank statement PDF automatically?&lt;/strong&gt;&lt;br&gt;
Read the file into a schema (header fields plus one row per transaction), then verify that opening balance plus movements equals closing balance. Tools differ mainly in whether they tell you which rows to distrust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I extract data from a scanned bank statement?&lt;/strong&gt;&lt;br&gt;
Yes, with OCR before extraction. Accuracy drops with skew, noise and photos, so scanned statements need a confidence threshold and a review step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I convert a bank statement to Excel or CSV?&lt;/strong&gt;&lt;br&gt;
Yes: once the transactions are structured, exporting to CSV or Excel is a formatting step. The hard part is the extraction and the check, not the export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know the extracted transactions are correct?&lt;/strong&gt;&lt;br&gt;
Run the balance check. If opening balance plus credits minus debits does not equal the closing balance, a row is wrong or missing. Per-field confidence tells you where to look first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to upload bank statements to an extraction tool?&lt;/strong&gt;&lt;br&gt;
Check retention, residency and certifications before you upload real data, and test with fictional statements first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work for statements from different banks?&lt;/strong&gt;&lt;br&gt;
Template-based tools need one template per layout. Schema-based tools read each layout from your field definitions, but you should still test a sample from each bank you handle.&lt;/p&gt;




&lt;p&gt;Start with your hardest documents. Upload the statements that break your current pipeline, define the fields you need, and see the values with their confidence and their evidence on the page. The Free tier includes 50,000 credits with no credit card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: anyformat sells document extraction; the example is fictional and the comparison criteria are stated.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/extract-data-from-bank-statements" rel="noopener noreferrer"&gt;anyformat.ai/blog/extract-data-from-bank-statements&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>ocr</category>
      <category>automation</category>
    </item>
    <item>
      <title>The 8 Best AWS Textract Alternatives in 2026</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Wed, 07 Oct 2026 07:58:00 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/the-8-best-aws-textract-alternatives-in-2026-1ehm</link>
      <guid>https://dev.to/felipe_anyformat/the-8-best-aws-textract-alternatives-in-2026-1ehm</guid>
      <description>&lt;p&gt;The best AWS Textract alternatives in 2026 are anyformat, Google Document AI, Azure Document Intelligence, ABBYY, Nanonets, Reducto, LlamaParse and Unstructured. Teams leave Textract for four reasons: it returns raw OCR rather than fields in your schema, it ships no workflow, review or evaluation layer, it runs only inside AWS regions under US jurisdiction, and its pricing is split across per-page APIs that are hard to forecast. Two of the eight, anyformat and Reducto, can be deployed on-premise in an air-gapped environment; if that is the constraint driving your search, start there.&lt;/p&gt;

&lt;p&gt;To be clear about what Textract does well: it is a mature OCR service with strong table detection, dedicated APIs for forms, expenses and identity documents, and native integration with S3, Lambda and Step Functions. If your documents are structured forms and your stack is fully AWS, it remains a serious option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method.&lt;/strong&gt; We evaluated each tool in July 2026 on five criteria: custom field extraction without training data, on-premise deployment, European data sovereignty, workflow orchestration, and pricing model. Facts come from vendor documentation, third-party benchmarks, and &lt;a href="https://anyformat.ai/blog/anyformat-document-ai-benchmarks" rel="noopener noreferrer"&gt;our own benchmark on 1,000+ real documents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Custom fields without training&lt;/th&gt;
&lt;th&gt;On-premise&lt;/th&gt;
&lt;th&gt;EU sovereignty&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;anyformat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Custom schemas on complex documents, with accuracy you can measure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (zero-shot)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, incl. air-gapped&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;EU-native&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Per page, per operator ($25/1,000 pages parsing)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Document AI&lt;/td&gt;
&lt;td&gt;GCP-native pipelines, 200+ languages&lt;/td&gt;
&lt;td&gt;Yes (Custom Extractor foundation model)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;US jurisdiction, GCP regions&lt;/td&gt;
&lt;td&gt;Per page, per processor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Document Intelligence&lt;/td&gt;
&lt;td&gt;Microsoft-stack teams&lt;/td&gt;
&lt;td&gt;No (5+ labeled docs + training)&lt;/td&gt;
&lt;td&gt;Partial (containers)&lt;/td&gt;
&lt;td&gt;US jurisdiction, Azure regions&lt;/td&gt;
&lt;td&gt;Per page, per model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABBYY&lt;/td&gt;
&lt;td&gt;Large IDP deployments with stable document types&lt;/td&gt;
&lt;td&gt;No (Skills + training)&lt;/td&gt;
&lt;td&gt;Yes (FlexiCapture)&lt;/td&gt;
&lt;td&gt;US-headquartered&lt;/td&gt;
&lt;td&gt;License + services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nanonets&lt;/td&gt;
&lt;td&gt;No-code automation for mid-market teams&lt;/td&gt;
&lt;td&gt;Partial (HITL annotation)&lt;/td&gt;
&lt;td&gt;Enterprise contract only&lt;/td&gt;
&lt;td&gt;US-based multi-tenant&lt;/td&gt;
&lt;td&gt;Per block run ($0.02‚Äì$0.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reducto&lt;/td&gt;
&lt;td&gt;Engineering teams building custom pipelines&lt;/td&gt;
&lt;td&gt;Yes (code-defined schemas)&lt;/td&gt;
&lt;td&gt;Yes (VPC, air-gapped)&lt;/td&gt;
&lt;td&gt;US-based, EU endpoints&lt;/td&gt;
&lt;td&gt;Flat $10 / 1,000 pages for parsing (r-1, Sep 2026); extraction and add-ons priced separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaParse&lt;/td&gt;
&lt;td&gt;Parsing documents into LLM-ready Markdown&lt;/td&gt;
&lt;td&gt;Partial (parsing-first)&lt;/td&gt;
&lt;td&gt;Enterprise self-hosted&lt;/td&gt;
&lt;td&gt;US-based cloud&lt;/td&gt;
&lt;td&gt;Per page (credits); +10 credits per page containing a form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unstructured&lt;/td&gt;
&lt;td&gt;RAG chunking, 71+ connectors&lt;/td&gt;
&lt;td&gt;No (element arrays)&lt;/td&gt;
&lt;td&gt;Yes (self-hosted)&lt;/td&gt;
&lt;td&gt;US-based&lt;/td&gt;
&lt;td&gt;API + self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. anyformat
&lt;/h2&gt;

&lt;p&gt;If you're looking for an AWS Textract alternative that gives you custom-schema extraction with no training data, a workflow and evaluation layer built in rather than assembled from Lambda and Step Functions, and EU-native jurisdiction instead of a US-governed AWS region, anyformat is built for exactly that gap. It's a European document automation platform that covers the whole pipeline a Textract replacement needs: extraction from a schema with no training data, workflow orchestration, human review, and accuracy evaluation, in one product. You define the fields you want and extraction works on the first document.&lt;/p&gt;

&lt;p&gt;In anyformat's published July 2026 benchmark on 1,000+ real documents, anyformat scored 78.1% on parsing quality at $25 per 1,000 pages; Textract scored 65.4% on the same set.&lt;/p&gt;

&lt;p&gt;Every extracted value returns a calibrated confidence score together with the evidence it was read from, the review interface highlights that evidence on the page, and a confidence threshold routes uncertain fields to a person while the rest go straight through. Deployment covers cloud, private cloud, on-premise and fully air-gapped environments, and &lt;a href="https://trust.anyformat.ai" rel="noopener noreferrer"&gt;ISO 27001:2022&lt;/a&gt; certification, GDPR tooling and zero-retention processing are included on every plan with no surcharge.&lt;/p&gt;

&lt;p&gt;If you need to know whether accuracy is holding up over time, anyformat ships that built in: a dataset of verified ground truth, versioned evaluations that score any workflow change field by field against it, and live production accuracy, confidence and through-rate side by side. Optimizer then improves a workflow's field descriptions automatically against that same dataset. On Textract, that measurement harness is something your team builds and maintains.&lt;/p&gt;

&lt;p&gt;Pricing is &lt;a href="https://anyformat.ai/pricing" rel="noopener noreferrer"&gt;credit-based&lt;/a&gt;, charged per page and per operator: Parse 25 credits per page, Extract 35, Classify 10, Split 25, with agentic variants at 100 and 150; on the Business plan 10 credits cost ‚Ç¨0.01, so the $25 per 1,000 pages above is the parsing rate. The free tier includes 50,000 credits with no credit card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need custom fields on &lt;a href="https://anyformat.ai/use-cases/complex-documents" rel="noopener noreferrer"&gt;complex documents&lt;/a&gt; with accuracy they can measure, and a review step their operations team can run without code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Google Document AI
&lt;/h2&gt;

&lt;p&gt;Google Document AI is GCP's document processing service, a natural fit for teams already on Google Cloud with standard document types. Its Enterprise Document OCR supports 200+ languages with handwriting recognition in 50, per Google's documentation, and it integrates tightly with BigQuery and Vertex AI. Custom fields no longer require labeling to get started: the Custom Extractor runs on a foundation model that extracts from a schema alone, with few-shot accepting up to 5 examples and fine-tuning requiring 50 training plus 50 test documents to reach production accuracy. The constraints are elsewhere: system limits cap many online processing requests at 15 pages, deployed custom processor versions carry hosting fees, and there is no on-premise option. In our parsing benchmark it scored 55.0%, with plain OCR at $1.50 per 1,000 pages, the lowest cost among the cloud providers we tested. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-google-document-ai" rel="noopener noreferrer"&gt;anyformat vs Google Document AI&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; GCP-native teams whose documents fit the online page limits and who want custom fields without a labeling project.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Azure Document Intelligence
&lt;/h2&gt;

&lt;p&gt;Azure Document Intelligence, formerly Form Recognizer, is Microsoft's document processing service and the default path for organizations on the Azure stack. Its pretrained models for invoices, receipts, IDs and tax forms are among the strongest cloud offerings: it scored 69.9% in our parsing benchmark, ahead of both Textract (65.4%) and Google Document AI (55.0%). Custom fields require labeled documents in Document Studio, five examples minimum per Microsoft's documentation and usually more, plus a training cycle; container deployment covers only a subset of features, and configuration spreads across Document Studio, the Azure portal and API code. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-azure-document-intelligence" rel="noopener noreferrer"&gt;anyformat vs Azure Document Intelligence&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Microsoft-stack teams whose documents match Azure's pretrained models.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ABBYY
&lt;/h2&gt;

&lt;p&gt;ABBYY is the longest-standing enterprise IDP vendor on this list, founded in 1989 and aimed at large organizations with stable, high-volume document processes. Its catalog lists 150+ pre-built extraction Skills across two products: Vantage (cloud) and FlexiCapture, one of the most mature on-premise capture systems available. The trade-off is the delivery model: implementations typically run weeks to months with third-party integrators, professional services budgets range from $15K to $200K+, and contracts often require 3-year terms with annual prepayment. Documents outside the Skill library need custom model training. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-abbyy" rel="noopener noreferrer"&gt;anyformat vs ABBYY&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Enterprises with stable document portfolios, a hard on-premise requirement, and budget for a traditional deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Nanonets
&lt;/h2&gt;

&lt;p&gt;Nanonets is a mid-market document automation platform, founded in 2017, aimed at teams that want no-code workflows without an engineering project. Its block-based workflow builder (import, data-action and export blocks) is well designed for non-technical users, and published pricing runs $0.02 to $0.30 per block run, with Nanonets' own pricing page putting a typical invoice workflow at four to six block runs per document. Nanonets' documentation notes that out-of-box models may require human-in-the-loop annotation to reach production accuracy on non-standard documents. Default deployment is US-based multi-tenant cloud, on-premise is available only via enterprise contract, and data may be retained for around 30 days after termination. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-nanonets" rel="noopener noreferrer"&gt;anyformat vs Nanonets&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mid-market teams automating standard document types at moderate volume, without strict EU sovereignty requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Reducto
&lt;/h2&gt;

&lt;p&gt;Reducto is a US-based document parsing API, founded around 2023, built for engineering teams that want a high-accuracy parsing primitive with full code control. It has raised $108M in total, most recently a $75M Series B led by a16z in October 2025; it publishes the open RD-TableBench table benchmark and offers Parse, Split, Extract and Edit endpoints with cloud, VPC and on-premise deployment including air-gapped environments. Extraction schemas need no training data, and since 2026 its Studio adds a visual pipeline builder, auto-generated schemas and evaluations on its Growth tier. What it still lacks is a human review queue with reviewer roles and per-field verification status, so the review step is yours to build. Since September 2026 its r-1 model parses at a flat $10 per 1,000 pages. It holds SOC 2 Type II and offers HIPAA processing with a BAA on higher tiers, but does not list ISO 27001, a frequent blocker in European procurement. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-reducto" rel="noopener noreferrer"&gt;anyformat vs Reducto&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Engineering teams replacing Textract's parsing layer inside a custom-built pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. LlamaParse
&lt;/h2&gt;

&lt;p&gt;LlamaParse is LlamaIndex's document parsing API, launched in February 2024, built to turn documents into LLM-ready Markdown for RAG pipelines. Speed is its standout trait: an independent Procycons benchmark measured around 6 seconds for both 1-page and 50-page documents in batch mode. The limits show up in structured extraction: Markdown output flattens merged cells and nested table relationships; confidence scores left beta in August 2026 and are documented as calibrated on three of its five parsing tiers, self-reported without a published evaluation, with no review queue to act on them; the standard offering is US cloud-only with enterprise self-hosting via Kubernetes. It holds SOC 2 Type 2 but does not list ISO 27001. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-llamaparse" rel="noopener noreferrer"&gt;anyformat vs LlamaParse&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Fast RAG ingestion inside the LlamaIndex ecosystem, not field-level extraction into business systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Unstructured
&lt;/h2&gt;

&lt;p&gt;Unstructured is a partially open-source parsing platform that chunks documents into element arrays for RAG pipelines, aimed at AI teams building retrieval systems. It has the widest connector ecosystem in the category, with 71+ connectors including Databricks, Elasticsearch, S3 and Google Drive, and its commercial platform holds SOC 2 Type II, ISO 27001 and HIPAA certifications (the open-source library is not in scope) with a self-hosted deployment option. The key caveat: it does not do field-level structured extraction, so if you need invoice totals or contract dates in your ERP, it solves a different problem. The Procycons 2025 benchmark also measured 51 seconds to process a single page, versus about 6 for the fastest alternatives. See the full &lt;a href="https://anyformat.ai/vs/anyformat-vs-unstructured" rel="noopener noreferrer"&gt;anyformat vs Unstructured&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; RAG preparation with maximum connector coverage; complementary to, not a replacement for, an extraction platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose the right one for your needs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your whole stack is on AWS and your documents are structured forms, Textract remains viable: the switching cost may not be worth it yet.&lt;/li&gt;
&lt;li&gt;Count the full pipeline cost, not just the page price: post-processing, validation, routing, a review UI and monitoring all cost engineering time to build if the vendor doesn't ship them.&lt;/li&gt;
&lt;li&gt;If you need custom fields without training data, anyformat (schemas defined in Studio), Reducto (code-defined) and Google's Custom Extractor all work from a schema alone.&lt;/li&gt;
&lt;li&gt;If you need to measure accuracy over time, look for a vendor with a dataset-backed evaluation tool, not a one-off benchmark number.&lt;/li&gt;
&lt;li&gt;If documents can't leave your infrastructure, anyformat, Reducto, ABBYY FlexiCapture, or a self-hosted Unstructured deployment all support that.&lt;/li&gt;
&lt;li&gt;If EU governing law is a hard requirement, only an EU-headquartered vendor actually changes the jurisdiction. An EU region on a US company doesn't.&lt;/li&gt;
&lt;li&gt;If your ops team needs to own the workflow without engineering, look for a visual builder (anyformat Studio, Nanonets' block editor).&lt;/li&gt;
&lt;li&gt;If you're building for RAG, LlamaParse or Unstructured are built specifically for that output shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best alternative to AWS Textract?&lt;/strong&gt;&lt;br&gt;
It depends on the constraint driving the switch. For an end-to-end platform with zero-shot extraction, workflow orchestration, human review and built-in evaluation, anyformat is the most complete option. For a code-first parsing API, Reducto is the closest fit. Teams consolidated on Google Cloud or Azure usually evaluate their own provider's document service first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best European alternative to AWS Textract?&lt;/strong&gt;&lt;br&gt;
anyformat is the only EU-native platform on this list: European governance, ISO 27001 certification covering the processing pipeline, zero-retention processing, and on-premise deployment including air-gapped environments. US providers offer EU regions, but region selection changes where data sits, not the jurisdiction that governs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there an open-source alternative to AWS Textract?&lt;/strong&gt;&lt;br&gt;
Partially. Unstructured has an open-source core, and self-hosted engines like Docling and PaddleOCR are free to run. In our parsing benchmark both scored below Textract (52.3% and 43.2% versus 65.4%), so budget for accuracy validation and the engineering effort to operate them in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do teams switch away from AWS Textract?&lt;/strong&gt;&lt;br&gt;
Four reasons come up consistently: Textract returns raw OCR that needs a custom post-processing pipeline before applications can use it; it has no workflow or evaluation layer, so orchestration and accuracy monitoring mean assembling Lambda, Step Functions, queues and your own measurement harness; pricing splits across per-page APIs, with combined forms and tables analysis at $65 per 1,000 pages and $70 with queries added (AWS pricing, 2026); and the service is governed under US jurisdiction regardless of region selection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there an on-premise or self-hosted alternative to AWS Textract?&lt;/strong&gt;&lt;br&gt;
Yes: anyformat and Reducto both offer &lt;a href="https://anyformat.ai/use-cases/document-intelligence" rel="noopener noreferrer"&gt;on-premise deployment including air-gapped&lt;/a&gt; environments, ABBYY FlexiCapture is a mature on-premise capture system, and Unstructured can be self-hosted, while Textract itself runs only inside AWS regions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which AWS Textract alternative works without training data?&lt;/strong&gt;&lt;br&gt;
anyformat, Reducto and Google's Custom Extractor extract from a schema alone, while Azure Document Intelligence, ABBYY and Nanonets still require labeled example documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does AWS Textract cost compared with the alternatives?&lt;/strong&gt;&lt;br&gt;
Textract's forms and tables analysis lists at $65 per 1,000 pages; among the alternatives, Reducto parses at a flat $10 per 1,000 pages, anyformat parses at $25 per 1,000 pages with extraction and classification priced separately per operator, and Google's plain OCR lists at $1.50 per 1,000 pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an EU region make AWS Textract GDPR-compliant?&lt;/strong&gt;&lt;br&gt;
An EU region can support GDPR compliance through in-region storage and standard contractual clauses, but it does not change which country's law governs the provider: a US company processing data in an EU region remains subject to US law regardless of where the servers sit. Buyers whose requirement is EU governing jurisdiction, not just GDPR compliance on paper, need an EU-headquartered vendor.&lt;/p&gt;




&lt;p&gt;If you have narrowed the decision down to two options, start with the deep dive: &lt;a href="https://anyformat.ai/vs/anyformat-vs-aws-textract" rel="noopener noreferrer"&gt;anyformat vs AWS Textract&lt;/a&gt; compares extraction approach, workflow orchestration, sovereignty and pricing side by side.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/best-aws-textract-alternatives" rel="noopener noreferrer"&gt;anyformat.ai/blog/best-aws-textract-alternatives&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>data</category>
      <category>tools</category>
    </item>
    <item>
      <title>OCR API vs open source</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:37:54 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/ocr-api-vs-open-source-3gg1</link>
      <guid>https://dev.to/felipe_anyformat/ocr-api-vs-open-source-3gg1</guid>
      <description>&lt;p&gt;Choosing between a managed OCR API and self-hosted open source comes down to one comparison: the monthly cost of the API at your volume, against the cost of running the open-source engine yourself, which is mostly the hours your team spends keeping it working. The prices below are public. The hours are yours, and they decide the answer.&lt;/p&gt;

&lt;p&gt;This page gives the formula, the published API prices as of October 2026, a worked example and a checklist for the parts a formula does not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What managed OCR APIs publish
&lt;/h2&gt;

&lt;p&gt;All prices are US dollars at list price, per 1,000 pages, as read on 1 October 2026. Check the linked page before you decide, since prices change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Price per 1,000 pages&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/textract/pricing/" rel="noopener noreferrer"&gt;Textract&lt;/a&gt;, text&lt;/td&gt;
&lt;td&gt;$1.50 ($0.60 above 1M)&lt;/td&gt;
&lt;td&gt;1,000 pages/month for 3 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Textract, tables&lt;/td&gt;
&lt;td&gt;$15 ($10 above 1M)&lt;/td&gt;
&lt;td&gt;See pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://azure.microsoft.com/en-us/pricing/details/document-intelligence/" rel="noopener noreferrer"&gt;Azure Read&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$1.50 ($0.60 above 1M)&lt;/td&gt;
&lt;td&gt;500 pages/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cloud.google.com/document-ai/pricing" rel="noopener noreferrer"&gt;Google OCR&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$1.50 ($0.60 above 5M)&lt;/td&gt;
&lt;td&gt;First 1,000 pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.mistral.ai/models/ocr-4-1" rel="noopener noreferrer"&gt;Mistral OCR 4.1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;See pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Azure's pricing page did not display figures when we read it; the Azure figures come from Microsoft's &lt;a href="https://prices.azure.com/api/retail/prices" rel="noopener noreferrer"&gt;Retail Prices API&lt;/a&gt; for the East US region. Mistral's batch discount lowers its price further, and many older articles still quote the price of its previous model, so check the current page. Textract and Azure also charge more for tables, forms and prebuilt extraction than for plain text, so compare the same capability across services.&lt;/p&gt;

&lt;h2&gt;
  
  
  What self-hosting costs
&lt;/h2&gt;

&lt;p&gt;Two parts of the cost have a price you can look up, and the rest does not.&lt;/p&gt;

&lt;p&gt;Infrastructure is the part with a public price. As an example, an AWS c6i.xlarge instance (4 vCPUs, 8 GiB) costs about $0.17 an hour on demand in us-east-1, close to $124.80 a month, according to the &lt;a href="https://instances.vantage.sh/aws/ec2/c6i.xlarge" rel="noopener noreferrer"&gt;Vantage instance table&lt;/a&gt;, which mirrors AWS list prices; confirm it on the AWS pricing page. Block storage on gp3 is $0.08 per GB-month on the &lt;a href="https://aws.amazon.com/ebs/pricing/" rel="noopener noreferrer"&gt;EBS pricing page&lt;/a&gt;. A GPU instance such as the g5.xlarge (one NVIDIA A10G) is about $1.006 an hour, close to $734 a month, per the same table.&lt;/p&gt;

&lt;p&gt;The rest has no public price: the engineering time to build it, preprocessing and post-processing code, upgrades, monitoring and on-call, labelling a test set, security work, and a fallback for pages that fail. These are real costs, and they are the reason the decision is not just a comparison of price lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;With P as pages per month and p as the API price per 1,000 pages, the API costs &lt;code&gt;P / 1000 × p&lt;/code&gt; a month. Self-hosting costs the infrastructure plus the people: infrastructure and storage, plus the monthly hours of upkeep times your loaded hourly rate, plus the build hours and the labelling, spread over the months you expect to run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price_per_1000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost_self_hosted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upkeep_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hourly_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;build_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labelling_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Infrastructure, upkeep, and the one-off build and labelling spread over `months`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;one_off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hourly_rate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;labelling_cost&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;infra&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;upkeep_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hourly_rate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;one_off&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;break_even_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upkeep_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;build_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labelling_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hourly rate at which both options cost the same, or None when there is no labour to price.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;labour_hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upkeep_hours&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;build_hours&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;labour_hours&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# compare infrastructure and labelling with the API cost directly
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;monthly_cost_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;infra&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;labelling_cost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;labour_hours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loaded hourly rate is your number: salary, overhead and the opportunity cost of what else those hours could do. This page does not guess it. The code sets the build hours and the labelling cost to zero by default, so a call that leaves them out covers ongoing costs only and understates self-hosting; pass your own values to include the one-off work. With no upkeep and no build hours there is no labour to price, so &lt;code&gt;break_even_rate&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, and the comparison is just infrastructure against the API. A negative result means self-hosting costs more than the API even if the labour were free.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Take 100,000 pages a month of plain text OCR. At $1.50 per 1,000 pages the API costs $150 a month, and at Mistral OCR 4.1's $4 it costs $400. Self-hosting on the c6i.xlarge with 100 GB of storage costs about $132.80 a month in infrastructure ($124.80 plus $8.00).&lt;/p&gt;

&lt;p&gt;The infrastructure alone is almost the same as the cheapest API. So the question is how many hours it takes to keep the engine running. As an illustrative placeholder, assume 20 hours a month of upkeep. Then self-hosting is cheaper than the $1.50 API only if one hour of that work costs less than about $0.86 (the $17.20 difference divided by 20 hours). At that price of labour, self-hosting does not win. Against the $4 API the same arithmetic gives about $13.36 an hour, which is where your own rate starts to matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;infra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;124.80&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;                      &lt;span class="c1"&gt;# c6i.xlarge plus 100 GB of gp3 storage
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;monthly_cost_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;           &lt;span class="c1"&gt;# 150.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                            &lt;span class="c1"&gt;# 132.8
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;break_even_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 0.86
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A GPU changes the picture only at high volume. A g5.xlarge at about $734 a month costs less than a $1.50 API only above roughly 489,000 pages a month, and less than a $4 API above roughly 183,500 pages a month, before any labour. These numbers are illustrations with sourced prices and placeholder hours; replace the hours and the rate with your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the formula leaves out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Document types. Tesseract documents its own limits on skew, low resolution and tables, and a managed service may handle them differently. Measure on your files.&lt;/li&gt;
&lt;li&gt;Data residency. Self-hosting keeps data in your network. Managed APIs often offer region choices, so check each vendor.&lt;/li&gt;
&lt;li&gt;Latency and throughput. Measure them, and note that some vendors price batch and synchronous calls separately.&lt;/li&gt;
&lt;li&gt;Team capacity. Someone has to own preprocessing, upgrades and monitoring.&lt;/li&gt;
&lt;li&gt;Accuracy target. Test 50 of your own documents on each option: pick a mix that covers your best, median and worst scans, label the fields you care about, run all candidates on the same inputs and set the pass threshold before you look at results. The scoring code is in &lt;a href="https://anyformat.ai/blog/is-tesseract-accurate-enough-for-production" rel="noopener noreferrer"&gt;Is Tesseract accurate enough for production?&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where anyformat fits
&lt;/h2&gt;

&lt;p&gt;anyformat is a third option: a platform that returns typed fields with a confidence score and the source text and page, rather than raw OCR text. Its self-hosted and on-premise deployment is an Enterprise option with no public price, so it has no row in the formula above; ask for a quote if that is your case, and run the same 50 documents through it. See &lt;a href="https://anyformat.ai/blog/on-premise-air-gapped-document-extraction" rel="noopener noreferrer"&gt;on-premise and air-gapped document extraction&lt;/a&gt; for how that deployment works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is it cheaper to self-host OCR?&lt;/strong&gt;&lt;br&gt;
Only at high volume or when the hours to run it are small. At $1.50 per 1,000 pages the infrastructure costs about as much as the API for 100,000 pages a month, so labour decides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does a managed OCR API cost?&lt;/strong&gt;&lt;br&gt;
Published list prices for plain text OCR were $1.50 per 1,000 pages at AWS Textract, Azure Document Intelligence and Google Document AI, and $4 at Mistral OCR 4.1, as of 1 October 2026. Tables, forms and prebuilt extraction cost more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does self-hosting make sense beyond cost?&lt;/strong&gt;&lt;br&gt;
When data cannot leave your network, when you need to run offline, or when you need control over the model and its version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the break-even volume?&lt;/strong&gt;&lt;br&gt;
It depends on your hours and hourly rate. Use the formula on this page: the loaded hourly rate at which both options cost the same is the API cost minus the infrastructure, divided by the monthly hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I compare accuracy between options?&lt;/strong&gt;&lt;br&gt;
Test 50 of your own documents, score the fields you care about, run every candidate on the same inputs and set the threshold first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/ocr-api-vs-open-source-self-hosted" rel="noopener noreferrer"&gt;anyformat.ai/blog/ocr-api-vs-open-source-self-hosted&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ocr</category>
      <category>ai</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Is Tesseract accurate enough for production?</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:26:13 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/is-tesseract-accurate-enough-for-production-2c2c</link>
      <guid>https://dev.to/felipe_anyformat/is-tesseract-accurate-enough-for-production-2c2c</guid>
      <description>&lt;p&gt;Tesseract is accurate enough for production when your documents look like the ones it was built for, clean printed pages at a sufficient resolution, and your check on your own files says so. It does not publish an accuracy figure, so nobody can answer the question for you, and the useful move is to measure it on a sample of your own documents before you commit.&lt;/p&gt;

&lt;p&gt;This page covers what Tesseract says about itself, what independent tests found, the conditions that make it fail, and a short test you can run in an afternoon with a go or no-go threshold set in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tesseract says about itself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tesseract-ocr/tesseract" rel="noopener noreferrer"&gt;Tesseract&lt;/a&gt; is an open-source OCR engine under the Apache 2.0 licence. It supports more than 100 languages and uses an LSTM-based engine. Its README gives no accuracy number. It says that to get better results you will often need to improve the quality of the image.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html" rel="noopener noreferrer"&gt;documentation on improving quality&lt;/a&gt; is specific about when accuracy drops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resolution. It works best on images of at least 300 DPI.&lt;/li&gt;
&lt;li&gt;Binarisation. Its default method can be suboptimal when the page background is unevenly dark.&lt;/li&gt;
&lt;li&gt;Skew. Line segmentation quality falls significantly if the page is too skewed.&lt;/li&gt;
&lt;li&gt;Noise. Noise that binarisation cannot remove can cause accuracy to drop, and dark scan borders can be read as characters.&lt;/li&gt;
&lt;li&gt;Tables. The documentation says it has a problem recognising text and data from tables without custom preprocessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its &lt;a href="https://tesseract-ocr.github.io/tessdoc/FAQ.html" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; says of handwriting that you can try, but it will not work very well, because Tesseract is designed for printed text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What independent tests found
&lt;/h2&gt;

&lt;p&gt;Few independent comparisons exist, and each has caveats.&lt;/p&gt;

&lt;p&gt;A peer-reviewed benchmark by Hegghammer, published in the Journal of Computational Social Science in 2021 (&lt;a href="https://doi.org/10.1007/s42001-021-00149-1" rel="noopener noreferrer"&gt;DOI 10.1007/s42001-021-00149-1&lt;/a&gt;), compared Tesseract, Amazon Textract and Google Document AI on scans of English books and Arabic articles, with artificial noise added. It found the two cloud services substantially better than Tesseract, especially on noisy documents. The data are book and article scans from 2021, not invoices.&lt;/p&gt;

&lt;p&gt;A vendor-run benchmark on receipts, published by &lt;a href="https://imagetotable.ai/references/paddleocr-vs-easyocr-receipt-benchmark" rel="noopener noreferrer"&gt;ImageToTable&lt;/a&gt; and last reviewed on 18 August 2026, reports character error rates of 0.3347 for Tesseract on CPU, 0.2833 for EasyOCR and 0.2045 for PaddleOCR on the SROIE and CORD receipt datasets. It is run by a vendor, which says so, and it does not state the Tesseract version or preprocessing, so read it as one data point.&lt;/p&gt;

&lt;p&gt;Neither result is yours. The same engine can do very well on clean scans and badly on skewed phone photos, which is why the test below matters more than any published figure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Tesseract works and when it breaks
&lt;/h2&gt;

&lt;p&gt;It tends to work on clean, upright, printed text at 300 DPI or more with a plain background, such as a modern scanned letter or a typed form. It tends to break on skewed or low-resolution images, uneven backgrounds, noisy scans, tables, and handwriting. Layout analysis is a segmentation step you tune through its page segmentation modes, which range from 0 to 13, so results can change a lot with one setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it on 50 of your own documents
&lt;/h2&gt;

&lt;p&gt;Pick 50 documents that cover your real mix: the best, the median and the worst scans you receive. Hand-label the fields you care about, such as invoice number, date and total, rather than the full text. Run Tesseract on all 50 with the same preprocessing, then score the results. Decide the pass threshold before you run it, so that the result does not shape the target.&lt;/p&gt;

&lt;p&gt;Two measures cover most needs: the character error rate for free text, and an exact match for numbers, dates and identifiers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;edit_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Report the results per document type and count the documents with any wrong critical field, because one wrong total can matter more than a hundred slightly noisy words. Fifty documents give a rough signal, not a guarantee, and a result from a small sample does not replace monitoring on production traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  A go or no-go checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The critical fields pass your threshold on the documents you actually receive, including the worst quarter.&lt;/li&gt;
&lt;li&gt;Pages arrive at 300 DPI or you control the scanning, and you handle skew and background before OCR.&lt;/li&gt;
&lt;li&gt;You do not depend on tables or handwriting, or you have a separate answer for them.&lt;/li&gt;
&lt;li&gt;Someone owns preprocessing, upgrades and monitoring, which a managed service would otherwise carry.&lt;/li&gt;
&lt;li&gt;A wrong value has a cost you can tolerate, or a check or review step catches it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answers are mostly no, you have the options of better preprocessing, a different open-source engine, a managed OCR API or a document processing platform that returns typed fields with confidence. How the cost compares is covered in &lt;a href="https://anyformat.ai/blog/ocr-api-vs-open-source-self-hosted" rel="noopener noreferrer"&gt;OCR API vs open source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where anyformat fits
&lt;/h2&gt;

&lt;p&gt;anyformat is a document extraction platform that sits at the end of that list. It returns typed fields with a confidence score and the source text and page they came from, so a person reviews the uncertain ones. It is not a substitute for the test above: run the same 50 documents through any option you consider, ours included. For the metrics that matter once documents reach production, see &lt;a href="https://anyformat.ai/blog/anyformat-beyond-accuracy-document-ai-metrics" rel="noopener noreferrer"&gt;beyond accuracy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How accurate is Tesseract?&lt;/strong&gt;&lt;br&gt;
Tesseract does not publish an accuracy figure. Its documentation says results depend on image quality, and independent tests show large differences between clean and noisy documents. Measure it on your own files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Tesseract good enough for invoices?&lt;/strong&gt;&lt;br&gt;
It can be, on clean, upright, high-resolution printed invoices. Skew, noise and tables are known weak points, and extracting named fields such as the total needs logic on top of the raw text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is Tesseract giving bad results?&lt;/strong&gt;&lt;br&gt;
Check resolution first (300 DPI or more), then skew, background and noise, then the page segmentation mode. The official documentation lists these in order of impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Tesseract read handwriting?&lt;/strong&gt;&lt;br&gt;
Its FAQ says it can try but will not work very well, because it is designed for printed text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a good character error rate?&lt;/strong&gt;&lt;br&gt;
It depends on your use. For free text you read afterwards, a higher rate can be fine; for amounts and identifiers you need an exact match. Set the threshold per field before you test.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/is-tesseract-accurate-enough-for-production" rel="noopener noreferrer"&gt;anyformat.ai/blog/is-tesseract-accurate-enough-for-production&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ocr</category>
      <category>python</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Best Python OCR libraries</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:15:50 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/best-python-ocr-libraries-179</link>
      <guid>https://dev.to/felipe_anyformat/best-python-ocr-libraries-179</guid>
      <description>&lt;p&gt;The best Python OCR library depends on what your images look like. On clean, upright, high-resolution printed text, Tesseract, EasyOCR and PaddleOCR all returned essentially perfect text in our test, so the choice there is about install size and speed. They differ when the input gets harder: low resolution, rotation, noise, or two columns.&lt;/p&gt;

&lt;p&gt;OCR, optical character recognition, turns pictures of text into text a program can use. You need it when the words you want are pixels and not characters: a scanned contract, a photo of a receipt, a screenshot, or a PDF that was made from a scan. If you can select and copy the text in your PDF, it already has a text layer and you do not need OCR at all, because a parser reads that layer directly. People run OCR to make scans searchable, to feed text to a language model, or to pull values out of invoices and forms.&lt;/p&gt;

&lt;p&gt;This page runs the three on the same synthetic images, shows the code, and says what we did and did not measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or newer.&lt;/li&gt;
&lt;li&gt;A few images of your own, ideally fictional or sample ones, because the examples print the recognised text and text from real client documents can end up in terminal and job logs. Include the ugliest ones you receive. Clean images make every library look good.&lt;/li&gt;
&lt;li&gt;A separate virtual environment per library, because EasyOCR and PaddleOCR each bring a large framework (PyTorch and PaddlePaddle).&lt;/li&gt;
&lt;li&gt;For Tesseract, the Tesseract program itself, not only the Python wrapper. On macOS: &lt;code&gt;brew install tesseract&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytesseract pillow    &lt;span class="c"&gt;# environment 1, plus the Tesseract program&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;easyocr               &lt;span class="c"&gt;# environment 2&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;paddlepaddle paddleocr   &lt;span class="c"&gt;# environment 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1. Run each library on one image
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tesseract-ocr/tesseract" rel="noopener noreferrer"&gt;Tesseract&lt;/a&gt;, through its Python wrapper pytesseract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/JaidedAI/EasyOCR" rel="noopener noreferrer"&gt;EasyOCR&lt;/a&gt;, which downloads about 108 MB of models on the first call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;easyocr&lt;/span&gt;

&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;easyocr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/PaddlePaddle/PaddleOCR" rel="noopener noreferrer"&gt;PaddleOCR&lt;/a&gt;, which downloads about 133 MB on the first call. This is the 3.x API, and we switched off its three optional preprocessing modules to test the bare detection and recognition pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;paddleocr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PaddleOCR&lt;/span&gt;

&lt;span class="n"&gt;ocr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaddleOCR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_doc_orientation_classify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;use_doc_unwarping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_textline_orientation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ocr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rec_texts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we tested
&lt;/h2&gt;

&lt;p&gt;We rendered 13 images with Pillow in Arial from known text: a clean paragraph at 300 DPI, the same text at 100 DPI and at 50 DPI, rotated 3 and 15 and 90 degrees, with heavy noise and blur, low contrast, in two columns, a Spanish paragraph with accents and invoice lines, two fonts that look handwritten, and a small table. We scored each output as one minus the edit distance to the true text divided by its length, so 1.0 is a perfect copy. Versions: Tesseract 5.5.3, EasyOCR 1.7.2, PaddleOCR 3.7.0.&lt;/p&gt;

&lt;p&gt;These are easy images by construction, rendered on one Apple Silicon Mac without a GPU. Treat the scores as a way to see how each library fails, not as a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Tesseract&lt;/th&gt;
&lt;th&gt;EasyOCR&lt;/th&gt;
&lt;th&gt;PaddleOCR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean, 300 DPI&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean, 100 DPI&lt;/td&gt;
&lt;td&gt;0.995&lt;/td&gt;
&lt;td&gt;0.979&lt;/td&gt;
&lt;td&gt;0.989&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean, 50 DPI&lt;/td&gt;
&lt;td&gt;0.505&lt;/td&gt;
&lt;td&gt;0.245&lt;/td&gt;
&lt;td&gt;0.957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotated 3 degrees, noise and blur&lt;/td&gt;
&lt;td&gt;0.989&lt;/td&gt;
&lt;td&gt;0.479&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotated 15 degrees&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.223&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotated 90 degrees&lt;/td&gt;
&lt;td&gt;0.165&lt;/td&gt;
&lt;td&gt;0.096&lt;/td&gt;
&lt;td&gt;0.106&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy noise and blur&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.202&lt;/td&gt;
&lt;td&gt;0.851&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low contrast&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.989&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two columns&lt;/td&gt;
&lt;td&gt;0.489&lt;/td&gt;
&lt;td&gt;0.489&lt;/td&gt;
&lt;td&gt;0.489&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish and invoice lines&lt;/td&gt;
&lt;td&gt;0.993&lt;/td&gt;
&lt;td&gt;0.993&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small table, read as text&lt;/td&gt;
&lt;td&gt;0.227&lt;/td&gt;
&lt;td&gt;0.943&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  On clean text the choice is about weight
&lt;/h3&gt;

&lt;p&gt;All three read the clean 300 DPI paragraph perfectly. Tesseract took 0.1 to 0.3 seconds per image on our machine against 1 to 5 seconds for the others, and its environment was about 36 MB against roughly 800 MB for EasyOCR and PaddleOCR, plus the Tesseract program. The first run of EasyOCR and PaddleOCR also took around 52 seconds each, because they download models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tesseract is sensitive to input quality
&lt;/h3&gt;

&lt;p&gt;At 15 degrees of rotation and under heavy noise and blur, Tesseract returned nothing useful (0.000), and at 50 DPI it scored 0.505, while PaddleOCR scored 1.000, 0.851 and 0.957 on the same images. Tesseract's own documentation says the same thing: it works best at 300 DPI or more, and skew reduces line segmentation quality significantly. We did not try preprocessing such as deskewing or upscaling, which is exactly what its documentation recommends, so the numbers are for raw input. Tesseract also correctly reported the 90 degree rotation through its orientation detection, so a rotate step would fix that case.&lt;/p&gt;

&lt;h3&gt;
  
  
  EasyOCR loses reading order on noisy lines
&lt;/h3&gt;

&lt;p&gt;EasyOCR scored 0.479 on the slightly rotated, blurred paragraph, mostly because it returned the line fragments in the wrong order and not because it misread them. It also scored 0.245 at 50 DPI and 0.202 under heavy noise. It had its latest release in September 2024 at the time of writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  None of them solves layout
&lt;/h3&gt;

&lt;p&gt;All three read across both columns line by line, interleaving the left and right text, so the content was complete but the order was wrong (0.489 for all three). None returns table structure, only text. Tesseract's default mode returned a single row of the table. If your job is tables or multi-column documents, these libraries give you text, not structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spanish needs setup
&lt;/h3&gt;

&lt;p&gt;The Homebrew build of Tesseract ships only English. With English alone it scored 0.967 on the Spanish paragraph but dropped nine accented words, among them número, Málaga and dirección, a failure that the overall score hides. Adding the Spanish language file (&lt;code&gt;spa.traineddata&lt;/code&gt;, about 2.3 MB from the tessdata_fast repository) fixed it, with 0.993 and no missed accented words. PaddleOCR and EasyOCR missed none and one respectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we did not test
&lt;/h3&gt;

&lt;p&gt;We did not test real handwriting: the two fonts we used only look handwritten, so we draw no conclusion. Tesseract's FAQ says handwriting will not work very well because it is designed for printed text, and EasyOCR's README lists handwriting as a future item. We did not test real scans, preprocessing, GPU speed or non-Latin scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check on your own images
&lt;/h2&gt;

&lt;p&gt;Write down the text you expect and compute the same score on your files. The function reports one number per image and never prints the text, so the output is safe to log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;edit_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install and licence notes
&lt;/h2&gt;

&lt;p&gt;All three are Apache 2.0 licensed. PaddleOCR 3.7.0 was released in June 2026 and EasyOCR 1.7.2 in September 2024. EasyOCR lists more than 80 languages and recommends a GPU without requiring one, and PaddleOCR's README lists more than 100 languages and support for CPU and GPU. Tesseract supports more than 100 languages and needs no GPU.&lt;/p&gt;

&lt;p&gt;To compare these libraries with managed OCR APIs on cost, see &lt;a href="https://anyformat.ai/blog/ocr-api-vs-open-source-self-hosted" rel="noopener noreferrer"&gt;OCR API vs open source&lt;/a&gt;. To test accuracy on your own files, see &lt;a href="https://anyformat.ai/blog/is-tesseract-accurate-enough-for-production" rel="noopener noreferrer"&gt;Is Tesseract accurate enough for production?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When these libraries are not enough: anyformat
&lt;/h2&gt;

&lt;p&gt;They return text. When you need typed fields (an invoice number, a total), structure for tables, or a confidence score you can act on, you need a layer on top: your own extraction code, or a document processing service. anyformat is one of the latter; it returns typed fields with a confidence score and the source text and page. We did not run it in this comparison, so this page makes no claim about how it scores on these images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best Python OCR library?&lt;/strong&gt;&lt;br&gt;
It depends on the images. On clean 300 DPI printed text, Tesseract, EasyOCR and PaddleOCR were equally accurate in our test, and Tesseract was the lightest and fastest. On rotated, noisy or low-resolution images, PaddleOCR scored highest in our test without preprocessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Tesseract better than EasyOCR?&lt;/strong&gt;&lt;br&gt;
On clean text they matched. Tesseract was faster and lighter, and EasyOCR did better on the small table read as text, but lost reading order on the rotated, blurred paragraph. Neither returns table structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a GPU for OCR in Python?&lt;/strong&gt;&lt;br&gt;
No. We ran all three on CPU. EasyOCR says a GPU is recommended but not required, and PaddleOCR supports both. We did not measure GPU speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is Tesseract missing accents in Spanish?&lt;/strong&gt;&lt;br&gt;
The default Homebrew install includes English only. Add the Spanish language file and pass &lt;code&gt;lang="spa"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can these libraries read handwriting?&lt;/strong&gt;&lt;br&gt;
We did not test real handwriting. Tesseract's FAQ says it is designed for printed text.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/best-python-ocr-library" rel="noopener noreferrer"&gt;anyformat.ai/blog/best-python-ocr-library&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ocr</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Extract tables from a PDF</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:13:45 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/extract-tables-from-a-pdf-2d0h</link>
      <guid>https://dev.to/felipe_anyformat/extract-tables-from-a-pdf-2d0h</guid>
      <description>&lt;p&gt;Extracting a table from a PDF means getting its rows and columns back as data, a list of rows or a pandas DataFrame, instead of a pile of text. It is harder than it looks, because a PDF stores characters at positions on a page and has no concept of a row or a column. A library has to infer the table from the lines drawn around the cells or from how the words line up.&lt;/p&gt;

&lt;p&gt;You usually want this to load figures into a spreadsheet or database, to feed a table into an analysis, or to give a language model a table it can read. This page runs the three most used Python libraries on the same five fictional PDFs, shows the code that worked, and reports where each one broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or newer.&lt;/li&gt;
&lt;li&gt;One PDF with a table in it, ideally a fictional or sample document, because the examples print what they find and text from real client documents can end up in terminal and job logs. Use the hardest one you have: no ruling lines, merged header cells or a table that runs across pages. A tidy ruled table makes every library look good.&lt;/li&gt;
&lt;li&gt;One virtual environment per library, so their dependencies do not collide:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pdfplumber       &lt;span class="c"&gt;# environment 1&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;camelot-py       &lt;span class="c"&gt;# environment 2&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pymupdf          &lt;span class="c"&gt;# environment 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1. Check that the PDF has a text layer
&lt;/h2&gt;

&lt;p&gt;All three libraries read the text stored in the PDF. A scan is an image with no text layer, and each of them returned an empty list on our scanned test file without raising an error. Check every page first, because a text PDF can have a short cover page or a blank one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pages_without_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;number&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pages_without_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages with little or no text (possible scans):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages_without_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;every page has a text layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page listed there is worth a look rather than a verdict, since a cover or a blank page also has little text. If a page with a table has no text layer, none of the code below will work and you need OCR or a layout-aware parser first; see &lt;a href="https://anyformat.ai/blog/best-python-ocr-library" rel="noopener noreferrer"&gt;the best Python OCR libraries&lt;/a&gt; for the OCR options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Extract the table with each library
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jsvine/pdfplumber" rel="noopener noreferrer"&gt;pdfplumber&lt;/a&gt;, the lightest of the three, reads ruled tables by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;extract_tables&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# a list of rows
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no table found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://camelot-py.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Camelot&lt;/a&gt; returns a pandas DataFrame, with &lt;code&gt;lattice&lt;/code&gt; for ruled tables and &lt;code&gt;stream&lt;/code&gt; for borderless ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;camelot&lt;/span&gt;

&lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flavor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lattice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://pymupdf.readthedocs.io/en/latest/page.html#Page.find_tables" rel="noopener noreferrer"&gt;PyMuPDF&lt;/a&gt; finds tables with &lt;code&gt;find_tables&lt;/code&gt;, and the result converts to a DataFrame too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tabs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;find_tables&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c1"&gt;# a list of rows
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;p&gt;We generated five fictional PDFs with a known table: one with ruling lines, one without any, one with merged header cells and a multi-line cell, one that runs across two pages with a repeated header, and one that is a scan. We compared every output cell by cell against the table we generated. The versions were pdfplumber 0.11.10, Camelot 2.0.0 and PyMuPDF 1.28.2, and each library was run in its default mode and in its text-based mode.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;pdfplumber&lt;/th&gt;
&lt;th&gt;Camelot&lt;/th&gt;
&lt;th&gt;PyMuPDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ruled table&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ruling lines, default mode&lt;/td&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ruling lines, text mode&lt;/td&gt;
&lt;td&gt;Right cells plus blank rows&lt;/td&gt;
&lt;td&gt;Right cells plus a title row&lt;/td&gt;
&lt;td&gt;Right cells plus blank rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged header, multi-line cell&lt;/td&gt;
&lt;td&gt;Correct in default mode; broken in text mode&lt;/td&gt;
&lt;td&gt;Correct in lattice; broken in stream&lt;/td&gt;
&lt;td&gt;Correct in default mode; broken in text mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two pages&lt;/td&gt;
&lt;td&gt;Two tables, header repeated&lt;/td&gt;
&lt;td&gt;Two tables, header repeated&lt;/td&gt;
&lt;td&gt;Two tables, header repeated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scan, no text layer&lt;/td&gt;
&lt;td&gt;Empty list, no warning&lt;/td&gt;
&lt;td&gt;Empty list, no warning&lt;/td&gt;
&lt;td&gt;Empty list, no warning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is five small, clean, generated files, so it shows how each library fails and says nothing about how often it fails on your documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ruling lines decide everything in the default modes
&lt;/h3&gt;

&lt;p&gt;The default mode of each library is the same idea with a different name: find the lines drawn around the cells and read the text inside them. On the ruled table all three were correct. On the table with no ruling lines all three returned nothing, and on that one you have to switch to the text-based strategy, which guesses cells from word positions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The text-based mode works but needs cleaning
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;vertical_strategy&lt;/code&gt; and &lt;code&gt;horizontal_strategy&lt;/code&gt; set to &lt;code&gt;"text"&lt;/code&gt;, pdfplumber found the right cells but added a blank row between every row of the table (11 rows where 6 were expected):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;extract_tables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vertical_strategy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;horizontal_strategy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# drop the blank rows
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After dropping empty rows the table was correct. PyMuPDF's text strategy gave the same blank rows, and Camelot's &lt;code&gt;stream&lt;/code&gt; mode added the page title as a first row. On the table with merged header cells and a multi-line cell, the text modes broke: the cell text was split across rows and only one of five rows came out intact in pdfplumber and PyMuPDF. PyMuPDF also cut "reorder monthly" to "reorder mont" in that run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merged cells and page breaks are not handled for you
&lt;/h3&gt;

&lt;p&gt;A merged header such as "Sales" over two sub-columns comes out as "Sales" followed by an empty cell, with the sub-headers on a second row. A table that runs across two pages comes back as two tables, each with its own copy of the header, and none of the three libraries joins them. You concatenate the pieces and drop the repeated header yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  A scan returns an empty list, not an error
&lt;/h3&gt;

&lt;p&gt;On the scanned file every library returned an empty result without a warning, which is easy to miss in a pipeline. That is why Step 1 checks for a text layer first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same test on a real financial PDF
&lt;/h2&gt;

&lt;p&gt;The video runs the three libraries on a real financial document from the public &lt;a href="https://huggingface.co/datasets/llamaindex/ParseBench" rel="noopener noreferrer"&gt;ParseBench dataset&lt;/a&gt;: two tables with 45 companies, multi-line names and merged cells. Every row was checked against the ground truth. The results shown in the video were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Rows correct&lt;/th&gt;
&lt;th&gt;What went wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pdfplumber&lt;/td&gt;
&lt;td&gt;51.1%&lt;/td&gt;
&lt;td&gt;Drops names in merged cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camelot, lattice&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;No ruling lines to detect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camelot, stream&lt;/td&gt;
&lt;td&gt;88.9%&lt;/td&gt;
&lt;td&gt;Merges the two tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyMuPDF&lt;/td&gt;
&lt;td&gt;82.2%&lt;/td&gt;
&lt;td&gt;Scrambles text in complex cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anyformat&lt;/td&gt;
&lt;td&gt;95.6%&lt;/td&gt;
&lt;td&gt;43 of 45 companies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is one document and one run each, so read it as a pattern and not as a benchmark. The pattern matches the synthetic test above: ruling lines decide the default modes, and the text-based modes keep more rows but damage them.&lt;/p&gt;

&lt;p&gt;None of the three libraries tells you that a row is wrong. They return a table either way, so a damaged cell looks exactly like a correct one, and that is why the check below is worth writing. anyformat returns the same table with a confidence score per field and the source text each value was read from, which is what lets a person check only the doubtful rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check on your own PDFs
&lt;/h2&gt;

&lt;p&gt;Write down one row you know is in your table and test for it. The function reports only which check failed, never the cell contents, so its output is safe to log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;expected_row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no table found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;expected_row&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected row is not intact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;North&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1200&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1350&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1480&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for each library on the same file and you have a comparison for your documents, which is worth more than ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the test
&lt;/h2&gt;

&lt;p&gt;The five PDFs come from a short script, so you can rerun everything. It needs &lt;code&gt;pip install reportlab&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib.pagesizes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.platypus&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TableStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimpleDocTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Paragraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Spacer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reportlab.lib.styles&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getSampleStyleSheet&lt;/span&gt;

&lt;span class="n"&gt;hdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;North&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1200&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1350&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1480&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;South&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;980&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1010&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;East&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1620&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1710&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;West&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;760&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;810&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;905&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4440&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4790&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5195&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FONTSIZE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALIGN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RIGHT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
             &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BOTTOMPADDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOPPADDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BACKGROUND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightgrey&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colWidths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TableStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nc"&gt;SimpleDocTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagesize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Paragraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sales by region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getSampleStyleSheet&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Heading2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a_ruled.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b_borderless.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; Notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;H1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;H2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Widget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Best seller&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;reorder monthly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gadget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discontinued&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;in Q4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gizmo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;60&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;70&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colWidths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TableStyle&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPAN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPAN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPAN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALIGN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CENTER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALIGN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MIDDLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]))&lt;/span&gt;
&lt;span class="nc"&gt;SimpleDocTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c_merged.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagesize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nb"&gt;long&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colWidths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repeatRows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TableStyle&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BACKGROUND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightgrey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FONTSIZE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)]))&lt;/span&gt;
&lt;span class="nc"&gt;SimpleDocTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d_twopage.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagesize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the scan, render the ruled PDF to an image and save it back as a PDF with no text layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a_ruled.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get_pixmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dpi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pixmap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;e_scan.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install and licence notes
&lt;/h2&gt;

&lt;p&gt;Camelot is the heaviest install of the three, around 157 MB with OpenCV, against about 42 MB for pdfplumber and 53 MB for PyMuPDF in our environments. Camelot 2.0.0 no longer needs Ghostscript by default and converts pages through pypdfium2. Older 0.x versions, which most tutorials still cover, failed on every default call with "Ghostscript is not installed" in our run, even with the &lt;code&gt;gs&lt;/code&gt; binary on the path, so install the current version.&lt;/p&gt;

&lt;p&gt;pdfplumber is MIT licensed and Camelot is MIT licensed. PyMuPDF is dual licensed under AGPL 3.0 or a commercial license from Artifex, which matters if you ship closed-source software; read its &lt;a href="https://pymupdf.readthedocs.io/en/latest/about.html" rel="noopener noreferrer"&gt;licensing page&lt;/a&gt; for your case. All three had a release within the last four months on 2026-10-01.&lt;/p&gt;

&lt;h2&gt;
  
  
  When these libraries are not enough: anyformat
&lt;/h2&gt;

&lt;p&gt;These libraries fit PDFs with a text layer and tables that are ruled or cleanly aligned. They stop fitting when tables have no lines, merged cells, multi-line cells or page breaks, or when the files are scans. For those cases people turn to a layout-aware parser, a vision language model or an OCR step that finds table structure.&lt;/p&gt;

&lt;p&gt;anyformat's parsing is one option. Its parse result lists a document's blocks, and table blocks carry their rows, according to the SDK reference. We did not run it on the five synthetic files; in the video it ran on the real financial PDF above and got 43 of 45 companies right, which is one document and one run. For long tables specifically, see &lt;a href="https://anyformat.ai/blog/anyformat-long-documents-extraction" rel="noopener noreferrer"&gt;long-document extraction&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the test
&lt;/h2&gt;

&lt;p&gt;The video version of this test runs about five minutes and covers the three libraries and the real-PDF results above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=yf3IP0Bx1oI" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=yf3IP0Bx1oI&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best Python library to extract tables from a PDF?&lt;/strong&gt;&lt;br&gt;
It depends on the table. For ruled tables in text-based PDFs all three worked in our test, and pdfplumber was the lightest to install. For tables without ruling lines, none of the defaults worked and every library needed its text-based mode plus cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can pdfplumber extract tables from a scanned PDF?&lt;/strong&gt;&lt;br&gt;
No. It reads the text layer, and a scan has none, so it returns an empty list. You need OCR or a layout-aware parser first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get a PDF table into a pandas DataFrame?&lt;/strong&gt;&lt;br&gt;
Camelot returns one directly (&lt;code&gt;tables[0].df&lt;/code&gt;), PyMuPDF converts with &lt;code&gt;to_pandas()&lt;/code&gt;, and with pdfplumber you pass the list of rows to &lt;code&gt;pandas.DataFrame&lt;/code&gt;, using the first row as the header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my table come out with blank rows?&lt;/strong&gt;&lt;br&gt;
The text-based strategies in pdfplumber and PyMuPDF find the cell edges from word positions and can add empty rows between real ones. Drop the rows where every cell is empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I extract a table that spans several pages?&lt;/strong&gt;&lt;br&gt;
Each library returns one table per page. Concatenate the tables and drop the repeated header rows yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/extract-tables-from-pdf" rel="noopener noreferrer"&gt;anyformat.ai/blog/extract-tables-from-pdf&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>ai</category>
      <category>rag</category>
    </item>
    <item>
      <title>PDF to markdown for LLMs</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:12:05 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/pdf-to-markdown-for-llms-o2e</link>
      <guid>https://dev.to/felipe_anyformat/pdf-to-markdown-for-llms-o2e</guid>
      <description>&lt;p&gt;Converting a PDF to markdown for an LLM means turning pages into text that keeps the structure a model needs: headings as headings, tables as tables, columns in reading order. Markdown is the usual target because models read it well, it is usually lighter than HTML, and a RAG pipeline can split it on headings instead of on arbitrary character counts.&lt;/p&gt;

&lt;p&gt;You do this for one of three reasons: to index documents for retrieval, to paste a document into a prompt without losing its tables, or to give an agent a file it can read. Plain text extraction is not enough for any of the three, because a table flattened into one cell per line is worse than no table.&lt;/p&gt;

&lt;p&gt;This page runs three open-source converters on the same PDFs, shows the code, and reports what came out. It also says what we did not test, which includes anyformat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or newer and about 4 GB of free disk if you install all three, because Docling and Marker download models on first use.&lt;/li&gt;
&lt;li&gt;One PDF of your own, ideally a sample or fictional document rather than a real client file. Use the ugliest one you have: a table, two columns, or a scan. Converters look the same on clean single-column text.&lt;/li&gt;
&lt;li&gt;A terminal. The three open-source tools need no API keys.&lt;/li&gt;
&lt;li&gt;One virtual environment per tool. We installed each one separately, so their dependencies do not collide:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"markitdown[pdf]"&lt;/span&gt;   &lt;span class="c"&gt;# environment 1&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;docling             &lt;span class="c"&gt;# environment 2&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;marker-pdf          &lt;span class="c"&gt;# environment 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How we tested
&lt;/h2&gt;

&lt;p&gt;We generated three fictional PDFs: a two-page report (headings, a bulleted list, a 5 by 4 table with a header row, then a page in two columns), a scan-style PDF with no text layer (a slightly rotated image of three sentences and a small table), and a 40-page document. We ran the minimal snippet from each project's README on an Apple Silicon Mac with no GPU, on 2026-09-30. The versions were MarkItDown 0.1.8, Docling 2.131.0 and Marker 2.0.0.&lt;/p&gt;

&lt;p&gt;This is one machine and small synthetic files, each timed once. Treat it as a way to see the failure modes, not as a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Run the three converters
&lt;/h2&gt;

&lt;p&gt;Each one is a few lines. &lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt;, from Microsoft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text_content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/docling-project/docling" rel="noopener noreferrer"&gt;Docling&lt;/a&gt;, from IBM Research:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;docling.document_converter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DocumentConverter&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DocumentConverter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_to_markdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/datalab-to/marker" rel="noopener noreferrer"&gt;Marker&lt;/a&gt;, from Datalab. Put it under a main guard, because on multi-page PDFs it starts worker processes and, without the guard, re-runs the top of your script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;marker.converters.pdf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PdfConverter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;marker.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_model_dict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;marker.output&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;text_from_rendered&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artifact_dict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;create_model_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text_from_rendered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MarkItDown&lt;/th&gt;
&lt;th&gt;Docling&lt;/th&gt;
&lt;th&gt;Marker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;Broken: one cell per line, columns mixed&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two columns&lt;/td&gt;
&lt;td&gt;Correct order, lines hard-wrapped&lt;/td&gt;
&lt;td&gt;Correct order, paragraphs unwrapped&lt;/td&gt;
&lt;td&gt;Correct order, one heading dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading levels&lt;/td&gt;
&lt;td&gt;None kept&lt;/td&gt;
&lt;td&gt;All headings came out as &lt;code&gt;##&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;##&lt;/code&gt; or &lt;code&gt;###&lt;/code&gt;, levels flattened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanned page&lt;/td&gt;
&lt;td&gt;Empty output&lt;/td&gt;
&lt;td&gt;Read, table correct&lt;/td&gt;
&lt;td&gt;Crashed without an extra install (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40 pages, second run&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;td&gt;about 3 s&lt;/td&gt;
&lt;td&gt;about 3.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First run&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;41 s, plus 28 s to initialise&lt;/td&gt;
&lt;td&gt;36 s, then about 7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install and models&lt;/td&gt;
&lt;td&gt;150 MB&lt;/td&gt;
&lt;td&gt;1.1 GB plus about 570 MB of models&lt;/td&gt;
&lt;td&gt;about 1 GB plus 418 MB of models&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Times come from one Apple Silicon Mac with no GPU, single runs, on 2026-09-30.&lt;/p&gt;

&lt;h3&gt;
  
  
  MarkItDown is fast and only good on clean text
&lt;/h3&gt;

&lt;p&gt;It converted two pages in 0.04 seconds and kept the two-column reading order. It kept no headings, turned bullet glyphs into &lt;code&gt;(cid:127)&lt;/code&gt; lines, and scrambled the table: each cell became its own paragraph and the column order was mixed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Region Q1 Q2 Q3
North
100
120
140
South
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the scanned PDF it returned an empty string. MarkItDown's README describes a separate &lt;code&gt;markitdown-ocr&lt;/code&gt; plugin that sends images to an LLM with vision; we did not test it. For clean single-column text it is the cheapest option by a wide margin. For tables or scans it is not enough on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docling gave the best output here, at a cost
&lt;/h3&gt;

&lt;p&gt;Docling returned the table as a correct markdown table, the list as a list and the two columns in order with paragraphs unwrapped. It also ran OCR on the scan with no configuration, returning the three sentences and a correct table. We compared by eye against the source and saw no errors; that is an observation, not an accuracy score.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Region   |   Q1 |   Q2 |   Q3 |
|----------|------|------|------|
| North    |  100 |  120 |  140 |
| South    |  107 |  125 |  143 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is about 1.1 GB of packages and about 570 MB of models, roughly 70 seconds the first time (model download and initialisation), then a few seconds per document. It also flattened heading levels: every heading came out as &lt;code&gt;##&lt;/code&gt;, so a title and its subsections look the same. If your chunker splits on heading depth, that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marker: check your install before you trust it
&lt;/h3&gt;

&lt;p&gt;Marker read the text PDFs, with a correct table and correct two-column order, apart from one heading it dropped on the two-column page. Two things went wrong in our run, and both are worth knowing.&lt;/p&gt;

&lt;p&gt;First, on a Mac without a GPU, Marker 2.0.0 could not convert the scanned page. It stopped with &lt;code&gt;SpawnError: llama-server binary not found&lt;/code&gt;. Its README says the CPU and Apple Silicon mode needs the &lt;code&gt;llama-server&lt;/code&gt; binary from llama.cpp installed separately. We did not install it, so we have no Marker result on scans and we are not claiming one.&lt;/p&gt;

&lt;p&gt;Second, on a 40-page PDF where every section had identical body text, Marker returned 701 characters and dropped the body of sections 2 to 40. We suspect its repeated-header removal, but we did not confirm that, so treat it as something we observed, not explained. On a 40-page PDF with unique text per section all 40 sections survived, but only 42 of 81 headings came through, with levels flattened to &lt;code&gt;###&lt;/code&gt; and &lt;code&gt;####&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read the licenses before you ship any of them. &lt;a href="https://github.com/datalab-to/marker" rel="noopener noreferrer"&gt;Marker's README&lt;/a&gt; says the code is Apache 2.0 and that the model weights use "a modified AI Pubs Open Rail-M license (free for research, personal use, and startups under $5M funding/revenue)", with a commercial route beyond that through the vendor. &lt;a href="https://github.com/docling-project/docling" rel="noopener noreferrer"&gt;Docling's&lt;/a&gt; code is MIT, and its README sends you to each model's own license. MarkItDown is MIT.&lt;/p&gt;

&lt;h3&gt;
  
  
  What none of them gave us
&lt;/h3&gt;

&lt;p&gt;No converter here preserved true heading levels, and none returns where on the page a block came from or how sure it is of it. For a RAG chunker that is usually fine. For anything where a reviewer must see the source, or where you decide automatically what to trust, you need that information, which is what the anyformat section below describes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check on your own PDFs
&lt;/h2&gt;

&lt;p&gt;Do not pick from our table. Write down a few things you know are in your document and test for them. It takes ten minutes and tells you more than any benchmark. The function below reports only which check failed, never the document's content, so its output is safe to log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;must_contain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;table_row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;must_contain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected text #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# a table row survives if a markdown line starting with | has exactly these cells
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_table_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;table_row&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_table_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table row is not intact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quarterly Operations Report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;North&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;140&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for each converter on the same file and you have your own comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Clean, single-column text, where speed matters more than structure: MarkItDown.&lt;/li&gt;
&lt;li&gt;Tables, scans and multi-column layouts, if you can afford the install: Docling.&lt;/li&gt;
&lt;li&gt;Marker, if you have a GPU or the llama.cpp dependency in place and you have read the model license. Test it on long documents first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Another way to do it: anyformat
&lt;/h2&gt;

&lt;p&gt;anyformat is a document extraction platform, and parsing is its first step, available on its own. You send a PDF and get markdown back from the API, with no models to install and no GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anyformat.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anyformat.sdk.errors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StillParsing&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;StillParsing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it with &lt;code&gt;pip install anyformat&lt;/code&gt;. The markdown that comes back is structured: it contains anchors that identify each block of the page. The full parse result also lists those blocks with their page, bounding box and a confidence score, which is how you keep the link from a piece of text back to where it sits on the page. This example fetches only the markdown string; the block output is described in the API docs. The response shape is in the &lt;a href="https://docs.anyformat.ai" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;. The same parse is reachable from an agent through the &lt;a href="https://anyformat.ai/blog/anyformat-ai-mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We did not run anyformat in the test above, so this page makes no claim about how its output compares on these files. Run the check from the section above on the markdown it returns and compare for yourself, using a fictional document, since real files can contain client data. If your documents are clean and you only need text for an index, a local open-source converter is the simpler choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best PDF to markdown converter for RAG?&lt;/strong&gt;&lt;br&gt;
It depends on your documents. On our test files Docling gave the most complete output, MarkItDown was the fastest and only reliable on clean text, and Marker needed extra setup for scans. Test on your own PDFs with a check like the one above before you pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does MarkItDown do OCR?&lt;/strong&gt;&lt;br&gt;
Not for a plain PDF conversion. Its README describes a separate &lt;code&gt;markitdown-ocr&lt;/code&gt; plugin that uses an LLM with vision to read images, and an option that uses Azure Document Intelligence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Marker free for commercial use?&lt;/strong&gt;&lt;br&gt;
Its code is Apache 2.0. The model weights use a modified AI Pubs Open Rail-M license that its README describes as free for research, personal use and startups under $5M funding or revenue. Read the license in its repository for your case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do tables break when converting a PDF to markdown?&lt;/strong&gt;&lt;br&gt;
A PDF stores characters at positions on a page, not rows and columns. A converter that only reads the text in order turns each cell into its own line. One that detects the table structure can rebuild the rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a GPU?&lt;/strong&gt;&lt;br&gt;
Not for the three tools we ran: all three converted text PDFs on a Mac without a GPU. Marker's README describes a GPU mode with higher accuracy, and its scanned-page mode on a Mac needed an extra binary, as described above.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/pdf-to-markdown-for-llms" rel="noopener noreferrer"&gt;anyformat.ai/blog/pdf-to-markdown-for-llms&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>rag</category>
    </item>
    <item>
      <title>Extract structured data from a PDF with an LLM</title>
      <dc:creator>Felipe Cardona</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:10:26 +0000</pubDate>
      <link>https://dev.to/felipe_anyformat/extract-structured-data-from-a-pdf-with-an-llm-5bnh</link>
      <guid>https://dev.to/felipe_anyformat/extract-structured-data-from-a-pdf-with-an-llm-5bnh</guid>
      <description>&lt;p&gt;Extracting structured data from a PDF with an LLM means reading the document's text, describing the fields you want as a schema, and asking the model to return values that fit that schema as JSON. It works on layouts the model has never seen, which is why it replaced per-supplier templates. It also fails in predictable ways, and most of the work in production is catching those failures.&lt;/p&gt;

&lt;p&gt;Most people do this for one of four reasons: to build a RAG over their own documents, to load the values into a database or spreadsheet, to trigger a workflow when a document arrives (an invoice to approve, a form to route), or to give an agent facts to work with instead of pages to read. In every case the point is the same: turn a document into fields a program can use.&lt;/p&gt;

&lt;p&gt;This page does it end to end in Python: one working script, then the five places it breaks, then what to add so you can trust the output. The example is an invoice because everyone has one, but nothing below is specific to invoices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or newer.&lt;/li&gt;
&lt;li&gt;An API key for the LLM you want to call. The example uses OpenAI; swap in Anthropic or Gemini if you prefer.&lt;/li&gt;
&lt;li&gt;One PDF, ideally a fictional or sample invoice, because the examples print results and you should not send real client documents to logs or to a model you have not approved. Use the most complex one you have: a multi-page document with a table, a scan, or two columns. A clean one-page invoice makes every approach look good and hides the failures this page is about.&lt;/li&gt;
&lt;li&gt;Five minutes to install the libraries:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pymupdf pydantic openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1. Get the text out of the PDF
&lt;/h2&gt;

&lt;p&gt;A PDF is a drawing instruction set, not a text file, so the first step is turning pages into text the model can read. If the PDF has a text layer, a library such as &lt;a href="https://pymupdf.readthedocs.io" rel="noopener noreferrer"&gt;PyMuPDF&lt;/a&gt; reads it directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pdf_to_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--- page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the page markers matters later: they let you point at where a value came from. If this returns almost nothing, the PDF is a scan and has no text layer. That is failure 1 below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Describe what you want as a schema
&lt;/h2&gt;

&lt;p&gt;A schema is a typed description of the output: field names, types and what each field means. Declaring it forces the model to return a value of the right shape instead of writing prose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description the model reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;invoice_number&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Invoice ID as printed, usually near the top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;issued_on&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;Issue date, not the due date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;supplier_name&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Who issued the invoice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currency&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;ISO code, for example EUR or USD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;purchase_order&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Purchase order number, only if one is printed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tax&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;Total tax amount, only if shown separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;Grand total including tax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;line_items&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;One entry per row in the table: description, quantity, unit price, amount before tax&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same thing written as code with &lt;a href="https://docs.pydantic.dev" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt;, which is what you paste into the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Line amount before tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice ID as printed, usually near the top&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;issued_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Issue date, not the due date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;supplier_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who issued the invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ISO 4217 code, e.g. EUR, USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;purchase_order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Purchase order number, only if one is printed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total tax amount, only if shown separately&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Grand total including tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The descriptions are the prompt. "Issue date, not the due date" prevents the most common mix-up on invoices, and it costs nothing. Every field is optional on purpose: if the document has no issue date or no total, the model must be able to say so instead of inventing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Ask the model for JSON that fits the schema
&lt;/h2&gt;

&lt;p&gt;Structured output means the model is constrained to return JSON that validates against your schema, so you get an object back, not text you have to parse. Most providers support it; &lt;a href="https://platform.openai.com/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;OpenAI's structured outputs guide&lt;/a&gt; describes the OpenAI version. This example uses the OpenAI Python SDK, and the same pattern works with Anthropic or Gemini.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# reads OPENAI_API_KEY from the environment
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# use the current model id on the day you run this
&lt;/span&gt;        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract the invoice fields from the document. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If a field is not present in the document, do not guess it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;text_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_parsed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function takes the text as an argument because the next step needs the same text to check the result against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Check the output before you trust it
&lt;/h2&gt;

&lt;p&gt;A validation step is deterministic code that tests whether the extracted values are consistent with each other and with the source. It catches the errors that look right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Completeness: the fields you cannot do without must be present.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issued_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Arithmetic: with tax stated, lines plus tax must equal the total, to the cent.
&lt;/span&gt;    &lt;span class="c1"&gt;#    Without it, lines can only be checked against the total as an upper bound.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;li&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines_sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line items plus tax do not match the total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;lines_sum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line items exceed the total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Grounding: every identifier the model returns must appear in the document.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supplier_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;purchase_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found in source text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Plausibility: dates in the future are usually a misread.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issued_on&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issued_on&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issued_on is in the future&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;source_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pdf_to_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all checks passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blocks on this page are one script: paste them into a single file in order and they run together. That is the whole pattern. This run prints the check results and not the invoice, and the walkthrough assumes a fictional invoice: with real documents, log field names and check results rather than extracted values, which can contain client data.&lt;/p&gt;

&lt;p&gt;Here is what the checks are for. The values below are illustrative, not from a real document.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good output
&lt;/h3&gt;

&lt;p&gt;A good output passes every check. The lines add up, the number is on the page and the date is sensible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2041"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"issued_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"supplier_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Hosting S.L."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hosting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;400.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;400.0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;150.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;300.0&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"tax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;147.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;847.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check()&lt;/code&gt; returns an empty list. The lines (700) plus the tax field (147) equal the total (847). The arithmetic check is exact only when the invoice states its tax separately. When it does not, the check can only tell that the lines exceed the total, so an under-read line goes unnoticed; that is why the other checks matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bad output
&lt;/h3&gt;

&lt;p&gt;A bad output looks just as tidy, and that is the danger. The model misread the table and invented a number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2014"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"issued_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2031-03-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"supplier_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Hosting S.L."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hosting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;400.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;400.0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unit_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1500.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3000.0&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"tax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;147.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;847.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check()&lt;/code&gt; returns three problems: line items plus tax do not match the total, &lt;code&gt;invoice_number&lt;/code&gt; is not in the source text and &lt;code&gt;issued_on&lt;/code&gt; is in the future. Nothing in the JSON itself said any of this was wrong; only the checks did.&lt;/p&gt;

&lt;p&gt;The grounding check is the cheap version of a large idea: a value that cannot be traced back to the page should not survive your pipeline. The longer treatment of that idea is in &lt;a href="https://anyformat.ai/blog/reduce-llm-hallucinations-document-extraction" rel="noopener noreferrer"&gt;how to reduce LLM hallucinations in document extraction&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the script breaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure 1: scanned PDFs
&lt;/h3&gt;

&lt;p&gt;A scan has no text layer, so &lt;code&gt;pdf_to_text&lt;/code&gt; returns almost nothing and the model is asked to extract from empty text. Detect it (less than a few hundred characters of text per page, not counting the page markers) and route the file to OCR or to a vision model that reads the page image. Never send the empty text to the LLM: it will often return plausible values anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 2: tables
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;page.get_text()&lt;/code&gt; flattens a table into a stream where columns and rows can interleave. A line item's quantity lands next to the wrong price and the arithmetic check in step 4 fails. Extract tables as tables (a layout-aware parser, or a vision model), or pass the model page images for the pages that contain them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 3: long documents
&lt;/h3&gt;

&lt;p&gt;A 200-page contract does not fit comfortably in one prompt, and models read the middle of a long context less reliably than the start and end. Split by section or page range, extract per chunk, then merge. The merge step is where duplicates and conflicts show up, so keep the page markers. There is more on this in &lt;a href="https://anyformat.ai/blog/anyformat-long-documents-extraction" rel="noopener noreferrer"&gt;long-document extraction&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 4: fields that are not in the document
&lt;/h3&gt;

&lt;p&gt;Ask for &lt;code&gt;purchase_order&lt;/code&gt; on an invoice without one and a model may return a plausible number instead of nothing. Make absence a legal answer: declare every field optional, as in the schema above, tell the model explicitly to return null rather than guess (the system prompt does), and let the grounding check catch the rest: it searches the document for the invoice number, the supplier and the purchase order, so an invented one is reported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 5: no way to tell right from wrong at scale
&lt;/h3&gt;

&lt;p&gt;The script returns the same clean JSON whether it read the page correctly or not. If you process ten documents you can eyeball them. If you process ten thousand you need a signal per field that says which ones to review. Raw model confidence is a poor signal, because it tends to be highest where the model is filling a gap. This is the problem the next section is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to add for production
&lt;/h2&gt;

&lt;p&gt;Three things separate a script from a pipeline: a pointer from each value back to where it was read, a confidence score you can set a threshold on, and a place where a person reviews what falls below it. You can build all three on top of the code above. The grounding check is a start, a calibration set and a threshold come next, and a review interface is the largest piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another way to do it: anyformat
&lt;/h2&gt;

&lt;p&gt;anyformat is a document extraction platform, and the script above is the job it does for you. The steps are the same (parse the document, apply a schema, return fields), but the pointer to the source, the confidence score and the review interface are already built.&lt;/p&gt;

&lt;p&gt;The workflow is defined once as a typed graph, a parse node feeding an extract node, then each document is one upload and one poll. Each field comes back with its value, a confidence score and the evidence it was read from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anyformat.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anyformat.workflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Schema&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANYFORMAT_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice ID as printed, usually near the top.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issued_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Issue date, not the due date.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Grand total including tax.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# value is a string; confidence runs from 0 to 100
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it with &lt;code&gt;pip install anyformat&lt;/code&gt;. As above, use a fictional document while you try it: the example prints a value and its evidence, and real documents can contain client data. A field the model could not find comes back as null, with a low confidence score so you can filter it. The evidence is the source text and the page number, so a reviewer checks a flagged field against the page in seconds. The response shape and the full node list are in the &lt;a href="https://docs.anyformat.ai" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;. The reasoning behind per-field confidence is in &lt;a href="https://anyformat.ai/blog/anyformat-calibrated-confidence" rel="noopener noreferrer"&gt;calibrated confidence&lt;/a&gt;, and the same workflow is reachable from an agent through the &lt;a href="https://anyformat.ai/blog/anyformat-ai-mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;anyformat is one option among several. If you process a few dozen documents of one type, the script above plus the checks in step 4 is enough, and you should not pay for more. The case for a platform starts when volume, variety or the cost of a wrong value makes the review layer the real project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can an LLM extract data from a PDF directly?&lt;/strong&gt;&lt;br&gt;
Yes, if you give it the PDF's text or page images and a schema for the output. The hard part is not the extraction call. It is handling scans, tables and long documents, and knowing which of the returned values to trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between structured output and JSON mode?&lt;/strong&gt;&lt;br&gt;
JSON mode guarantees the reply is valid JSON. Structured output guarantees it also matches your schema: the field names, the types and the required fields. For extraction you want the second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop an LLM from making up values in extraction?&lt;/strong&gt;&lt;br&gt;
You cannot bring it to zero, so make it detectable: declare fields optional, instruct the model to return null instead of guessing, verify that each value appears in the source, and validate values against each other. More on this in &lt;a href="https://anyformat.ai/blog/reduce-llm-hallucinations-document-extraction" rel="noopener noreferrer"&gt;reducing hallucinations in document extraction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work on scanned PDFs?&lt;/strong&gt;&lt;br&gt;
Not with a text library alone. A scan has no text layer, so you need OCR or a vision model first. Detect the empty text layer and route accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does LLM extraction cost per page?&lt;/strong&gt;&lt;br&gt;
It depends on the model, the page length and whether you send text or images. Measure it on 20 of your own documents before you pick a model, because public price lists change often enough that any number here would go stale.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://anyformat.ai/blog/extract-structured-data-from-pdf-with-llm" rel="noopener noreferrer"&gt;anyformat.ai/blog/extract-structured-data-from-pdf-with-llm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>pdf</category>
    </item>
  </channel>
</rss>
