From trafilatura¶
trafilatura extracts the main text and metadata from a web page: it downloads the URL, scores the content body against navigation and boilerplate, and returns the article alongside its title, author, date, description, and site name. It serializes to plain text, Markdown, XML/TEI, JSON, or CSV, and layers in optional comment extraction, table and link handling, deduplication across a crawl, language detection, and precision-tuned publication-date inference (the last from the htmldate library it builds on). It is a common front end for building text corpora for NLP and search indexing.
turbohtml covers the extraction core of that: article() scores a parsed page and harvests its
declared metadata in one C pass, returning an Article record. It works on HTML you already have,
so pair it with your own downloader and, when you need trafilatura’s heavier heuristics (inferred dates, Markdown/XML
output), keep those alongside it. Detected language it now matches: turbohtml.detect.detect_language() classifies
the extracted text.
turbohtml vs trafilatura¶
Dimension |
turbohtml |
trafilatura |
|---|---|---|
Scope |
Full WHATWG HTML parser with DOM, query, serialize, and content extraction on top |
Content-and-metadata extraction plus fetching, crawling, and multi-format output |
Feature breadth |
One C scoring pass yielding a body element and declared metadata ( |
Extraction with comments, tables, links, images, dedup, language ID, and inferred dates |
Performance |
C scoring pass over the parsed tree (see below) |
Python scoring over an lxml tree, with optional readability/justext fallbacks |
Typing |
Fully annotated, ships PEP 561 stubs |
Annotated pure-Python source |
Dependencies |
Compiled C extension |
lxml plus courlan, htmldate, and charset-normalizer |
Maintenance |
Actively developed |
Actively developed and widely used |
Feature overlap¶
The shared surface ports one call to one call:
trafilatura.extract(html)->parse(html).article().text(ormain_text()), the scored body as plain text.extract_metadata(html).title->article().title.extract_metadata(html).author->article().byline.extract_metadata(html).date->article().date(as declared, see the date pitfall below).extract_metadata(html).description->article().description.the extracted content body ->
article().element, the scored element (htmlfor its markup), orNonewhen nothing reads as content.
What turbohtml adds¶
The extraction rides on a full WHATWG parse, so the scored body comes back as a DOM element you can query, mutate, and serialize, not only as text.
main_content()returns that element directly.Metadata is harvested from what the page declares (
<html lang>,article:published_time,rel=author,og:*,<title>) in the same C pass as the body, with no second Python analysis stage.turbohtml.parse()follows the WHATWG recovery rules and never raises on malformed markup, and the parsed tree is reusable for anything else you need from the page.
What trafilatura has that turbohtml does not¶
Fetching and crawling:
fetch_url(url),fetch_response, plus sitemap, feed, and spider helpers. turbohtml has no fetcher; read the page withurlliborhttpxand pass the markup toparse().Output formats.
extract(html, output_format="markdown" | "xml" | "xmltei" | "json" | "csv")serializes the result; turbohtml returns plain text and the DOM element, so build Markdown or JSON fromarticle()yourself.Comment, table, link, and image extraction toggles (
include_comments,include_tables,include_links,include_images). turbohtml scores one prose body; extract those regions from the DOM by hand.favor_precision/favor_recalltuning and the readability-lxml / justext fallbacks. turbohtml has a single scoring model with no drop-in aggressiveness switch.Cross-document deduplication (the LRU cache that drops repeated segments across a crawl). No equivalent.
Inferred publication dates via htmldate.
article().datereturns the declared date string and does not infer; keep htmldate for pages where the date is only inferable. Prose language, by contrast, turbohtml now infers:turbohtml.detect.detect_language()classifies the extracted text (article().langstill only reports the declared<html lang>).License and hostname metadata fields.
article()exposestitle,byline,date,description,lang,canonical,site_name,tags, andimage(the lead image); derive the hostname fromcanonicalwithurllib.parse.urlsplit()and read a license<link rel="license">with a selector.
Performance¶
article() scores and harvests in one C pass over the parsed tree; trafilatura builds an lxml tree
in Python first and scores it there. Numbers vary with input and hardware.
input |
turbohtml |
|
|---|---|---|
post (4 KiB) |
6.88 µs |
639 µs (93.0x) |
longform (16 KiB) |
26.2 µs |
1.88 ms (71.7x) |
How to migrate¶
Swap the trafilatura import for turbohtml.parse() and read the fields off one article()
call:
from turbohtml import parse
The call mapping:
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
the extracted content body |
|
|
fetch the page yourself ( |
Before and after, harvesting the body and metadata from a full page:
doc = parse(
"<html lang=en><head><title>Comets</title>"
"<meta property=article:published_time content='2024-05-06'></head>"
"<body><article class=post><h1>Comets</h1>"
"<p>By <a rel=author href='/u'>Ada Lovelace</a></p>"
"<p>A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.</p>"
"</article></body></html>"
)
art = doc.article()
print(art.title, "|", art.byline, "|", art.date, "|", art.lang)
Comets | Ada Lovelace | 2024-05-06 | en
Gotchas and pitfalls¶
trafilatura downloads URLs;
article()takes parsed HTML. Fetch the page yourself (urlliborhttpx) and pass the markup toturbohtml.parse().article().datereturns the date exactly as the page declares it (the first of a<time>, anarticle:published_timemeta, or a common date meta such asdate,pubdate,dc.date) without parsing or normalizing it. Wrap it indatetime.date.fromisoformatordateutilfor a real date object, and keep htmldate for pages whose date is only inferable.article().langreports the document’s declared<html lang>attribute, not a language detected from the prose. To infer the language the way trafilatura’s optional language filter does, pass the extractedtexttoturbohtml.detect.detect_language(), which returns an ISO 639-3 code with a confidence.A page with no scoring article leaves
elementNoneandtextempty while still filling the metadata, so branch onart.elementrather than assuming a body.turbohtml returns plain text only. For the Markdown, XML, or JSON that
extract(..., output_format=...)produces, serialize thearticle()fields (and theelementsubtree) yourself.