Self-Healing Web Scrapers: Selectors That Fix Themselves
Build a self-healing web scraper that catches broken selectors and repairs them. Real run: CSS learned from three pages, cached extractions at $0.

On this page
A site renames one CSS class and your .price_color starts returning
nothing. Nothing crashes: the pipeline keeps writing empty prices for a
week until someone notices the report is blank. A self-healing web
scraper exists to end exactly that story: it validates its own selectors
on every run, and when they break, it repairs them without you in the
loop.
I ran the whole thing end to end on books.toscrape.com, a public practice sandbox: a crawl taught itself CSS selectors from three sample pages, every later page extracted for free, and a page the crawl never visited came back correct with zero LLM tokens. Here is the setup, the output, and the two places this approach genuinely fails.
Before you start
- Magpie: a local-first scraper in Go
(github.com/motherlodelab/magpie,
AGPL-3.0-or-later or commercial). Build it:
go build -o magpie ./cmd/magpie. No runtime dependencies. - An LLM key for the learning runs: any of
anthropic,openai,openrouter, or a local Ollama model. The learning phase needs a model; the extraction phase doesn’t. - A schema: a small YAML describing the fields you want.
- A target you’re allowed to scrape. books.toscrape.com is built for this. Point the machinery at real sites only where their terms and robots.txt allow it.
The Magpie commands below come from the 2026-10-16 core build. Everything shown is real output, abridged only where marked.
Step 1: Validate selectors before you trust the data
Detection is the unglamorous half and the more important half. Repair is worthless if you never notice the break, and the default scrape pipeline is silent: a dead selector yields nulls, nulls become empty strings, and your database fills with confident garbage.
So the first move is measuring per-field failure, per run. Magpie does this in a sliding window: it records whether each schema field came back null on every page, and a field is flagged when more than 30% of the last 50 observations are null. Two details in the code matter. A field needs at least 10 observations before it can trigger, so one fluke page can’t fire a repair. And the window resets after triggering, so a field that stays broken re-alarms on fresh evidence instead of screaming on every page.
You can read the current state any time:
magpie cache inspect --domain books.toscrape.com
domain=books.toscrape.com schema_hash=ab2a88968221 samples=3 synthesized=2026-10-16T03:04:35Z
field=availability type=css expr=.instock.availability null_rate=0.00
field=price type=css expr=.price_color null_rate=0.00
field=title type=css expr=h1:nth-of-type(1) null_rate=0.00
That null_rate column is your dashboard. If you’re not using Magpie,
the poor man’s version is one assertion per field per run. Fail loudly
when price is missing on a page that rendered fine. It catches most of
what you’ll hit, for ten lines of code. The DIY escalation is the
try/except that ships the page HTML plus the dead selector to a model
with the instruction “return a working CSS selector” — it works, and
you’ll rewrite it for every project. What neither can do is fix anything
on its own, which is the next step’s job.
Step 2: Pick your repair strategy
Three designs dominate, and they fail differently.
| Approach | How it heals | Needs LLM at extract time | Blind spot | Representative tools |
|---|---|---|---|---|
| Fuzzy re-location | Stores an element fingerprint (tag, attributes, position, text); re-finds the element after renames by similarity | No | Total redesigns; new sites | Scrapling (Python, BSD-3-Clause, ~88k stars), Healenium (Java/Selenium, Apache-2.0) |
| LLM re-synthesis | Detects null-rate breakage, then re-derives selectors against recently captured ground truth | No, after learning | Fields invisible in the page text you extract from | Magpie (Go, AGPL-3.0-or-later or commercial) |
| No selectors at all | Nothing to break; the model reads the page every run | Yes, every page | Cost scales with volume; wrong answers look like right ones | Kadoa (hosted SaaS), Firecrawl’s /extract |
The deeper comparison between the first two schools is its own article: LLM extraction vs CSS selectors.
My ranking after this test: if you re-scrape the same sites weekly, learn-once selectors with self-healing win on cost: the cache does the work and the model only wakes up when something breaks. If you point at a different URL every time, selector-free extraction is simpler and the per-page cost is just what it costs. Fuzzy re-location is the right budget pick when you can’t run a model at all, but it has a hard ceiling: a redesign that changes structure, not just class names, defeats it. Where the selector-free products fit in the broader toolset, the AI web scraper overview lays it out.
Step 3: Let the scraper learn the site
Magpie’s synthesis works like this: the first pages of a crawl run through plain LLM extraction. Pages where every required field comes back non-null are kept as ground-truth samples (up to three). At three samples, Magpie proposes candidate CSS selectors (heuristics first, model proposals when heuristics miss) and keeps only the candidates that reproduce the model’s own answer on the retained pages. Those selectors are cached per domain and per schema hash, so changing your schema starts a fresh cache instead of poisoning the old one.
The schema, for a book detail page:
title: Book detail page to structured record
type: object
required: [title, price, availability]
properties:
title:
type: string
description: Book title as shown in the page heading
price:
type: string
description: Price including currency symbol, e.g. £51.77
availability:
type: string
description: Stock status text, e.g. "In stock (22 available)"
The crawl. Detail pages only, category indexes excluded:
magpie crawl https://books.toscrape.com/catalogue/category/books/travel_2/index.html \
--schema book.yaml \
--include '**/catalogue/**' --exclude '**/category/**' \
--max-pages 14 --max-depth 3 \
--provider openrouter --model anthropic/claude-haiku-4.5 \
--out books.jsonl
What you should see: one honest warning per imperfect field, then a clean run count.
warning: selector: field "price" agrees on 2/3 via heuristic; caching anyway
warning: selector: field "title" agrees on 2/3; caching anyway
crawl: run_id=e3eba3b2444b68e6-14734 pages_ok=14 pages_err=0 records=14
What it means: availability synthesized at full agreement, and
price and title were cached at 2-of-3 sample agreement. That’s the
system telling you the selector is probably right but not proven. For
prices I’d spot-check before trusting it unattended. All 14 records came
back with real data: £30.54 for The Great Railway Bazaar, “In stock
(6 available)”, and so on. The learning crawl ran 14 LLM extractions at
roughly $0.001 each on claude-haiku-4.5; the whole demo, mistakes
included, cost me about four cents.
The failure worth seeing
My first attempt had a fourth field, rating, and it failed in an
instructive way: every book came back rated 0. On books.toscrape.com the
rating lives only in a CSS class (star-rating Three), an empty
paragraph. Cleaned markdown has no classes and no text for that element,
so the model never saw the signal, returned a guess, and the synthesized
selector could not agree with a wrong ground truth. Magpie refused to
cache rating and fell back to per-page LLM extraction for it, which is
the correct behavior, but no amount of healing fixes a field the
extracted text doesn’t contain. (The markdown-vs-HTML tradeoff here is
its own topic:
markdown vs HTML for LLMs.)
Step 4: Prove the cache on a page it never saw
The crawl wrote selectors into a local SQLite cache. Single scrapes read that cache (it’s read-only), so one-off scrapes benefit from the crawl’s learning. Pick a book the crawl never touched:
magpie scrape https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html \
--schema book.yaml \
--provider openrouter --model anthropic/claude-haiku-4.5
{
"extracted": {
"title": "Tipping the Velvet",
"price": "£53.74",
"availability": "In stock (20 available)"
},
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"usd_estimate": 0,
"attempts": 0
},
"from_cache": true
}
from_cache: true, zero tokens, zero dollars, correct extraction. That
is the entire economic argument for a self-healing web scraper over
selector-free extraction: the model was paid during learning, and every
run after that is CSS. When you’re debugging and want to bypass the
cache deliberately, add --no-cache.
Step 5: What happens when the site breaks for real
Suppose the store renames .price_color to .product-price next month.
The loop, in order:
- The null-rate window trips once
pricenulls exceed 30% of the last 50 pages (minimum 10 observations). - Magpie first tries relocation: if it has an element fingerprint for
the field, it re-finds the element on recent samples and checks the
result against ground truth it already paid for. This tier costs no
LLM tokens; the log says
relocated without LLM. - Relocation failed → re-synthesize just that field from retained samples.
- Half or more of the fields broken → the layout didn’t drift, it changed. The whole selector template is re-synthesized instead of patching a corpse.
There’s a manual override, and it bit me, so here’s the honest version:
magpie cache heal --domain books.toscrape.com --schema book.yaml \
--seed-url https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Output: cache: healed books.toscrape.com (0 fields from 3 samples): zero
fields, and it had overwritten my working cache with an empty doc.
The CLI heal path proposes heuristic candidates only and checks them
against whatever samples it can collect from the single seed URL, which
was not enough. I restored the cache by re-running the Step 3 crawl. So:
treat cache heal as a debugging tool for now, and let the crawl-time
loop do the real healing. Re-check this command after upgrades; it may
gain a model-backed proposer.
Step 6: Make it stick
Three habits turn this from a demo into infrastructure.
Schedule the crawl; keep the learning warm. A weekly cron of the
Step 3 command refreshes both data and selectors. The crawl is
checkpointed and resumable by run_id, and same-host scope plus the
default 1 req/sec rate keep you polite. Scheduling patterns live in
Automated Web Scraping.
Watch the pages you don’t crawl. For a single page you care about
hourly, magpie watch is zero-LLM:
magpie watch https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html \
--once --webhook https://hooks.local/magpie
--once is the cron mode: one check, one snapshot diff, and on change
exactly one webhook POST with {url, changed, old_hash, new_hash, diff}.
Spot-check like an adult. Healing is automation, not truth. Keep one
known record per site (title, price, one odd Unicode character) and diff
it after every repair. A healed selector that grabs the wrong <h1>
produces confident wrong data just as happily as a broken one produces
nulls. Nulls at least are honest. And know the hard edges: this does
not bypass logins you don’t have, bot defenses, or fields that don’t
exist in the extracted text. Sites that fight scrapers are a different
problem with different tools
(scraping without getting blocked
covers the polite end of it).
FAQ
What is a self-healing web scraper?
A scraper that monitors its own extraction quality and repairs broken selectors without human intervention. Typically it tracks per-field failure rates, and when a field starts returning nulls it either re-locates the element by similarity or re-derives the selector from recent page captures. The goal is pipelines that survive site redesigns instead of silently producing empty data.
Do self-healing scrapers need an LLM?
Not always. Scrapling and Healenium relocate broken elements by stored fingerprints and similarity scoring, no model involved. That handles class renames and small moves. An LLM enters when the change is bigger: re-deriving selectors or reading the page directly. The strongest setups use heuristics first and the model as the escalation tier.
How much does self-healing cost?
In my run: about $0.001 per page during the learning crawl (claude-haiku-4.5 via OpenRouter), then $0.00 per extraction from the selector cache, including pages never crawled. The model only wakes up when a field breaks. Selector-free extraction pays the model on every page forever, which is the cost difference that matters at volume.
What if the healed selector grabs the wrong element?
Then you get clean-looking wrong data, which is why the checks never
turn off after a repair. Keep a known-value record per site, diff it
after repairs, and read the null-rate column in cache inspect as your
health dashboard. Magpie caches selectors at full agreement, or below it
with a warning you should not ignore.
Can this run fully offline?
The scraping can; the learning can’t, unless you swap in a local model. Magpie’s provider list includes Ollama, so synthesis and extraction run against a model on your own machine with no API key. Slower and weaker than a hosted model, but nothing leaves your machine.


