Table Extraction
Tables are one of the most challenging elements to extract from PDFs. OpenDataLoader achieves 0.928 table accuracy in benchmarks - the highest of any parser. In this lesson, you'll master table extraction techniques.
Table Structure in JSON
When you convert a PDF with tables, each table element contains rich structure:
{
"type": "table",
"id": 15,
"page number": 1,
"bounding box": [72.0, 400.0, 540.0, 550.0],
"rows": 5,
"columns": 4,
"kids": [
{
"type": "table-row",
"row index": 0,
"kids": [
{
"type": "table-cell",
"row index": 0,
"column index": 0,
"content": "Product",
"bounding box": [72.0, 530.0, 180.0, 550.0]
},
{
"type": "table-cell",
"row index": 0,
"column index": 1,
"content": "Price",
"bounding box": [180.0, 530.0, 288.0, 550.0]
}
]
}
]
}
Key Table Properties
| Property | Description |
|---|---|
rows | Total number of rows |
columns | Total number of columns |
kids | Nested table-row elements |
row index | 0-based row position |
column index | 0-based column position |
Extracting Tables
Basic Table Extraction
import opendataloader_pdf
import json
# Convert PDF with tables
opendataloader_pdf.convert(
input_path="financial-report.pdf",
output_dir="output/",
format="json"
)
# Load and extract tables
with open('output/financial-report.json', 'r') as f:
data = json.load(f)
tables = [el for el in data['kids'] if el['type'] == 'table']
print(f"Found {len(tables)} tables")
Converting Tables to 2D Arrays
def table_to_array(table_element):
"""Convert OpenDataLoader table to 2D array."""
rows = table_element.get('rows', 0)
cols = table_element.get('columns', 0)
# Initialize empty grid
grid = [['' for _ in range(cols)] for _ in range(rows)]
# Fill in cells
for row in table_element.get('kids', []):
if row['type'] == 'table-row':
for cell in row.get('kids', []):
if cell['type'] == 'table-cell':
r = cell.get('row index', 0)
c = cell.get('column index', 0)
grid[r][c] = cell.get('content', '')
return grid
# Use it
for i, table in enumerate(tables):
array = table_to_array(table)
print(f"\nTable {i+1} ({table['rows']}x{table['columns']}):")
for row in array:
print(row)
Converting to pandas DataFrame
import pandas as pd
def table_to_dataframe(table_element, header_row=True):
"""Convert OpenDataLoader table to pandas DataFrame."""
array = table_to_array(table_element)
if header_row and len(array) > 0:
df = pd.DataFrame(array[1:], columns=array[0])
else:
df = pd.DataFrame(array)
return df
# Extract all tables as DataFrames
dataframes = [table_to_dataframe(t) for t in tables]
# Work with pandas
for i, df in enumerate(dataframes):
print(f"\nTable {i+1}:")
print(df.to_string())
# Export to CSV
df.to_csv(f'table_{i+1}.csv', index=False)
Handling Complex Tables
Borderless Tables
OpenDataLoader detects tables without visible borders using cluster analysis:
# No special configuration needed - works automatically
opendataloader_pdf.convert(
input_path="borderless-tables.pdf",
output_dir="output/",
format="json"
)
Merged Cells
Tables with merged cells (spanning multiple rows/columns) are handled with row span and column span:
{
"type": "table-cell",
"row index": 0,
"column index": 0,
"row span": 2,
"column span": 1,
"content": "Category Header"
}
Handle merged cells in code:
def table_to_array_with_spans(table_element):
"""Handle tables with merged cells."""
rows = table_element.get('rows', 0)
cols = table_element.get('columns', 0)
grid = [['' for _ in range(cols)] for _ in range(rows)]
for row in table_element.get('kids', []):
if row['type'] == 'table-row':
for cell in row.get('kids', []):
if cell['type'] == 'table-cell':
r = cell.get('row index', 0)
c = cell.get('column index', 0)
content = cell.get('content', '')
# Handle spans by filling multiple cells
row_span = cell.get('row span', 1)
col_span = cell.get('column span', 1)
for dr in range(row_span):
for dc in range(col_span):
if r + dr < rows and c + dc < cols:
grid[r + dr][c + dc] = content
return grid
Page Range Processing
For large documents, extract tables from specific pages:
# Process only pages 1-5 and page 10
opendataloader_pdf.convert(
input_path="large-report.pdf",
output_dir="output/",
format="json",
pages="1-5,10"
)
Page Range Syntax
| Syntax | Meaning |
|---|---|
"1" | Page 1 only |
"1-5" | Pages 1 through 5 |
"1,3,5" | Pages 1, 3, and 5 |
"1-3,7-9" | Pages 1-3 and 7-9 |
Multi-Column Documents
OpenDataLoader's XY-Cut++ algorithm correctly handles multi-column layouts:
┌─────────────────────────────────┐
│ Column 1 │ Column 2 │
│ ┌─────────┐ │ ┌─────────┐ │
│ │ Table A │ │ │ Table B │ │
│ └─────────┘ │ └─────────┘ │
│ Text... │ Text... │
└─────────────────────────────────┘
Tables are extracted in reading order (left column first, then right column).
# Reading order is automatic
for i, table in enumerate(tables):
page = table['page number']
bbox = table['bounding box']
print(f"Table {i+1} on page {page}")
print(f" Position: left={bbox[0]:.0f}, top={bbox[3]:.0f}")
Markdown Table Output
For quick viewing, use Markdown output:
opendataloader_pdf.convert(
input_path="report.pdf",
output_dir="output/",
format="markdown"
)
Output in report.md:
## Quarterly Results
| Quarter | Revenue | Growth |
|---------|---------|--------|
| Q1 2024 | $2.1M | 15% |
| Q2 2024 | $2.4M | 14% |
| Q3 2024 | $2.8M | 17% |
Challenge: Financial Report Analyzer
Create a script that:
- Extracts all tables from a financial report PDF
- Identifies tables with numeric data
- Calculates column totals where applicable
- Exports each table to CSV
import opendataloader_pdf
import json
import re
def analyze_financial_report(pdf_path):
# Convert PDF
opendataloader_pdf.convert(
input_path=pdf_path,
output_dir="output/",
format="json"
)
# Load JSON
json_path = f'output/{pdf_path.replace(".pdf", ".json")}'
with open(json_path, 'r') as f:
data = json.load(f)
# Your code here:
# 1. Extract all tables
# 2. For each table, detect numeric columns
# 3. Calculate totals for numeric columns
# 4. Export to CSV with totals row
pass
def is_numeric(value):
"""Check if a string represents a number (including currency)."""
# Remove currency symbols and commas
cleaned = re.sub(r'[$€£,]', '', str(value).strip())
try:
float(cleaned)
return True
except ValueError:
return False
# Run analysis
analyze_financial_report("quarterly-report.pdf")
Expected output:
Table 1: Revenue Summary (Page 1)
- Detected 3 numeric columns: Revenue, Costs, Profit
- Calculated totals: $10.5M, $7.2M, $3.3M
- Exported to: table_1_revenue_summary.csv
Table 2: Department Breakdown (Page 2)
- Detected 2 numeric columns: Budget, Actual
- Calculated totals: $5.0M, $4.8M
- Exported to: table_2_department_breakdown.csv