| name | web-scraping |
| description | Efficient web scraping patterns for data extraction from websites and APIs. Use when scraping content, fetching structured data, building ETL connectors, or processing web responses. Key principle - use programmatic extraction with uv scripts instead of reading raw HTML into context. |
Efficient Web Scraping
Extract data from websites efficiently using Python scripts. Never bloat context by reading raw HTML/JSON directly.
Core Principle
Programmatic extraction > Reading thousands of lines
| Approach | Tokens | Quality | Speed |
|---|
| Read raw HTML into context | 50,000+ | Poor | Slow |
| Script + structured output | ~200 | Excellent | Fast |
When This Applies
- Building ETL connectors (VA.gov, CareerOneStop, etc.)
- Scraping resource provider websites
- Fetching API responses
- Processing RSS/JSON feeds
- Crawling multiple pages
The Pattern
1. Identify Target Data
Before writing code, specify exactly what you need:
**Target**: VA.gov facility data
**Fields needed**: name, address, phone, services, hours
**Output format**: JSON matching ResourceCandidate schema
2. Write a Python Script
Create a script with proper error handling:
"""
Extract resource data from [target].
Output: Structured JSON to stdout matching ResourceCandidate.
"""
import json
import sys
import requests
from bs4 import BeautifulSoup
def extract_data(url: str) -> dict:
"""Extract target fields from URL."""
response = requests.get(url, headers={
"User-Agent": "Vibe4Vets Resource Aggregator (research)"
})
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
return {
"name": soup.find("h1").get_text(strip=True) if soup.find("h1") else None,
"description": soup.find("meta", {"name": "description"})["content"]
if soup.find("meta", {"name": "description"}) else None,
"phone": extract_phone(soup),
"address": extract_address(soup),
}
if __name__ == "__main__":
url = sys.argv[1] if len(sys.argv) > 1 else None
if not url:
print("Usage: script.py <url>", file=sys.stderr)
sys.exit(1)
result = extract_data(url)
print(json.dumps(result, indent=2, ensure_ascii=False))
3. Run and Use Output
python script.py "https://example.com/page" > output.json
Then read only the small JSON output (~200 tokens) instead of the full page (~50,000 tokens).
Vibe4Vets Connector Pattern
Connector Interface
All connectors must implement the base interface:
from typing import Protocol
from app.models.resource import ResourceCandidate
class Connector(Protocol):
"""Base connector interface."""
def run(self) -> list[ResourceCandidate]:
"""Fetch and return resource candidates."""
...
def metadata(self) -> dict:
"""Return connector metadata (source tier, name, etc.)."""
...
Example: VA.gov Connector
import requests
from typing import Optional
from app.models.resource import ResourceCandidate
from app.core.taxonomy import CATEGORIES
class VAGovConnector:
"""Connector for VA.gov Lighthouse API."""
BASE_URL = "https://api.va.gov/services/va_facilities/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"apikey": api_key,
"Accept": "application/json",
})
def run(self) -> list[ResourceCandidate]:
"""Fetch VA facilities and convert to ResourceCandidates."""
candidates = []
for facility in self._fetch_facilities():
candidate = self._to_candidate(facility)
if candidate:
candidates.append(candidate)
return candidates
def _fetch_facilities(self, state: Optional[str] = None):
"""Fetch facilities from VA API."""
params = {"per_page": 100}
if state:
params[] = state
response = .session.get(, params=params)
response.raise_for_status()
data = response.json()
data.get(, [])
() -> [ResourceCandidate]:
attrs = facility.get(, {})
address = attrs.get(, {}).get(, {})
ResourceCandidate(
name=attrs.get(),
description=._build_description(attrs),
category=._map_category(attrs.get()),
phone=attrs.get(, {}).get(),
website=attrs.get(),
address_line1=address.get(),
city=address.get(),
state=address.get(),
zip_code=address.get(),
source_url=,
source_id=facility.get(),
)
() -> :
mapping = {
: ,
: ,
: ,
}
mapping.get(facility_type, )
() -> :
{
: ,
: ,
: ,
}
Example: Web Scraping Connector
import requests
from bs4 import BeautifulSoup
from app.models.resource import ResourceCandidate
class StateVeteranAgencyConnector:
"""Scrape state veteran agency websites."""
def __init__(self, state: str, url: str):
self.state = state
self.url = url
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Vibe4Vets Resource Aggregator"
})
def run(self) -> list[ResourceCandidate]:
"""Scrape and return resource candidates."""
response = self.session.get(self.url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
candidates = []
for item in soup.select(".resource-item"):
candidate = self._parse_item(item)
if candidate:
candidates.append(candidate)
return candidates
def _parse_item(self, item) -> Optional[ResourceCandidate]:
"""Parse a single resource item from HTML."""
name = item.select_one("h3")
if not name:
ResourceCandidate(
name=name.get_text(strip=),
description=item.select_one().get_text(strip=)
item.select_one() ,
phone=._extract_phone(item),
website=item.select_one()[]
item.select_one() ,
state=.state,
source_url=.url,
)
() -> []:
phone_el = item.select_one()
phone_el:
re
phone_text = phone_el.get_text()
= re.search(, phone_text)
.group()
() -> :
{
: ,
: ,
: ,
}
Best Practices
DO
- Specify exact fields needed before writing script
- Limit results (e.g.,
[:100], pagination)
- Output structured JSON matching ResourceCandidate schema
- Handle errors gracefully with try/except
- Add rate limiting for multiple requests
- Use CSS selectors not regex for HTML
- Include source URL for traceability
DON'T
- Read full page HTML into context
- Fetch data you won't use
- Skip error handling
- Make unlimited requests
- Parse HTML with regex
- Ignore robots.txt on production
Error Handling
try:
result = extract_data(url)
print(json.dumps(result, indent=2))
except requests.exceptions.HTTPError as e:
print(json.dumps({
"error": f"HTTP {e.response.status_code}",
"url": url
}))
sys.exit(1)
except Exception as e:
print(json.dumps({
"error": str(e),
"type": type(e).__name__
}))
sys.exit(1)
Token Savings Example
| Scenario | Without Script | With Script |
|---|
| VA.gov facility page | ~45,000 tokens | ~150 tokens |
| State agency listing | ~30,000 tokens | ~800 tokens |
| API response (100 items) | ~20,000 tokens | ~500 tokens |
10-100x token reduction = faster, cheaper, better context for reasoning.
Quick Checklist