cull / html table → csv
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.
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.
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 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 …
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
# 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.
pandas read_html | pup / htmlq | cull | |
|---|---|---|---|
| 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 merge | MultiIndex columns (you flatten them) | — | automatic |
| Pick tables by CSS selector / contained text | attrs + regex match | select only | any 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.
# 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.