Building a Self-Healing Scraper: Let AI Fix Your Selectors When Sites Change A developer has outlined a self-healing web scraping architecture that uses an LLM only as a fallback when hardcoded CSS selectors break, rather than for every request. The approach validates extracted data against a Pydantic schema to detect silent failures, then trims HTML by 60–80% and asks a model to recover the value and generate a replacement selector, which is verified and saved for subsequent runs. The design keeps the expensive model call on the exception path so stable sites never incur LLM costs. Your scraper doesn't fail with a stack trace. It fails silently. A site ships a redesign, renames one CSS class, and your .css "span.price" quietly starts returning None . Nothing crashes. The pipeline keeps running, writing rows of empty prices into your database, and you find out a week later when someone downstream asks why the dashboard is flat. The hard part of scraping was never writing the first version. It's keeping it alive as the target keeps moving. In this tutorial we'll build a scraper that notices when its own selectors break, uses an LLM to find the data again and generate a fresh selector , then saves that selector so the next run is cheap again. The key idea, before we write any code: the LLM is not doing the scraping. It only runs when the normal path breaks. That one constraint is what keeps this practical instead of a way to set money on fire. Here's the whole control flow: request → try SAVED selectors │ ┌───────┴────────┐ works fails validation │ │ return trim HTML → ask LLM for value + new selector │ verify the new selector actually works │ save it → return The expensive branch — trimming HTML, calling a model — is the exception, not the rule. On a stable site it never fires. It only kicks in on the day a site changes, and after it heals once, every following request is back on the fast path. Start with something completely ordinary. httpx to fetch, parsel to parse, hardcoded selectors. python import httpx from parsel import Selector DEFAULT SELECTORS = { "title": "h1.product-title", "price": "span.price", } def extract html: str, selectors: dict - dict: tree = Selector html return { field: tree.css f"{sel}::text" .get for field, sel in selectors.items } Nothing new here. This is the scraper you already write. The interesting part is how we decide whether its output is any good. Most people check for breakage with if result is None . That catches a selector that matches nothing, but it misses the nastier case: a selector that matches the wrong element and returns plausible-looking garbage. A schema is a much stronger health check. Define what a valid result looks like with pydantic , and let validation be your detector: python from pydantic import BaseModel, ValidationError, field validator class Product BaseModel : title: str price: float @field validator "title" @classmethod def title not blank cls, v: str - str: if not v or not v.strip : raise ValueError "empty title" return v.strip def validate raw: dict - Product | None: clean = {} for key, value in raw.items : if value is None: return None if key == "price": value = "".join c for c in value if c.isdigit or c == "." clean key = value try: return Product clean except ValidationError: return None Now "is my scraper healthy?" has a precise answer: validate extract ... returns a Product , or it returns None . A price of "Add to cart" fails the float coercion. An empty title fails the validator. That None is our trigger. When validation fails, we hand the problem to a model. Two things matter here: cost control and trust. Cost control means never sending raw HTML. A product page is mostly