# AI-Powered News / Content Scraper Pipeline General pattern for building a scraper that chains **web search (Firecrawl/SearXNG)** → **LLM classification** → **structured score output with dedup**. ## Architecture ``` Firecrawl search (6+ queries) │ └─ Returns {url, title, description} ▼ URL dedup (processed-urls.json) │ └─ Skip previously seen URLs ▼ Admin-AI LLM classification (deepseek-chat) │ └─ Returns JSON: {region, event_type, description, confidence} ▼ Region/category matcher (regex rules) │ └─ Maps classified region → game region ID ▼ pending-scores.json │ └─ Structured events with points, source_url, confidence ▼ Cron (daily 8 AM ET) ``` ## Key Components ### 1. Search phase Run multiple queries to maximize recall (Firecrawl returns 10 results per query max). Append today's date to get recency: ```python SEARCH_QUERIES = [ "shark attack today", "shark sighting", "shark bite", "shark incident", ] today_str = date.today().isoformat() for query in SEARCH_QUERIES: results = firecrawl_search(api_key, f"{query} {today_str}") time.sleep(0.5) # rate-limit courtesy ``` **Dedup by URL** across queries — Firecrawl may return the same article for different queries. Use a `dict[str, dict]` keyed by URL. ### 2. Classification phase Send title+description to the LLM. **Key design choices:** - **System prompt** forces strict JSON output: `"Return valid JSON only — no markdown, no extra text"` - **Schema** includes `event_type` with a sentinel value `"none"` for non-incidents - **Temperature 0.1** for deterministic classification - **Max tokens 300** — we only need the JSON response - **Truncate article text** to ~3000 chars — Firecrawl descriptions are short - **Parse JSON with regex fallback** — some models wrap in markdown code fences despite instruction: ```python json_match = re.search(r'\{.*\}', content, re.DOTALL) ``` ### 3. Category/Region matching Use regex with multiple fallback patterns per region. Try standalone names first, then broader patterns. ### 4. Structured output (pending-scores.json) Each event gets `region_id`, `region_name`, `event_type`, `description`, `source_url`, `points`, `event_date`, `verified`, `confidence`, `classification_ts`. ### 5. Dedup (processed-urls.json) Load → skip seen → append after classification → persist. ### 6. Cron wrapper ```bash #!/bin/bash cd /path/to/project || exit 1 source .venv/bin/activate python3 scraper/scrape.py >> data/scraper.log 2>&1 ``` TZ=America/New_York at crontab top. ## Points Scoring Convention | Event Type | Points | |---|---| | sighting | 1 | | bite | 5 | | fatality | 10 | ## Pitfalls - **Rate limit between LLM and search API calls** — 0.3-0.5s spacing - **JSON parsing of LLM output** — always regex-fallback for code fences - **Sentinel event_type "none"** — check both `!= "none"` and `is not None` - **Filter pages that list historical incidents** — LLM tends to classify "attack map" pages as real incidents - **Region regex ordering** — broad patterns first, standalone city names as fallbacks - **Processed URLs accumulate** — archive URLs >90 days to prevent unbounded growth - **Firecrawl API v1/v2 response structure differences** — check field names - **State files need backup** — pending-scores.json and processed-urls.json are critical state for game/score systems ## Verification Pattern Import the matching functions via `exec()` and test all region patterns in isolation — no API calls needed.