🔥 0
0
Lesson 3 of 5 25 min +100 XP

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

PropertyDescription
rowsTotal number of rows
columnsTotal number of columns
kidsNested table-row elements
row index0-based row position
column index0-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

SyntaxMeaning
"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

🧠 Quick Quiz

Test your understanding of this lesson.

1

How does OpenDataLoader detect borderless tables?

2

What format is best for preserving table structure in code?

3

How can you extract tables from specific pages only?

PDF Structure & Bounding Boxes