goose3 Returned No Labeled Boilerplate. It Also Returned Nothing Twice.

Last Updated on August 17, 2026
goose3 Returned No Labeled Boilerplate. It Also Returned Nothing Twice.
AI Summary
Across the fixtures where a leak is even definable, goose3 returned 1.0000 content-token precision and zero contaminating tokens. Not one word of nav, ad, sidebar, comment or promo made it into its output. Nothing else in a six-library comparison matched that. It also has the worst article recall in the set — 0.8243 across all 22 fixtures, against 1.0000 for Mozilla Readability — because on two of them it returned an empty string. Those metrics are coupled by the scoring rule: an empty result contributes nothing to conditional precision, while recall records the miss.

Across the fixtures where a leak is even definable, goose3 returned 1.0000 content-token precision and zero contaminating tokens. Not one word of nav, ad, sidebar, comment or promo made it into its output. Nothing else in a six-library comparison matched that.

It also has the worst article recall in the set — 0.8243 across all 22 fixtures, against 1.0000 for Mozilla Readability — because on two of them it returned an empty string.

Those metrics are coupled by the scoring rule: an empty result contributes nothing to conditional precision, while recall records the miss.

What goose3 is

goose3 is the Python-3 continuation of a lineage that starts with Gravity Labs' Goose in Scala and passes through python-goose. It is an article extractor with metadata, not a text-dumper: construct a Goose, call extract(), and you get back an Article object with around twenty-eight accessible fields — cleaned text, title, authors, publish date, top image, meta description, tags, links, tweets, and more.

Official reference: goose3's official repository.

System diagram: From HTML to Article Fields

Version tested: 3.1.22, Apache-licensed, 912 GitHub stars, last pushed 2026-07-23 — actively maintained at time of testing. Python 3.14.2.

The API is two calls and one obligation:

from goose3 import Goose
g = Goose()
try:
    article = g.extract(raw_html=html)
    text = article.cleaned_text
finally:
    g.close()          # close explicitly after use

That close() is worth flagging because it is easy to forget and nothing warns you. This review did not run a loop probe to quantify retained sessions, connections, or memory when closing is skipped, so “resource leak” would be stronger than the evidence. Treat explicit closing as the lifecycle requirement shown by the API usage here.

The trade-off, measured

Six extractors, one annotated fixture set, one scorer. Every block of every fixture is labelled article or boilerplate and carries a unique sentinel token, so "did it recover this unit" is exact substring membership rather than a similarity score.

LibraryArticle recall (all 22)Boilerplate leakContent-token precisionContaminating tokensProduced output
Readability1.00000.23530.91093522/22
trafilatura0.98650.05880.9411422/22
newspaper4k0.98650.00000.9452022/22
resiliparse0.90540.05880.9381722/22
jusText0.83780.47060.87607419/22
goose30.82430.00001.0000020/22

sixway-scores.json. Recall is over all 22 fixtures; leak rate and precision over the 11 that contain both article and boilerplate units.

Read the precision column with the last column, always. Precision here is conditional on producing output — a library that returns an empty string on a fixture contributes nothing to the numerator and nothing to the denominator, so declining is free in this average. The numerator is the multiset overlap between extracted non-stopword tokens and labeled article tokens; the denominator is every extracted non-stopword token. “Contaminating tokens” is narrower: overlap with the labeled boilerplate tokens only. An extra extracted token that matches neither labeled article nor labeled boilerplate lowers precision without increasing that contamination count; repeated tokens beyond the article multiset can do the same. That is why newspaper4k can show 0 contaminating tokens and precision below 1.0000. goose3's 1.0000 was scored on 10 of the 11 fixtures; Readability, trafilatura, newspaper4k, and resiliparse were scored on 11 of 11.

The 1.0000 remains useful within this synthetic set. Across the ten scored fixtures, goose3 emitted none of the labeled boilerplate tokens; Readability emitted 35 on the same pages. If a model consumes the output, this means no token spend on the boilerplate labels registered in those ten fixtures. It does not establish zero waste on real pages, and an empty result can add fallback or retry cost elsewhere in the pipeline.

The two silences, and what they mean

goose3 returned nothing on exactly two fixtures. One of them is defensible and one is a genuine limit.

The near-empty document. A page with one 32-character article unit. goose3 declines. So does jusText. In this fixture set, Readability produced output on all 22 pages, so its result does not support goose3's silence here. Whether declining this tiny document is acceptable depends on the caller's minimum-content contract.

System diagram: Treat Empty Output as Failure

The article made entirely of <li> elements. Six article units, none of them in a <p> tag. goose3 returns an empty string.

That second one puzzled me, because goose3's own default configuration reads parse_lists=True. So I probed it — three configurations against a working control, because one unproductive run is not a finding about a library:

ConfigurationList-only page<p> control
defaults0 chars937 chars
strict=False0 chars937 chars
parse_lists=True (explicit)0 chars937 chars

list-only-probe.json.

Zero in all three, while the control returns 937 characters in all three. So parse_lists=True governs whether lists are kept inside an article goose3 has already located — it does not let the candidate scorer treat a list as the article. goose3's node scoring needs paragraph-shaped blocks to find the body at all, and a page whose body is a list has none.

The supported result is narrower: a body shaped like this synthetic fixture — six article units, all <li>, with no paragraph candidate — returned an empty string. Changelogs, API references, recipes, FAQ pages, and comparison posts are sensible risk samples for a real-page replay because they may be list-heavy, but this one fixture does not establish that those page categories fail generally.

The empty string is machine-detectable only if the caller validates non-empty output. It is easier to gate than plausible-looking text that contains none of the article, but it is still a silent failure in exception-only monitoring. A production caller needs a minimum-output check and a fallback or explicit failed-page record.

Setup reality

pip install goose3 pulls 16 packages and 44.3 MiB in about 6 to 9 seconds. Cold import measured in a fresh subprocess: 2.181 s.

Official reference: goose3 on PyPI.

LibraryPackagessite-packagesCold importExtraction p50
resiliparse521.0 MiB0.015 s0.06 ms
jusText322.4 MiB0.777 s0.56 ms
goose31644.3 MiB2.181 s1.85 ms
newspaper4k2247.5 MiB2.812 s2.69 ms
trafilatura1769.9 MiB1.584 s0.51 ms

install-and-import.json. Each library in its own empty virtualenv, so no footprint is inherited from a sibling.

Middle of the pack on weight, middle on speed. It installed and imported cleanly on Python 3.14.2, which is not universal in this category.

Three defaults worth knowing before you deploy

System diagram: Three defaults worth knowing before you deploy

Reading the shipped Configuration object rather than the docs turns up nineteen settings. Three of them will surprise someone.

It announces itself. browser_user_agent defaults to Goose/3.1.22. If you let goose3 do its own fetching, every server you touch logs the library name and its exact version. That is honest, and it is also a fingerprint. Set it deliberately or fetch the HTML yourself and pass raw_html.

It points at a MacPorts binary. imagemagick_convert_path defaults to /opt/local/bin/convert and imagemagick_identify_path to /opt/local/bin/identify. On my machine neither exists — /opt/local is MacPorts, which most people do not have; Homebrew puts binaries in /opt/homebrew. The default is inert unless you turn on image fetching (enable_image_fetching defaults to False, sensibly), but if you do turn it on expecting top-image extraction to work, this is where it quietly will not.

It assumes English. target_language defaults to en with use_meta_language=True, so it will follow the page's own declaration when there is one and fall back to English when there is not. Fine for English-language work, worth setting explicitly for anything else.

The rest are reasonable: parser_class is lxml, http_timeout 30 seconds, strict on, log_level ERROR, parse_headers and keep_footnotes on, images_min_bytes 4,000.

Memory, and what broken HTML does to it

Two things every review in this batch listed as untested, now measured.

The broader stress-test context is in the ten-library memory and malformed-HTML comparison.

Peak resident memory, via /usr/bin/time -l, one fresh process per cell — the import floor is what the library costs loaded and idle, the peaks include the document.

LibraryRuntimeImport floor226 KB peak10 MB peak
html2textpython3.1418.719.971.2
pyquerypython3.1430.333.9172.5
resiliparsepython3.1420.525.1225.1
markdownifypython3.1423.928.9278.5
goose3python3.1444.152.4398.5
cheerionode2266.876.5398.5
justextpython3.1430.336.6431.2
newspaper4kpython3.1452.661.8668.5
trafilaturapython3.1452.564.8927.1
turndownnode2247.868.42947.1

memory-results.json. Python and Node baselines are not comparable to each other; the interpreter is inside both.

goose3's floor is 44.1 MiB and it peaks at 398.5 MiB on a 10 MB document. Its import floor is the third highest among the Python libraries shown, which is worth knowing for a cold-start-sensitive deployment.

Broken HTML. Twelve documents each breaking exactly one thing — unclosed tags, mis-nested inline elements, unquoted attributes with spaces, stray closers, no <html> at all, duplicate attributes, a document truncated mid-tag, bad entities, an unclosed <script>, a lying charset declaration, a comment containing markup, and 600 levels of nesting — plus two well-formed controls at matched sizes, because "it returned nothing" only says something about malformedness if the library is not also silent on a clean document of the same size.

goose3 raised on 0 of 14 and returned nothing on 10, recovering 2/33 sentinels across the broken fixtures (malformed-results.json). One fixture is excluded from that count: per HTML5 everything after an unclosed <script> is script content, so losing it there is correct and recovering it is the deviation.

Pros and cons

In its favour. Zero labeled boilerplate tokens across the ten content-fidelity fixtures where it produced output. Around twenty-eight article fields are available, although their accuracy was inventoried rather than scored. Sensible image-fetching default (off). Clean install on Python 3.14. Actively maintained. Apache-2.0. An empty result is straightforward to gate when the caller checks it explicitly.

Against it. The lowest article recall in the set at 0.8243, driven entirely by returning nothing at all rather than by returning the wrong thing. A page whose article is a list produces an empty string regardless of configuration. 44.3 MiB and a 2.2-second cold import are heavy next to resiliparse's 21.0 MiB and 15 ms. Requires close(). Two defaults point at things that are wrong on most machines.

Who should use it, and who should not

Use goose3 as a candidate when extracted text goes into a model or database where labeled boilerplate is costly and the pages are conventional articles with paragraphs. On this fixture set it emitted no labeled boilerplate tokens when it answered. The metadata surface is available but unvalidated here; title, author, date, and image accuracy need separate ground-truth fixtures before they become a selection benefit.

Skip it if your corpus is list-heavy — you will get empty strings and no explanation. Skip it if cold-start cost matters, where resiliparse imports 145× faster. And skip it if you need an answer on every page, because "no answer" is a real outcome here: two of 22 fixtures, and both silently, in the sense that an empty string is not an exception.

A pairing worth testing: goose3 as the primary with a fallback when cleaned_text is empty or below your minimum-content rule. Readability recovered every article unit in this 22-fixture set, including both of goose3's empty cases. That synthetic result supports the architecture pattern, not a promise that the fallback never misses on real pages.

Where a managed API fits

This benchmark tested goose3's raw_html extraction path: HTML had already been acquired before goose3 saw it. goose3 also has its own network-fetch path, as its User-Agent setting shows, but that path was not tested here. JavaScript rendering and anti-bot behavior were not tested either.

For the same fixtures across all six extractors, see the six-library extraction comparison.

A managed fetch/render/extraction service, including our own Thunderbit, covers a different responsibility boundary. Thunderbit was not benchmarked in this test. The relevant distinction is supplied-HTML article extraction versus a hosted service that acquires and processes a URL; this article provides no same-metric performance or quality comparison.

The fair comparison: goose3's field set is fixed and article-shaped, which is exactly right when your pages are articles and wrong when they are product listings. If you already hold the HTML and your pages are articles, goose3 is free and very clean.

For the hosted field, our web scraping API roundup is the wider view; for self-hosted alternatives, the open-source scraper pillar. If the text is bound for a model, converting HTML to Markdown in Python covers where fidelity actually gets lost.

Try Thunderbit for Web Data Extraction

Should you use goose3?

Yes, if paragraph-shaped articles match the workload and the caller treats empty output as a failed extraction rather than success.

On this fixture set, goose3 emitted no labeled boilerplate tokens when it answered and returned two empty strings. One was a near-empty page and one was the list-only synthetic body. That is a precision/coverage trade, not proof of a product-wide temperament.

If recall matters more, test a fallback with an explicit minimum-output guard. Readability recovered every article unit in these 22 fixtures; newspaper4k showed zero labeled boilerplate-unit leak and 0.9865 recall while producing output on all 22. Those results rank this synthetic set at defaults, not unknown production workloads.

goose3 earns its place when the cost of a wrong word is higher than the cost of a missing page.

Try Thunderbit for Web Data Extraction Get Started Free

FAQs

Is goose3's perfect precision real, or an artifact of it declining? Both, and the two are separable. It was scored on 10 of the 11 fixtures that contain boilerplate, so one fixture is missing from the average — that part is an artifact of declining. But on those ten it returned zero contaminating tokens against deliberately adversarial boilerplate, where Readability leaked 35. The precision is real for pages it answers; the recall column is where declining shows up.

Why does goose3 return nothing on a page whose article is a list? Its candidate scoring needs paragraph-shaped blocks to locate the article body, and a page built from <li> has none. The parse_lists=True default does not change this — I tried it explicitly, plus strict=False, and got zero characters in all three configurations while a <p>-based control returned 937 in all three. parse_lists decides whether lists are kept inside an article that has already been found.

Do I have to call close()? Close it explicitly with try/finally, as shown above. This review did not measure what accumulates when closing is omitted, so it does not claim a quantified loop leak; it does establish that Goose has a lifecycle the caller must manage.

What does goose3 send as its User-Agent? Goose/3.1.22 by default — the library name and exact version. That only applies when you let it fetch, and passing raw_html sidesteps it entirely. If you do let it fetch, set the User-Agent deliberately; the default tells every server you touch precisely what is calling.

What did this review not test? Real-world pages, at all — these are controlled fixtures. Multilingual extraction, despite target_language being a first-class setting. The metadata fields (title, authors, date, top image) were inventoried but not scored for accuracy. Image fetching, which is off by default and whose ImageMagick paths point at a package manager most machines do not have. Concurrent or sustained-load memory behavior, plus throughput under load; the memory table measured one fresh process handling one document.

Ke
Ke
CTO at Thunderbit | Senior Data Scientist & ML Expert With nearly a decade of experience in machine learning and data science, Ke Shen is a Columbia University alumnus and former Senior Data Scientist at Walmart Labs. With deep, peer-recognized expertise in Python, R, Java, and Statistics, he shares battle-tested insights on taking complex AI algorithms from theory to production-grade architecture.
Table of Contents
Thunderbit · AI web data agent

Extract data from any page in 1 click

Trusted by 250,000+ users
free plan available
From webpage to spreadsheet
Describe what you need — Thunderbit's AI Agent scrapes it and exports to Excel, Google Sheets, Airtable, or Notion. Free to start.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week