From pandas¶
pandas is the standard Python library for tabular data analysis: labeled columns, typed
arrays, group-by, joins, and file I/O across CSV, Parquet, SQL, and more. Web scrapers reach for exactly one corner of
it, read_html, which parses every <table> on
a page into a list of DataFrame objects, resolving rowspan/colspan and optionally inferring headers and
dtypes. To do that it needs a third-party HTML backend (lxml, html5lib, or bs4) plus pandas and NumPy
themselves.
turbohtml covers only that table-reading slice, and covers it without the dependency stack.
rows() and records() walk the cell grid in C and hand back plain lists
and dicts, so a scraper that only needs a grid of strings never imports NumPy. Everything else pandas does – the
DataFrame, its dtypes, and its analytics – stays outside turbohtml’s scope; when you do want a frame, the records
are the exact shape pandas.DataFrame takes, so pandas stays an optional last step rather than a hard parse-time
dependency.
turbohtml vs pandas¶
Dimension |
turbohtml |
pandas |
|---|---|---|
Scope |
WHATWG HTML parse plus a C cell-grid reader that returns lists and dicts |
Full tabular analytics library; |
Feature breadth |
|
Header inference, dtype coercion, |
Performance |
C cell-grid walk into plain containers; roughly 12 to 90 times faster than |
Builds a NumPy-backed |
Typing |
Ships |
Ships |
Dependencies |
Zero runtime dependencies (self-contained C extension) |
pandas and NumPy, plus |
Maintenance |
Actively developed, single-engine |
Mature, very widely deployed, long release history |
Feature overlap¶
The shared surface is table reading; these port one-to-one (see the mapping under How to migrate):
Parsing table markup you already hold into a rectangular grid.
Resolving
rowspanandcolspaninto a filled grid of cells.Reading one table’s body rows as a list of lists (
rows()forread_html(...)[0]).Reading a table as header-keyed dicts (
records()forread_html(..., header=0)[0].to_dict("records")).Reading every table in a document in one call (
tables()for thelist[DataFrame]thatread_htmlreturns).
What turbohtml adds¶
No NumPy or pandas import.
rows()andrecords()return built-inlist/dict, so reading a grid of strings pulls in nothing. pandas keepsDataFrameoptional and last.A C engine. Parsing and the span-resolving grid walk both run in C, so small tables avoid the per-frame overhead that dominates
read_html.A full document model. The same parse gives you
find/select/XPath, mutation, and WHATWG serialization, so table extraction lives inside a real DOM rather than a one-shotiohelper.One deterministic algorithm. turbohtml always runs the WHATWG parser; there is no backend flavor to pick or install, and no behavioral drift between
lxmlandhtml5libmodes.
What pandas has that turbohtml does not¶
Dtype inference.
read_htmlcoerces cells toint/float/NaN(and honorsdtype,converters,thousands,decimal,parse_dates). turbohtml cells are alwaysstr. Workaround: passrecords()topandas.DataFrameand let it infer, or convert columns yourself.Header and index inference.
header=,index_col=, and multi-row headers reshape the frame. turbohtml treats the first row (typically thetheadrow) as the header forrecords()and offers no index concept. Workaround: readrows()and slice the header rows yourself.Sparse span handling.
read_htmlcan leave a spanned cell as a single value withNaNpadding depending on options; turbohtml copies a spanned cell’s text into every slot it covers. No option toggles this.URL and file I/O with backend flavors.
read_htmlaccepts a URL, path, or file object and picks anlxml/html5lib/bs4parser. turbohtml parses markup you already hold. Workaround: fetch withurllib.request(orrequests) and pass the bytes toturbohtml.parse().Table matching and filtering.
read_htmlfilters tables bymatch=regex,attrs=, andextract_links=. turbohtml has no such filter ontables(). Workaround:find()/select()the<table>you want first, then callrows().
Performance¶
Reading a table with rows() and records() against
pandas.read_html, both parsing the markup and resolving spans into a rectangular grid. turbohtml’s C cell-grid walk
returns plain lists and dicts where read_html builds a DataFrame (importing NumPy), so it runs roughly twelve to
ninety times faster: the gap is widest on small tables, where pandas pays a fixed per-frame cost, and narrows as the row
count grows. See Performance for the methodology.
input |
turbohtml |
|
|---|---|---|
rows (10 rows) |
2.8 µs |
379 µs (136x) |
records (10 rows) |
2.89 µs |
481 µs (167x) |
rows (100 rows) |
24.8 µs |
1.06 ms (42.9x) |
records (100 rows) |
26.1 µs |
1.23 ms (47.3x) |
rows (1000 rows) |
263 µs |
8.31 ms (31.7x) |
records (1000 rows) |
275 µs |
9.12 ms (33.2x) |
How to migrate¶
Swap the read_html call for a parse plus a grid method. There is no backend flavor to pass, and no NumPy to install;
if you still want a DataFrame, feed records() to pandas.DataFrame as the last step:
# pandas
import pandas as pd
frame = pd.read_html(html, header=0)[0]
records = frame.to_dict("records")
import turbohtml
table = turbohtml.parse("<table><tr><th>name</th><th>qty</th></tr><tr><td>pen</td><td>3</td></tr></table>").find(
"table"
)
records = table.records()
print(records)
# pandas.DataFrame(records) builds the frame, keeping pandas optional
[{'name': 'pen', 'qty': '3'}]
API mapping¶
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
tables() returns every table in a subtree, each already shaped as rows(),
so a whole document reads back without locating each <table> first:
document = turbohtml.parse("<table><tr><td>a</td></tr></table><table><tr><td>b</td><td>c</td></tr></table>")
print(document.tables())
[[['a']], [['b', 'c']]]
Gotchas and pitfalls¶
Cells are strings, always. turbohtml does not infer
int/float/NaNdtypes the way pandas does; convert columns yourself, or letpandas.DataFramedo it afterrecords().Spans are filled, not collapsed. A
rowspan/colspancell’s text is copied into every slot it covers, where pandas can leave a single value andNaNpadding depending on options.The header is the first row (typically the
theadrow); there is noheader=/index_col=inference or a multi-row header. For anything fancier, readrows()and slice it yourself.No I/O or flavors.
read_htmlfetches URLs and picks anlxml/bs4parser; turbohtml parses markup you already hold with the one WHATWG algorithm, so fetch the page (e.g. withurllib.request) and pass the bytes toturbohtml.parse().