* * * GUIDES * * *
Extract tables from a PDF into pandas.
UPDATED
Rule-based extractors like Camelot and Tabula need a digital PDF with clean lines. If your PDFs are scans or photos, OCR the page into markdown first, then turn each markdown table into a DataFrame. A short parser is all it takes.
01 / INSTALL AND GET A KEY
One dependency. Your first 100 pages are free, no card, at pennyocr.com/dashboard.
pip install pennyocr
export PENNYOCR_API_KEY=pk_live_...02 / READ THE DOCUMENT
from pennyocr import PennyOCR
result = PennyOCR().ocr("report.pdf") # markdown, tables preserved
text = result.text03 / WHAT YOU GET BACK
Tables arrive as markdown tables between the prose. Multi-line cells are joined with <br>:
## Q3 results
| Region | Revenue | Growth |
|---|---:|---:|
| North | 1,204.50 | 12% |
| South | 980.00 | -3% |
Some prose between tables.
| SKU | Description | Qty |
|---|---|---:|
| A-100 | Bolt, M8<br>zinc | 400 |04 / PARSE EVERY TABLE
Each table comes back as its header row and its body rows:
def markdown_tables(md: str):
"""Yield each markdown table as (header, rows)."""
block = []
for line in md.splitlines() + [""]:
if line.strip().startswith("|"):
block.append([c.strip().replace("<br>", " ")
for c in line.strip().strip("|").split("|")])
elif block:
body = [r for r in block[1:] if not set("".join(r)) <= set("-: ")]
yield block[0], body
block = []
tables = list(markdown_tables(text))
print(len(tables), "tables")05 / LOAD INTO PANDAS
Cells arrive as strings, so convert the numeric columns you need:
import pandas as pd
frames = [pd.DataFrame(rows, columns=header) for header, rows in tables]
revenue = frames[0]
revenue["Revenue"] = revenue["Revenue"].str.replace(",", "").astype(float)
print(revenue)06 / SAVE EACH TABLE AS CSV
One file per table:
for i, df in enumerate(frames, 1):
df.to_csv(f"table_{i}.csv", index=False)
print("wrote", len(frames), "CSV files")Cost: $0.00075 per page, tables included, versus about $15 per 1,000 pages for AWS Textract's table feature. For tables split across pages, read result.page_results and join tables with the same header.