How to convert an HTML table to CSV in Python
pandas.read_html is the usual answer, but it needs pandas plus lxml or BeautifulSoup. This script uses only Python's standard library, so it runs anywhere Python 3 does.
1. The script
Save as table2csv.py:
import csv, sys, urllib.request
from html.parser import HTMLParser
class Tables(HTMLParser):
# Collects every top-level <table> as a list of rows (text only; colspan repeats the cell)
def __init__(self):
super().__init__(); self.tables = []; self.row = None; self.cell = None; self.span = 1; self.depth = 0
def handle_starttag(self, tag, attrs):
if tag == 'table': self.depth += 1; self.tables.append([]) if self.depth == 1 else None
elif self.depth == 1 and tag == 'tr': self.row = []
elif self.depth == 1 and tag in ('td', 'th') and self.row is not None:
self.cell = []; self.span = int(dict(attrs).get('colspan') or 1)
elif tag == 'br' and self.cell is not None: self.cell.append(' ')
def handle_endtag(self, tag):
if tag == 'table': self.depth -= 1
elif self.depth == 1 and tag in ('td', 'th') and self.cell is not None:
self.row += [' '.join(''.join(self.cell).split())] * self.span; self.cell = None
elif self.depth == 1 and tag == 'tr' and self.row is not None:
if self.row: self.tables[-1].append(self.row)
self.row = None
def handle_data(self, data):
if self.cell is not None: self.cell.append(data)
url, index = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 0
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (table2csv)'})
p = Tables(); p.feed(urllib.request.urlopen(req).read().decode('utf-8', 'replace'))
print(f'{len(p.tables)} tables; sizes: {[len(t) for t in p.tables][:10]}', file=sys.stderr)
csv.writer(sys.stdout).writerows(p.tables[index])
2. Run it
python3 table2csv.py https://en.wikipedia.org/wiki/List_of_sovereign_states_and_dependencies_by_area 1 > countries.csv
It prints the number of tables and their row counts to the terminal (our run: 3 tables; sizes: [1, 277, 6]), so you can see which index you want: table 0 here is a one-row legend, table 1 is the data. The first lines of countries.csv:
,Country / dependency,Total in km2 (mi2),Land in km2 (mi2),Inland water in km2 (mi2),% water,
–,Earth,"510,072,000 (196,940,000)","148,940,000 (57,506,000)","361,132,000 (139,434,000)",70.8,
1,Russia,"17,098,246 (6,601,667)","16,376,870 (6,323,142)","721,380 (278,530)",4.2,[b]
–,Antarctica,"14,200,000 (5,480,000)","14,200,000 (5,480,000)",0,0,[c]
Cells that contain commas are quoted automatically by the csv module, so the file opens correctly in Excel or Google Sheets.
3. Limits
- rowspan is not expanded: a cell that spans several rows appears only in its first row, so later cells shift left. colspan is repeated. If your table uses rowspan, paste its HTML into the free HTML table to CSV tool, which expands both.
- Only the server HTML: tables built by JavaScript after the page loads are not in the downloaded HTML. For those, use the browser extension below.
- Nested tables are skipped (only top-level tables are collected).
4. No code
For a table you can see in the browser, PlainTables exports it to CSV, Excel or Markdown in one click, including tables built by JavaScript.
Building the page yourself? See how to add an "Export to Excel" button with JavaScript.
Frequently asked questions
Does this need pandas?
No. It uses only html.parser, csv and urllib from the standard library. If you already use pandas, pandas.read_html is the common alternative (it needs lxml or BeautifulSoup too).
Why does it find no tables?
The table is probably built by JavaScript, or the site blocks scripts. Check by viewing the page source (not the inspector): if the table is not there, the script cannot see it.
Written with AI assistance. The script was run on 2026-10-01 against the Wikipedia list of countries by area (3 tables found; table 1 = 277 rows, 7 columns on every row) with the Python 3 standard library only. PlainTables is our extension; the other methods are free and built into Excel, Google Sheets and pandas.