From extruct¶
extruct pulls the machine-readable metadata a page embeds: JSON-LD,
Microdata, RDFa, microformats, the OpenGraph/Twitter card tags, and Dublin Core. You call one extract(html) entry
point, optionally naming a syntaxes list, and get back a dict keyed by syntax name. Under the hood it builds an lxml
tree once and then runs a separate extractor per syntax you asked for, each on top of lxml, w3lib, mf2py,
and an RDFa/DublinCore stack.
It is a scraping-pipeline staple, developed by Zyte (formerly Scrapinghub) and used alongside Scrapy to lift structured
product, article, and event data off crawled pages. turbohtml covers the same ground from the other direction: it is a
WHATWG HTML parser first, and structured_data() reads every supported format off the already
parsed tree in a single C walk, so extraction is not a second tree build but a walk of the one you already have.
turbohtml vs extruct¶
Dimension |
turbohtml |
extruct |
|---|---|---|
Scope |
Full WHATWG parser; structured data is one feature of many |
Focused metadata extractor, one job |
Feature breadth |
JSON-LD, Microdata, OpenGraph/Twitter, RDFa, Dublin Core; microformats reserved (empty list) |
JSON-LD, Microdata, RDFa, OpenGraph, microformats, Dublin Core |
Performance |
One C walk of the parsed tree; ~9-11x faster on a combined page |
Builds an lxml tree, then one extractor per requested syntax |
Typing |
Frozen, fully typed records ( |
Plain dicts and lists, no shipped type hints |
Dependencies |
Zero runtime deps (self-contained C extension) |
|
Maintenance |
Actively developed single extension |
Mature, Zyte-backed, slower cadence |
Feature overlap¶
The shared surface you can port one-to-one:
extruct.extract(html)->structured_data(), one call gathering every format.syntaxes=["json-ld"]->json_ld(), each<script type="application/ld+json">block decoded with the standard libraryjson.syntaxes=["opengraph"]->opengraph(), theog:/twitter:<meta>tags.syntaxes=["microdata"]->microdata(), returningMicrodataItemrecords withitemtype,itemid, and the nesteditempropvalues.Both skip a malformed JSON-LD block rather than raising, the forgiving default
extructalso takes.
What turbohtml adds¶
A single walk over the parsed tree returns every format at once as a frozen, fully typed
StructuredDatarecord whose six fields you read by attribute, so reading a result never depends on which extractors you enabled.The locating runs in the C core under the per-tree critical section; only JSON-LD decoding stays in Python.
The records hold no reference back into the tree, so they outlive the document they came from.
Standalone string entry points
turbohtml.extract.microdata()andturbohtml.extract.opengraph()cover themicrodataandopengraphlibraries’ call shapes on the same C walk, so an extruct migration also folds those in.No third-party runtime dependency: no lxml, w3lib, or mf2py to install or pin.
What turbohtml also covers now¶
RDFa:
structured_data()fillsrdfawithRdfaItemrecords (also viardfa()). Rather than extruct’s expanded JSON-LD triples, turbohtml returns the Microdata item shape –type/resource/properties– withpropertykeys andtypeofIRIs expanded against the in-scope@vocaband@prefix(the RDFa 1.1 initial context seeds the well-known prefixes).Dublin Core:
dublin_core()(anddublin_core) maps thedc.*/dcterms.*<meta>names, lower-cased, to their content.
What extruct has that turbohtml does not¶
microformats: a documented later phase.
structured_data()returns themicroformatsfield as an empty list today. Keep extruct for that syntax until the phase lands; the field name is already in place so code that reads it will not break.RDFa as expanded triples: extruct returns RDFa as JSON-LD-expanded triples via
pyRdfa; turbohtml returns the Microdata-shapedRdfaIteminstead, and does not expand CURIEs embedded in arbitrary IRIs or surface thedatatypetype IRI (typed literals come back as their lexical string).``uniform`` / ``return_html_node`` options: no equivalent. turbohtml’s output shape is fixed per format; there is no normalized cross-syntax view or embedded lxml node handle.
Performance¶
extruct starts from the raw HTML string, so it parses before it extracts: it builds an lxml tree and runs a separate
extractor per syntax, where structured_data() parses to the WHATWG tree and gathers every
format in one C walk. On a product page carrying JSON-LD, Microdata, and OpenGraph at once, the single pass runs roughly
nine to eleven times faster:
input |
turbohtml |
|
|---|---|---|
product |
6.22 µs |
64.3 µs (10.4x) |
catalog 8 KiB |
56.3 µs |
560 µs (10.0x) |
How to migrate¶
Swap the import and the extractor call for a parse plus a method on the document.
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
the |
Before, with extruct, each syntax comes back under a string key in a dict:
import extruct
html = (
'<head><meta property="og:title" content="Widget"></head>'
'<body><script type="application/ld+json">{"@type": "Product"}</script>'
'<div itemscope itemtype="https://schema.org/Offer"><span itemprop="price">9.99</span></div></body>'
)
data = extruct.extract(html, syntaxes=["json-ld", "opengraph", "microdata"])
print(data["json-ld"])
print(data["opengraph"])
print(data["microdata"])
After, one walk returns a typed record whose fields you read by attribute:
from turbohtml import parse
doc = parse(
'<head><meta property="og:title" content="Widget"></head>'
'<body><script type="application/ld+json">{"@type": "Product"}</script>'
'<div itemscope itemtype="https://schema.org/Offer"><span itemprop="price">9.99</span></div></body>'
)
data = doc.structured_data()
print(data.json_ld)
print(data.opengraph)
offer = data.microdata[0]
print(offer.type, offer.properties)
[{'@type': 'Product'}]
{'og:title': 'Widget'}
https://schema.org/Offer {'price': ['9.99']}
The per-format helpers return the same plain objects, holding no reference back into the tree, so you can keep them after the document is gone:
doc = parse('<script type="application/ld+json">{"@type": "Article", "name": "Hi"}</script>')
print(doc.json_ld())
[{'@type': 'Article', 'name': 'Hi'}]
Gotchas and pitfalls¶
The OpenGraph result is a flat
{key: value}mapping, notextruct’s list of namespaced property tuples, andog:andtwitter:tags share the one mapping because pages mix thepropertyandnameattributes freely. When a key repeats, the last occurrence wins; readjson_ld()when you need every occurrence of a repeated key.microformatsis a later phase:structured_data()returns it as an empty list today, so code that reads it will not break when it lands, but the values are not there yet. RDFa and Dublin Core are populated, inrdfaanddublin_core.RDFa
propertykeys andtypeofIRIs are expanded against the in-scope@vocab/@prefix(with the RDFa 1.1 initial context seeding the common prefixes); an undeclared prefix or a bare term with no vocabulary in scope stays verbatim. Passbase_url=tordfa()to absolutize theresource/href/srcIRIs.A JSON-LD block whose body is not valid JSON is skipped rather than raising, matching
extruct’s default error handling;extruct’serrors="strict"mode has no turbohtml equivalent. Pass the raw<script>text tojsonyourself if you need to see the decode error. A block whose payload is a scalar ornullis also dropped, since it is not a JSON-LD node object.Extracted URL values are returned verbatim by default. Pass
base_url=tostructured_data()(or toopengraph()/microdata()) to absolutize the Microdata and OpenGraph URL-valued fields against it, the wayextruct’sbase_urlargument resolves them; a<base href>refines it. JSON-LD is left verbatim, so resolve any@idyourself withurllib.parse.urljoin().turbohtml decodes the string to the WHATWG tree;
extructdecodes bytes withw3libper itsencodingargument. Hand turbohtml already-decodedstrand let it apply the WHATWG encoding rules rather than pre-forcing a codec.