Extract the main article¶
Isolate the article from the navigation, sidebars, and boilerplate with main_content(), pull it
with its metadata, and classify paragraphs one at a time.
Extract the main article¶
main_content() returns the dominant content element (the article body with the navigation,
sidebars, advertising and comment boilerplate scored out), so you can work on just the prose. It is the role
resiliparse fills with its main-content extractor.
main_text() is the shortcut that renders that element with to_text():
import turbohtml
page = turbohtml.parse(
"<body>"
"<nav><a href='/'>Home</a> <a href='/about'>About</a></nav>"
"<article class='post'><h1>Comets</h1>"
"<p>A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.</p>"
"<p>The tail always points away from the Sun, pushed out by the solar wind and radiation.</p>"
"</article>"
"<aside class='sidebar'><p>Related: meteors, asteroids, the Oort cloud, and more links here.</p></aside>"
"</body>"
)
print(page.main_content().tag)
print(page.main_text())
article
Comets
A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.
The tail always points away from the Sun, pushed out by the solar wind and radiation.
The score is a content-density heuristic: long paragraphs with prose punctuation raise their container, while a class or
id like sidebar, comment or nav lowers it or drops the subtree outright. A page with no real article (only
short snippets or pure navigation) yields None from main_content and "" from main_text, so guard the
result:
stub = turbohtml.parse("<nav><a href='/'>Home</a></nav>")
print(stub.main_content())
None
Extract the article with metadata¶
article() returns an Article record: the scored content element and its plain
text, plus the page metadata harvested beside it – title, byline, date, description and lang. This
is the one call that replaces trafilatura or newspaper3k, and folds in the publication-date lookup (the htmldate use
case):
import turbohtml
page = turbohtml.parse(
"<html lang='en'>"
"<head><title>Comets — Astronomy Today</title>"
"<meta property='og:description' content='A short guide to comets and their tails.'>"
"<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'>Ada Lovelace</a></p>"
"<p>A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.</p>"
"<p>The tail always points away from the Sun, pushed out by the solar wind and radiation.</p>"
"</article></body></html>"
)
art = page.article()
print(art.title)
print(art.byline)
print(art.date)
print(art.description)
print(art.lang)
print(art.element.tag)
Comets
Ada Lovelace
2024-05-06
A short guide to comets and their tails.
en
article
Each field is harvested from the first source that supplies it, so a partial page still yields what it can. title
prefers the first <h1>, then og:title, then <title>; byline a rel="author" link, then a author
meta, then article:author; date a <time> (its datetime or text), then article:published_time, then a
common date meta; description og:description then a description meta; and lang the <html lang>
attribute. A field with no source is None, and a page with no article body leaves element None and text
empty while the metadata is still filled:
bare = turbohtml.parse("<html lang='fr'><head><title>Sommaire</title></head><body><p>x</p></body></html>")
art = bare.article()
print(art.element, repr(art.text), art.title, art.lang)
None '' Sommaire fr
Classify paragraphs individually¶
turbohtml.extract.boilerplate() gives the per-paragraph view of the same scoring: it segments the page into
paragraph units and marks each one good or boilerplate, the call shape justext and boilerpy3 expose. Units
outside the content body are boilerplate; units inside it must still clear the length and link-density thresholds a
Extraction config carries:
from turbohtml.extract import Extraction, boilerplate
page = (
"<body><nav><ul><li><a href='/'>Home</a></li><li><a href='/faq'>FAQ</a></li></ul></nav>"
"<article class='post'><h1>Comets</h1>"
"<p>A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.</p>"
"<p>Share this!</p>"
"</article></body>"
)
for paragraph in boilerplate(page):
print(paragraph.is_boilerplate, paragraph.is_heading, paragraph.text)
True False Home
True False FAQ
False True Comets
False False A comet is an icy body that releases gas, forming a visible tail, as it nears the Sun.
True False Share this!
The good paragraphs concatenate to the article, so "\n".join(p.text for p in boilerplate(page) if not
p.is_boilerplate) is the justext extraction idiom. Extraction.justext() mirrors justext’s stricter defaults (a
70-character floor and 0.2 link density), and Extraction(keep_headings=False) subjects headings to the length floor
like any prose, justext’s no_headings mode.
To turn those spans into something printable, pass the returned (text, labels) pair to one of the two output
processors. turbohtml.annotation_surface() groups each label’s matched substrings into a dict, in document order,
the surface forms an NLP or information-extraction pipeline consumes:
import turbohtml
text, labels = turbohtml.parse("<h1>Q3</h1><p>Up <b>12%</b> on the year.</p>").to_annotated_text({
"h1": ["heading"],
"b": ["metric"],
})
print(turbohtml.annotation_surface(text, labels))
{'heading': ['Q3'], 'metric': ['12%']}
turbohtml.annotation_tags() weaves the spans back into the text as inline <label>...</label> markup. The
innermost span always closes first, so properly nested spans stay well-formed:
text, labels = turbohtml.parse("<p>a <b><i>both</i></b> c</p>").to_annotated_text({"b": ["bold"], "i": ["italic"]})
print(turbohtml.annotation_tags(text, labels))
a <italic><bold>both</bold></italic> c