cull / html table → csv

Extract HTML tables to CSV from the command line

cull turns any HTML <table> into clean CSV — or one JSON object per row — with one flag: --table. It fetches URLs itself, expands colspan/rowspan so every row has the same number of cells, merges multi-row headers into sensible column names, and quotes fields properly. Single small static binary.

The one-liner

A real Wikipedia table, output verbatim (first lines):

$ cull --table 'table.wikitable' https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)
Country or territory,Population (1 July 2022),Population (1 July 2023),Change (%),UN continental region[1],UN statistical subregion[1]
World,"8,021,407,192","8,091,734,930",+0.88%,–,–
India,"1,425,423,212","1,438,069,596",+0.89%,Asia,Southern Asia
China[a],"1,425,179,569","1,422,584,933",−0.18%,Asia,Eastern Asia
United States,"341,534,046","343,477,335",+0.57%,Americas,Northern America

Cells containing commas are quoted per RFC 4180, so the output opens directly in any spreadsheet and imports cleanly into sqlite, pandas, or anything else that reads CSV. Works the same on local files: cull --table report.html > report.csv.

Messy real-world tables: colspan, rowspan, stacked headers

Wikipedia-grade tables have header rows on top of header rows — a "City proper" group spanning Population / Area / Density columns. Naive extractors emit ragged rows; cull expands every span and merges the header stack into one name per column. Verbatim from the largest-cities table:

$ cull --table 'table.wikitable' https://en.wikipedia.org/wiki/List_of_largest_cities
City[a],Country,UN 2025 population estimates[12],City proper[b] Definition,City proper[b] Population,…
Jakarta,Indonesia,"41,913,860",Special region,"10,154,134",…
Dhaka,Bangladesh,"36,585,479",Capital city,"10,295,407",…

Every data row comes out the same width as the header — no manual cleanup pass before importing.

JSON rows instead of CSV

--json-rows emits one JSON object per row, keyed by the header — NDJSON, ready for jq:

$ cull --table --json-rows 'table.wikitable' "$URL" \
    | jq -r 'select(.Country == "India") | .["City[a]"]'
Delhi
Kolkata
Mumbai
…

Picking the right table

Pages often carry several tables (sidebars, navboxes, the one you want). Count them, then narrow with any CSS selector — or filter by the text a table must contain:

# how many tables are on the page?
$ cull table -c "$URL"
5
# only tables whose text contains "Population"
$ cull --table table --has-text 'Population' "$URL"
# or the usual suspects: a class, an id, or the nth table
$ cull --table 'table.wikitable' "$URL"
$ cull --table 'table:nth-of-type(2)' page.html

Into sqlite, spreadsheets, or anything downstream

# SQL over a web table in two commands
$ cull --table 'table.wikitable' "$URL" > cities.csv
$ sqlite3 :memory: '.mode csv' '.import cities.csv cities' \
    'SELECT Country, COUNT(*) FROM cities GROUP BY Country ORDER BY 2 DESC LIMIT 5;'

More patterns (cron snapshots, multi-file batches, CI checks) are in the cookbook.

How this compares to the alternatives

pandas read_htmlpup / htmlqcull
Runs without a language runtime— (Python + lxml)✓ (single ~2 MB binary)
Table → CSV at all✓ (via DataFrame)--table
Table → JSON per row✓ (via DataFrame)--table --json-rows
colspan/rowspan expansion
Multi-row header mergeMultiIndex columns (you flatten them)automatic
Pick tables by CSS selector / contained textattrs + regex matchselect onlyany selector + --has-text
Fetch the URL itself✓ (incl. -H headers, --timeout)

Honest note: if you're already in a Python notebook doing analysis, pandas.read_html is the right tool — cull's niche is the shell: cron jobs, CI, pipelines, machines where installing a Python stack is the expensive part. And cells come out as rendered text — footnote markers like [a] and thousands separators included — so strip those downstream if you need bare numbers.

Install

# macOS / Linux (Homebrew)
$ brew install rashida-thorne/cull/cull
# Rust
$ cargo install cull        # or: cargo binstall cull
# Anywhere (prebuilt binary)
$ curl -fsSL https://raw.githubusercontent.com/rashida-thorne/cull/main/scripts/install.sh | sh

All options (Scoop, Nix, Docker, mise) are on the main page. Or try --table without installing anything in the browser playground — the real engine compiled to WebAssembly.