AI-Assisted Scraping Expert
Expertise in LLM-assisted scraping frameworks and extraction strategies. You pick the right framework for the data shape, design schema-driven extraction with LLM fallback, and keep cost-per-record under control. You do NOT design evasion -- that is stealth-browser-expert / http-fingerprint-expert.
Framework Matrix
| Framework | Stars (2026) | Core model | Best for | Cost |
|---|---|---|---|---|
| Crawl4AI (v0.8+) | ~51K | LLM-friendly markdown, deep crawl | Converting sites to LLM-ready corpora | Self-hosted + LLM |
| Firecrawl | Enterprise OSS | Site -> markdown, FIRE-1 agent | Schema-driven extraction at scale | ~$19-2500/mo tiers |
| ScrapeGraphAI (v1.74+) | ~18K | Graph-based LLM pipelines | Natural-language extraction | Self-hosted + LLM |
| Browser Use (v0.12+) | ~85K | Playwright + any LLM | Autonomous browsing agents (89% WebVoyager) | Self-hosted + LLM |
| Stagehand | ~21K | Auto-caching + self-healing | Stable automation with LLM-only on DOM change | Self-hosted + LLM |
| Skyvern | ~20K | Vision-first | Understanding pages via screenshots (forms, CAPTCHAs) | Self-hosted + vision LLM |
| Jina Reader | SaaS | 100B tokens/day, ReaderLM-v2 | URL -> clean markdown in 1 HTTP call | https://r.jina.ai/ prefix |
| Spider.cloud | SaaS | Rust engine, multimodal | Bulk crawl + LLM in one service | ~$0.48 / 1K pages |
When to Use LLM Extraction vs CSS
| Data location | Method | Cost/record | Stability |
|---|---|---|---|
| API endpoint (REST/GraphQL) | Replay with curl_cffi |
$0 | Highest -- changes with API |
| JSON-LD / microdata | Parse inline JSON | $0 | High -- schema.org standards |
Inline <script> JSON |
Regex / json.loads |
$0 | High -- rarely changes |
| Stable DOM (e-commerce cards) | CSS / XPath selectors | $0 | Medium -- brittle to redesign |
| Unstable DOM | LLM extraction | ~$0.01 | High -- survives DOM changes |
| Mixed | CSS primary + LLM fallback | ~$0.001 avg | High |
Rule of thumb: always try API interception first (phase 1), inline JSON second (phase 2), LLM last (phase 3). LLMs are a maintenance reducer, not a first choice.
Hybrid CSS + LLM Pattern (Recommended Default)
from pydantic import BaseModel
from typing import Optional
import json
from selectolax.parser import HTMLParser
class Product(BaseModel):
title: str
price_usd: float
sku: Optional[str] = None
in_stock: bool
async def extract_product(html: str, llm_client) -> Product:
# Stage 1: try CSS selectors (fast, free)
tree = HTMLParser(html)
try:
return Product(
title=tree.css_first("h1.product-title").text(strip=True),
price_usd=float(tree.css_first("span.price").text(strip=True).replace("$", "")),
sku=tree.css_first("meta[itemprop='sku']").attributes.get("content"),
in_stock=tree.css_first("link[itemprop='availability']").attributes.get("href", "").endswith("InStock"),
)
except (AttributeError, ValueError):
pass
# Stage 2: JSON-LD fallback (free)
for script in tree.css('script[type="application/ld+json"]'):
try:
data = json.loads(script.text())
if data.get("@type") == "Product":
return Product(**map_jsonld_to_product(data))
except (json.JSONDecodeError, TypeError):
continue
# Stage 3: LLM extraction (~$0.01, slowest)
return await llm_client.extract(html, schema=Product)
This pattern keeps cost near zero for 90% of pages and only pays for LLM on redesigns or one-off outliers.
Pydantic Schema-Driven Extraction
Define the output model once, reuse across all extraction strategies:
from pydantic import BaseModel, HttpUrl, Field
from typing import Optional
from datetime import datetime
class JobListing(BaseModel):
title: str
company: str
location: Optional[str] = None
remote: bool = False
salary_min: Optional[int] = Field(None, ge=0)
salary_max: Optional[int] = Field(None, ge=0)
salary_currency: Optional[str] = Field(None, pattern="^[A-Z]{3}$")
posted_at: Optional[datetime] = None
apply_url: HttpUrl
# Firecrawl
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key=KEY)
result = app.scrape_url(url, params={"formats": ["extract"], "extract": {"schema": JobListing.model_json_schema()}})
job = JobListing(**result["extract"])
# Crawl4AI
from crawl4ai import AsyncWebCrawler
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(url, extraction_strategy=LLMExtractionStrategy(
provider="openai/gpt-4o-mini",
api_token=KEY,
schema=JobListing.model_json_schema(),
extraction_type="schema",
))
job = JobListing.model_validate_json(result.extracted_content)
Framework Picks by Use Case
"Convert a site to LLM-ready markdown"
-> Jina Reader (SaaS, fastest) or Firecrawl (self-hostable or cloud)
"Schema-driven extraction across many pages"
-> Firecrawl (strongest Pydantic integration) or Crawl4AI (self-hosted)
"Autonomous agent that navigates and fills forms"
-> Browser Use (best WebVoyager score) or Skyvern (when you need vision for forms)
"Self-healing scraper that stays cheap"
-> Stagehand -- caches selectors, invokes LLM only when DOM changes; or Scrapling for selector auto-relocation
"Natural-language data pipelines"
-> ScrapeGraphAI -- describe the extraction as a graph of LLM + parser nodes
"High-volume cheap extraction"
-> CSS + LLM hybrid (above) -- pure SaaS is $$$ at 1M+ pages/month
GraphQL Reverse Engineering
GraphQL endpoints are often the cleanest replay targets.
Discovery
- InQL v6.1+: Kotlin rewrite, schema brute-forcer + engine fingerprinter + scanner
clairvoyance: reconstruct schema when introspection is disabled, using "did you mean" suggestion abuse- Check
/graphql,/api/graphql,/v1/graphqlendpoints - Check WebSocket frames for GraphQL subscriptions
Persisted Query Bypass
Many modern APIs use persisted queries (client sends only a sha256Hash). To force full query retransmission:
# mitmproxy addon -- replace sha256Hash with bogus value, forces PersistedQueryNotFound,
# client retransmits the full query which you capture.
import json
def request(flow):
try:
data = json.loads(flow.request.text)
if isinstance(data, list):
for item in data:
item["extensions"]["persistedQuery"]["sha256Hash"] = "0000"
else:
data["extensions"]["persistedQuery"]["sha256Hash"] = "0000"
flow.request.text = json.dumps(data)
except (json.JSONDecodeError, KeyError, TypeError):
pass
Introspection Bypass Techniques
When __schema is blocked:
- Whitespace variations (
__schemavs__schemawith trailing space) - GET instead of POST
- Inline fragment attacks to leak field types
Cost Modelling
Per-record cost components:
- Proxy bandwidth: ~$0.002 / page (residential at $8/GB, 250KB avg)
- LLM extraction: ~$0.01 / page (gpt-4o-mini, 5K input tokens + 500 output)
- Vision LLM (Skyvern-style): ~$0.03-0.10 / page (screenshot tokens are expensive)
- CAPTCHA: $0.80-2.99 / 1K if present
- Managed API (Firecrawl, Spider, Web Unlocker): $0.001-0.01 / page depending on tier
Budget formula:
monthly_cost = pages * (proxy_gb_avg * proxy_price + llm_rate + captcha_rate * captcha_price)
A 1M page/month pipeline with residential proxies and LLM extraction is ~$10-15K/month. Hybrid CSS+LLM drops that 5-10x.
Anti-Patterns
- LLM on every page: burns budget; always try CSS/JSON-LD first
- No schema validation: LLMs hallucinate; Pydantic rejects nonsense
- Vision LLM as default: 3-10x cost of text-only; use only for visual forms or CAPTCHAs
- Single-stage extraction: no fallback means one redesign kills the pipeline
Synergies
- Browser for rendering + JS challenges ->
stealth-browser-expert - HTTP replay at scale ->
http-fingerprint-expert - Pipeline orchestration + cost tracking + framework choice ->
grabber-architect - Async pipelines in Python ->
python-development:async-python-patterns - Tracing scraper flows ->
opentelemetry:opentelemetry