Skip to content

Commit ae3367a

Browse files
nunombispostephengruppettarealpython-botbzaczynskiclaude
authored
Added materials for Docling vs LlamaParse tutorial (#787)
* Added materials for Docling vs LlamaParse tutorial * Updated materials after TR #1 * Updated lint from TR#1 updates * Added README for pdf-table-extraction-docling-vs-llamaparse * Update pdf-table-extraction-docling-vs-llamaparse/README.md Co-authored-by: Real Python Bot <42617967+realpython-bot@users.noreply.github.com> * Updates after DR #1 * Rename folder to match post slug (extract-table-from-pdf-python) * Updated README * Final QA fixes for the Docling vs LlamaParse materials - Reformat llamaparse_tables.py so ruff format --check passes, and add its missing trailing newline, module docstring, and import grouping to match the other five scripts - Update the README title and link text to the final article title - Point the LlamaParse docs link at developers.llamaindex.ai, since the old docs.cloud.llamaindex.ai URL now redirects Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: stephengruppetta <51741022+stephengruppetta@users.noreply.github.com> Co-authored-by: Real Python Bot <42617967+realpython-bot@users.noreply.github.com> Co-authored-by: Bartosz Zaczyński <bartosz.zaczynski@gmail.com> Co-authored-by: Bartosz Zaczyński <bartosz@realpython.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8763d2f commit ae3367a

9 files changed

Lines changed: 293 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Docling vs LlamaParse: How to Extract PDF Tables in Python
2+
3+
This folder contains the code examples for the Real Python tutorial [Docling vs LlamaParse: How to Extract PDF Tables in Python](https://realpython.com/extract-table-from-pdf-python/).
4+
5+
The scripts parse `sample_report.pdf`, a short financial report with tables, and compare two approaches:
6+
7+
- **[Docling](https://github.com/docling-project/docling)** runs locally and exports structured document data, including tables as pandas DataFrames.
8+
- **[LlamaParse](https://developers.llamaindex.ai/llamaparse/parse/getting_started/)** uses the Llama Cloud API for parsing and schema-driven extraction.
9+
10+
## Files
11+
12+
| File | Description |
13+
|------|-------------|
14+
| `sample_report.pdf` | Sample PDF used by all scripts |
15+
| `docling_extraction.py` | Parse the PDF with Docling and print Markdown output |
16+
| `docling_tables.py` | Inspect detected tables and print selected DataFrames |
17+
| `docling_formats.py` | Export Docling results to Markdown, JSON, HTML, and DataFrames |
18+
| `llamaparse_extraction.py` | Parse the PDF with LlamaParse and print Markdown output |
19+
| `llamaparse_tables.py` | Find HTML tables in the LlamaParse Markdown and print selected tables |
20+
| `llamaparse_formats.py` | Export LlamaParse results to Markdown, plain text, and JSON |
21+
| `requirements.txt` | Pinned dependencies for this folder |
22+
23+
## Installation
24+
25+
Create and activate a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/), then install the dependencies:
26+
27+
```shell
28+
$ python3 -m venv venv/
29+
$ source venv/bin/activate
30+
(venv) $ python -m pip install -r requirements.txt
31+
```
32+
33+
Run the scripts from this folder so the relative path to `sample_report.pdf` resolves correctly.
34+
35+
## Docling examples
36+
37+
Docling runs on your machine and does not require an API key.
38+
39+
```shell
40+
(venv) $ python docling_extraction.py
41+
(venv) $ python docling_tables.py
42+
(venv) $ python docling_formats.py
43+
```
44+
45+
`docling_formats.py` writes `output_docling.md`, `output_docling.json`, and `output_docling.html` in the current directory.
46+
47+
## LlamaParse examples
48+
49+
The LlamaParse scripts require a [Llama Cloud API key](https://cloud.llamaindex.ai/). Export it before running:
50+
51+
```shell
52+
(venv) $ export LLAMA_CLOUD_API_KEY="your-api-key"
53+
(venv) $ python llamaparse_extraction.py
54+
(venv) $ python llamaparse_tables.py
55+
(venv) $ python llamaparse_formats.py
56+
```
57+
58+
`llamaparse_formats.py` writes `output_llamaparse.md`, `output_llamaparse.text`, and `output_llamaparse.json` in the current directory.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Parse a PDF with Docling and print Markdown output."""
2+
3+
from pathlib import Path
4+
5+
from docling.document_converter import DocumentConverter
6+
7+
PDF_PATH = Path("sample_report.pdf")
8+
9+
10+
def main() -> None:
11+
converter = DocumentConverter()
12+
result = converter.convert(PDF_PATH)
13+
14+
markdown = result.document.export_to_markdown()
15+
print(markdown[:3000])
16+
print("\n---\n")
17+
print(f"Pages parsed: {len(result.document.pages)}")
18+
print(f"Tables found: {len(result.document.tables)}")
19+
20+
21+
if __name__ == "__main__":
22+
main()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Export Docling parse results to Markdown, JSON, HTML, and pandas DataFrames."""
2+
3+
import json
4+
from pathlib import Path
5+
6+
from docling.document_converter import DocumentConverter
7+
8+
PDF_PATH = Path("sample_report.pdf")
9+
10+
11+
def main() -> None:
12+
converter = DocumentConverter()
13+
document = converter.convert(PDF_PATH).document
14+
15+
markdown = document.export_to_markdown()
16+
Path("output_docling.md").write_text(markdown, encoding="utf-8")
17+
18+
payload = document.export_to_dict()
19+
Path("output_docling.json").write_text(
20+
json.dumps(payload, indent=2),
21+
encoding="utf-8",
22+
)
23+
24+
html = document.export_to_html()
25+
Path("output_docling.html").write_text(html, encoding="utf-8")
26+
27+
for index, table in enumerate(document.tables):
28+
frame = table.export_to_dataframe(doc=document)
29+
print(f"Table {index} shape: {frame.shape}")
30+
print(frame.head(), end="\n\n")
31+
32+
33+
if __name__ == "__main__":
34+
main()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Inspect and export tables from a Docling parse result."""
2+
3+
from pathlib import Path
4+
5+
from docling.document_converter import DocumentConverter
6+
7+
PDF_PATH = Path("sample_report.pdf")
8+
9+
10+
def main() -> None:
11+
document = DocumentConverter().convert(PDF_PATH).document
12+
13+
print(f"Tables found: {len(document.tables)}\n")
14+
15+
for index, table in enumerate(document.tables):
16+
pages = sorted({prov.page_no for prov in table.prov})
17+
frame = table.export_to_dataframe(doc=document)
18+
print(f"Table {index}: pages {pages}, shape {frame.shape}")
19+
20+
index_table = document.tables[0].export_to_dataframe(doc=document)
21+
print("\nFinancial statement index (table 0):")
22+
print(index_table.to_string(index=False), end="\n\n")
23+
24+
operations_table = document.tables[1].export_to_dataframe(doc=document)
25+
print("Operations statement preview (table 1, first 4 rows):")
26+
print(operations_table.head(4).to_string())
27+
28+
29+
if __name__ == "__main__":
30+
main()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Parse a PDF with LlamaParse (llama-cloud SDK) and print Markdown output."""
2+
3+
import os
4+
from pathlib import Path
5+
6+
from llama_cloud import LlamaCloud
7+
8+
PDF_PATH = Path("sample_report.pdf")
9+
10+
11+
def main() -> None:
12+
client = LlamaCloud(api_key=os.environ["LLAMA_CLOUD_API_KEY"])
13+
14+
uploaded = client.files.create(file=PDF_PATH, purpose="parse")
15+
result = client.parsing.parse(
16+
file_id=uploaded.id,
17+
tier="agentic",
18+
version="latest",
19+
expand=["markdown"],
20+
)
21+
22+
pages = ""
23+
for page in result.markdown.pages:
24+
pages += page.markdown
25+
pages += "\n---\n"
26+
27+
print(pages[:3000])
28+
print(f"Pages parsed: {len(result.markdown.pages)}")
29+
30+
31+
if __name__ == "__main__":
32+
main()
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Export LlamaParse results to Markdown, Text, and schema-driven JSON."""
2+
3+
import json
4+
import os
5+
from pathlib import Path
6+
7+
from llama_cloud import LlamaCloud
8+
from pydantic import BaseModel, Field
9+
10+
PDF_PATH = Path("sample_report.pdf")
11+
12+
13+
class RevenueRow(BaseModel):
14+
period: str = Field(
15+
description="Fiscal period label, e.g. FY 2025 or September 27, 2025",
16+
)
17+
revenue_millions: float = Field(
18+
description="Revenue in millions of USD",
19+
)
20+
growth_percent: float | None = Field(
21+
default=None,
22+
description="Year-over-year growth percentage if stated",
23+
)
24+
25+
26+
class RevenueTable(BaseModel):
27+
rows: list[RevenueRow] = Field(
28+
description="One row per fiscal period in the table"
29+
)
30+
31+
32+
def main() -> None:
33+
client = LlamaCloud(api_key=os.environ["LLAMA_CLOUD_API_KEY"])
34+
35+
uploaded = client.files.create(file=PDF_PATH, purpose="parse")
36+
37+
parsed = client.parsing.parse(
38+
file_id=uploaded.id,
39+
tier="agentic",
40+
version="latest",
41+
expand=["markdown", "text"],
42+
)
43+
44+
markdown_pages = "\n\n".join(
45+
page.markdown for page in parsed.markdown.pages
46+
)
47+
Path("output_llamaparse.md").write_text(markdown_pages, encoding="utf-8")
48+
49+
if parsed.text and parsed.text.pages:
50+
text_pages = "\n".join(page.text for page in parsed.text.pages)
51+
Path("output_llamaparse.text").write_text(text_pages, encoding="utf-8")
52+
53+
extract_file = client.files.create(file=PDF_PATH, purpose="extract")
54+
job = client.extract.run(
55+
file_input=extract_file.id,
56+
configuration={
57+
"data_schema": RevenueTable.model_json_schema(),
58+
"extraction_target": "per_doc",
59+
"tier": "agentic",
60+
},
61+
)
62+
63+
Path("output_llamaparse.json").write_text(
64+
json.dumps(job.extract_result, indent=2),
65+
encoding="utf-8",
66+
)
67+
print(json.dumps(job.extract_result, indent=2))
68+
69+
70+
if __name__ == "__main__":
71+
main()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Find HTML tables in a LlamaParse Markdown response and print them."""
2+
3+
import os
4+
import re
5+
from pathlib import Path
6+
7+
from llama_cloud import LlamaCloud
8+
9+
PDF_PATH = Path("sample_report.pdf")
10+
TABLE_PATTERN = re.compile(r"<table\b.*?</table>", re.DOTALL | re.IGNORECASE)
11+
12+
13+
def main() -> None:
14+
client = LlamaCloud(api_key=os.environ["LLAMA_CLOUD_API_KEY"])
15+
16+
uploaded = client.files.create(file=PDF_PATH, purpose="parse")
17+
result = client.parsing.parse(
18+
file_id=uploaded.id,
19+
tier="agentic",
20+
version="latest",
21+
expand=["markdown"],
22+
)
23+
24+
tables = []
25+
for page_no, page in enumerate(result.markdown.pages, start=1):
26+
for table_html in TABLE_PATTERN.findall(page.markdown):
27+
tables.append((page_no, table_html))
28+
29+
print(f"Tables found: {len(tables)}\n")
30+
for index, (page_no, table_html) in enumerate(tables):
31+
print(f"Table {index}: page {page_no}")
32+
33+
print("\nTable 0:")
34+
print(tables[0][1], end="\n\n")
35+
36+
print("Table 1 (truncated):")
37+
print(tables[1][1][:700], "...")
38+
39+
40+
if __name__ == "__main__":
41+
main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
docling==2.102.2
2+
onnxruntime>=1.7.0,<2.0.0
3+
llama-cloud>=2.9.0
4+
pandas>=2.0.0
5+
pydantic>=2.0.0
155 KB
Binary file not shown.

0 commit comments

Comments
 (0)