Web Scraping Foundations
Use When
- Choose the least invasive authorised acquisition path for web data and validate politeness, structure, and failure handling.
Do Not Use When
- Use dataset-discovery first when published data may exist and scraping-engineering-python only after scaling needs are observed.
Inputs
| Input |
Source/provider |
If absent |
| Target, fields, purpose, volume, freshness, and authority |
Requester and target terms |
Stop requests and return an acquisition plan. |
| Robots/politeness result and representative pages |
Target site |
Do not scrape until assessed. |
Workflow
- Confirm authority and check published APIs/datasets.
- Inspect response HTML, JSON endpoints, and structured data in order.
- Choose the cheapest working stack and define typed failures.
- Test representative records; stop on blocks, consent barriers, or unstable selectors.
- Recover by narrowing scope, slowing requests, or returning a gap; escalate to engineering only when justified.
Outputs
| Artifact |
Consumer |
Acceptance condition |
| Acquisition decision and extraction plan |
Implementer/operator |
Selected path, authority, politeness, schema, errors, and stop conditions are explicit. |
| Validated sample dataset |
Analyst |
Expected fields, null handling, provenance, and observed errors are recorded. |
Evidence Produced
| Category |
Artifact |
Acceptance condition |
| Correctness |
Representative extraction test |
Sample records match source values and missing fields are explicit. |
Capability Contract
Planning and review default to read-only. Network requests, browser control, account/session use, form submission, persistent storage, or production crawling require explicit authority. Never bypass access controls.
Degraded Mode
Without network, browser, credentials, or target permission, return a design and offline parser test where possible. Mark live politeness, rendering, and extraction checks unassessed.
Decision Rules
| Choice |
Action |
Failure/risk avoided |
| Published API/dataset exists |
Use it instead of scraping |
Fragile duplicate collection |
| JSON endpoint supplies data |
Call it directly |
Browser overhead |
| Block or consent challenge appears |
Stop and notify |
Circumvention |
Quality Standards
The route is authorised, least invasive, polite, source-traceable, failure-aware, and proven on representative records.
Anti-Patterns
- Starting with a browser. Fix: run the decision tree.
- Ignoring robots/politeness. Fix: assess before requests.
- Retrying all errors. Fix: classify failures.
- Positional selectors. Fix: use semantic selectors.
- Claiming success from one happy record. Fix: test representative variation.
Worked Example
If a page exposes the needed records through a documented JSON endpoint, choose it over browser automation, test null and pagination behaviour, and stop if the service signals blocking.
References
- Politeness and rate limiting
- Troubleshooting
- Browser automation
The single entry skill for web scraping. Encodes the decision tree, stack choices, and orchestration rules. Detailed sub-disciplines live in references/ and are loaded only when the situation demands them.
Companion skill
scraping-engineering-python — kept separate because it is the Python-specific scaling layer (caching, concurrency, dynamic content, Scrapy framework selection). Load it when the crawl exceeds ~1,000 URLs, requires resumability, or needs concurrent downloading.
Reference index
When the situation matches the trigger, load the named reference verbatim. Do not load all references by default.
| Reference |
Load when |
references/politeness-and-ratelimiting.md |
Every non-trivial scrape — robots.txt, throttle, backoff, identification headers, block-detection signals |
references/troubleshooting-brody.md |
Scraper returns empty/different content, gets blocked, or needs to behave like a real browser (header spoofing, session cookies, hidden CSRF tokens, missing-element resilience, debugging workflow) |
references/browser-automation-playwright.md |
Decision tree below has eliminated plain-HTTP options and JS rendering is required — Playwright recipes, wait strategies, login replay, network interception, stealth |
1. The decision tree (memorise — never skip)
Run in this order on every job. Stop at the first step that succeeds.
- Is the data available from a published API or open dataset? Use it. Skip scraping. (See
dataset-discovery-and-analysis.)
- View Source. Is the data in the response HTML? Yes →
requests/httpx + BeautifulSoup. Done.
- DevTools → Network → XHR. Is there a JSON endpoint? Yes → call it directly. Faster and more stable than HTML parsing.
- Inline JSON in
<script> tags? (__NEXT_DATA__, window.__INITIAL_STATE__.) Extract and parse before considering a browser.
- Does the page require JS interaction (click, scroll, multi-step form)? Only now → load
references/browser-automation-playwright.md.
- Multi-domain crawl with politeness, dedup, pause/resume needs? Load
scraping-engineering-python for Scrapy.
Brody's cheapest diagnostic: "View Source → Cmd-F for a known datum." If the datum is visible there, no JS is needed.
2. Always check structured-data shortcuts FIRST
Many sites publish clean structured data:
- JSON-LD in
<script type="application/ld+json"> — Schema.org Product/Article/Organization (tools.scraping.extract_jsonld).
- Open Graph + Twitter Cards —
og:title, og:image, og:description (tools.scraping.extract_opengraph).
- RSS / Atom feeds —
feedparser (tools.scraping.extractors.feeds.parse_feed).
/sitemap.xml — server-blessed enumeration without link-following.
/robots.txt — Sitemap declarations + Disallow rules.
A scraper that ignores these layers wastes effort.
3. Stack choices
| Layer |
Default |
Switch when |
| HTTP |
httpx (async) or requests (sync) |
TLS-fingerprint blocks → curl-cffi |
| Parser |
BeautifulSoup(html, "lxml") |
>1k pages/sec → selectolax |
| Selectors |
CSS .select() |
CSS can't (axis nav, position) → XPath |
| Headless |
Playwright |
Migrating legacy → Selenium |
| Crawler |
Hand-rolled requests loop |
Multi-domain or resumable → Scrapy (load scraping-engineering-python) |
| Storage |
SQLite ad-hoc; Postgres prod; Parquet + JSONL for scraped artefacts |
CSV only for human share |
| Rate limit |
tools.scraping.Throttle |
Unknown server behaviour → AdaptiveThrottle |
4. Pagination patterns
| Pattern |
Recognise by |
Strategy |
| Query-string offset |
?page=N |
paginate_offset() |
| Cursor-based |
server returns next token |
paginate_cursor() |
| Infinite scroll / Load More |
XHR-backed |
DevTools → find endpoint → paginate_xhr() |
Hash-based (#page=2) |
client-side only |
Find underlying API or use Playwright |
Edge-case probe: try page_size=1000 first — servers often don't validate against UI options.
5. Error taxonomy
| Class |
When |
Action |
RobotsBlocked |
robots.txt disallows |
Stop. Don't override without authorisation. |
RateLimited (429) |
Server says slow down |
Honour Retry-After; reduce concurrency; lengthen delay floor |
ScrapeError (4xx) |
Auth / not found |
Don't retry. Surface to operator. |
ScrapeError (5xx) |
Server fault |
Exponential-backoff retry, max 3 |
SoftBlock |
200 but body is CAPTCHA / interstitial |
Stop. Reduce footprint. |
Anti-pattern: silent retry on 4xx. Surface, don't bury.
6. Orchestration — what to load and when
new scraping task
├─ load this SKILL.md
├─ ALWAYS load references/politeness-and-ratelimiting.md before issuing requests
├─ Step 2 fails (content differs from browser, or empty)
│ └─ load references/troubleshooting-brody.md
├─ Step 5 fires (JS interaction required)
│ └─ load references/browser-automation-playwright.md
└─ Crawl exceeds 1,000 URLs / needs resumability / multi-domain
└─ load scraping-engineering-python skill
7. Caching raw HTML
Lawson's central discipline: store raw HTML, not just parsed records. Re-extraction is free; re-crawling is expensive. Use tools.scraping.DiskCache or SQLiteCache for any non-trivial crawl. Detail in scraping-engineering-python.
8. Parser-performance baseline
Lawson's 1000-iteration benchmark on the same page: regex 5.5 s · lxml 7.0 s · BeautifulSoup pure-Python 42.8 s. Default to BeautifulSoup(html, "lxml") (fast + ergonomic). Only profile for selectolax (~10× faster than lxml on huge pages) when the parser is the actual bottleneck — network usually dominates.
9. Universal anti-patterns
time.sleep() as a wait strategy in browser code — use wait_for_selector.
verify=False as a habit — only for self-signed dev hosts.
- Indexing by visual position (
soup.find_all('div')[3]) — use semantic selectors.
- Single giant try/except wrapping the whole crawl — swallows real bugs.
- Re-crawling on every run — cache raw HTML.
- Hard-coded selectors with no graceful "MISSING DATA" fallback.
- Parsing HTML with regex (parser preferred; regex only on extracted text).
- Hammering a server with no delay — use
Throttle.
- Running a headless browser when a JSON XHR exists.
10. Ship gate (universal)
See also
scraping-engineering-python — caching, concurrency, dynamic content, Scrapy
dataset-discovery-and-analysis — try this before scraping
data-quality-assessment — score scraped batches on the four-axis model
evidence-discipline — when scraping for OSINT/DD, evidence rules override defaults
1---2name: web-scraping-foundations3description: Use when choosing and validating the least invasive acquisition path for web data across published APIs, JSON endpoints, structured HTML, or browser automation, including politeness and error handling; use scraping-engineering-python when a Python crawl needs caching, concurrency, resumability, or Scrapy.4---56# Web Scraping Foundations78<!-- dual-compat-start -->910## Use When1112- Choose the least invasive authorised acquisition path for web data and validate politeness, structure, and failure handling.1314## Do Not Use When1516- Use dataset-discovery first when published data may exist and scraping-engineering-python only after scaling needs are observed.1718## Inputs1920| Input | Source/provider | If absent |21|---|---|---|22| Target, fields, purpose, volume, freshness, and authority | Requester and target terms | Stop requests and return an acquisition plan. |23| Robots/politeness result and representative pages | Target site | Do not scrape until assessed. |2425## Workflow26271. Confirm authority and check published APIs/datasets.282. Inspect response HTML, JSON endpoints, and structured data in order.293. Choose the cheapest working stack and define typed failures.304. Test representative records; stop on blocks, consent barriers, or unstable selectors.315. Recover by narrowing scope, slowing requests, or returning a gap; escalate to engineering only when justified.3233## Outputs3435| Artifact | Consumer | Acceptance condition |36|---|---|---|37| Acquisition decision and extraction plan | Implementer/operator | Selected path, authority, politeness, schema, errors, and stop conditions are explicit. |38| Validated sample dataset | Analyst | Expected fields, null handling, provenance, and observed errors are recorded. |3940## Evidence Produced4142| Category | Artifact | Acceptance condition |43|---|---|---|44| Correctness | Representative extraction test | Sample records match source values and missing fields are explicit. |4546## Capability Contract4748Planning and review default to read-only. Network requests, browser control, account/session use, form submission, persistent storage, or production crawling require explicit authority. Never bypass access controls.4950## Degraded Mode5152Without network, browser, credentials, or target permission, return a design and offline parser test where possible. Mark live politeness, rendering, and extraction checks unassessed.5354## Decision Rules5556| Choice | Action | Failure/risk avoided |57|---|---|---|58| Published API/dataset exists | Use it instead of scraping | Fragile duplicate collection |59| JSON endpoint supplies data | Call it directly | Browser overhead |60| Block or consent challenge appears | Stop and notify | Circumvention |6162## Quality Standards6364The route is authorised, least invasive, polite, source-traceable, failure-aware, and proven on representative records.6566## Anti-Patterns6768- Starting with a browser. Fix: run the decision tree.69- Ignoring robots/politeness. Fix: assess before requests.70- Retrying all errors. Fix: classify failures.71- Positional selectors. Fix: use semantic selectors.72- Claiming success from one happy record. Fix: test representative variation.7374## Worked Example7576If a page exposes the needed records through a documented JSON endpoint, choose it over browser automation, test null and pagination behaviour, and stop if the service signals blocking.7778## References7980- [Politeness and rate limiting](references/politeness-and-ratelimiting.md)81- [Troubleshooting](references/troubleshooting-brody.md)82- [Browser automation](references/browser-automation-playwright.md)8384<!-- dual-compat-end -->8586The single entry skill for web scraping. Encodes the decision tree, stack choices, and orchestration rules. Detailed sub-disciplines live in `references/` and are loaded only when the situation demands them.8788## Companion skill8990- `scraping-engineering-python` — kept separate because it is the Python-specific scaling layer (caching, concurrency, dynamic content, Scrapy framework selection). Load it when the crawl exceeds ~1,000 URLs, requires resumability, or needs concurrent downloading.9192## Reference index9394When the situation matches the trigger, load the named reference verbatim. Do not load all references by default.9596| Reference | Load when |97|---|---|98| `references/politeness-and-ratelimiting.md` | Every non-trivial scrape — robots.txt, throttle, backoff, identification headers, block-detection signals |99| `references/troubleshooting-brody.md` | Scraper returns empty/different content, gets blocked, or needs to behave like a real browser (header spoofing, session cookies, hidden CSRF tokens, missing-element resilience, debugging workflow) |100| `references/browser-automation-playwright.md` | Decision tree below has eliminated plain-HTTP options and JS rendering is required — Playwright recipes, wait strategies, login replay, network interception, stealth |101102## 1. The decision tree (memorise — never skip)103104Run in this order on every job. Stop at the first step that succeeds.1051061. **Is the data available from a published API or open dataset?** Use it. Skip scraping. (See `dataset-discovery-and-analysis`.)1072. **View Source. Is the data in the response HTML?** Yes → `requests`/`httpx` + BeautifulSoup. Done.1083. **DevTools → Network → XHR. Is there a JSON endpoint?** Yes → call it directly. Faster and more stable than HTML parsing.1094. **Inline JSON in `<script>` tags?** (`__NEXT_DATA__`, `window.__INITIAL_STATE__`.) Extract and parse before considering a browser.1105. **Does the page require JS interaction (click, scroll, multi-step form)?** Only now → load `references/browser-automation-playwright.md`.1116. **Multi-domain crawl with politeness, dedup, pause/resume needs?** Load `scraping-engineering-python` for Scrapy.112113> Brody's cheapest diagnostic: *"View Source → Cmd-F for a known datum."* If the datum is visible there, no JS is needed.114115## 2. Always check structured-data shortcuts FIRST116117Many sites publish clean structured data:118119- **JSON-LD** in `<script type="application/ld+json">` — Schema.org Product/Article/Organization (`tools.scraping.extract_jsonld`).120- **Open Graph + Twitter Cards** — `og:title`, `og:image`, `og:description` (`tools.scraping.extract_opengraph`).121- **RSS / Atom feeds** — `feedparser` (`tools.scraping.extractors.feeds.parse_feed`).122- **`/sitemap.xml`** — server-blessed enumeration without link-following.123- **`/robots.txt`** — Sitemap declarations + Disallow rules.124125A scraper that ignores these layers wastes effort.126127## 3. Stack choices128129| Layer | Default | Switch when |130|---|---|---|131| HTTP | `httpx` (async) or `requests` (sync) | TLS-fingerprint blocks → `curl-cffi` |132| Parser | `BeautifulSoup(html, "lxml")` | >1k pages/sec → `selectolax` |133| Selectors | CSS `.select()` | CSS can't (axis nav, position) → XPath |134| Headless | Playwright | Migrating legacy → Selenium |135| Crawler | Hand-rolled `requests` loop | Multi-domain or resumable → Scrapy (load `scraping-engineering-python`) |136| Storage | SQLite ad-hoc; Postgres prod; **Parquet + JSONL** for scraped artefacts | CSV only for human share |137| Rate limit | `tools.scraping.Throttle` | Unknown server behaviour → `AdaptiveThrottle` |138139## 4. Pagination patterns140141| Pattern | Recognise by | Strategy |142|---|---|---|143| Query-string offset | `?page=N` | `paginate_offset()` |144| Cursor-based | server returns `next` token | `paginate_cursor()` |145| Infinite scroll / Load More | XHR-backed | DevTools → find endpoint → `paginate_xhr()` |146| Hash-based (`#page=2`) | client-side only | Find underlying API or use Playwright |147148**Edge-case probe:** try `page_size=1000` first — servers often don't validate against UI options.149150## 5. Error taxonomy151152| Class | When | Action |153|---|---|---|154| `RobotsBlocked` | robots.txt disallows | Stop. Don't override without authorisation. |155| `RateLimited` (429) | Server says slow down | Honour `Retry-After`; reduce concurrency; lengthen delay floor |156| `ScrapeError` (4xx) | Auth / not found | Don't retry. Surface to operator. |157| `ScrapeError` (5xx) | Server fault | Exponential-backoff retry, max 3 |158| `SoftBlock` | 200 but body is CAPTCHA / interstitial | Stop. Reduce footprint. |159160**Anti-pattern:** silent retry on 4xx. Surface, don't bury.161162## 6. Orchestration — what to load and when163164```165new scraping task166├─ load this SKILL.md167├─ ALWAYS load references/politeness-and-ratelimiting.md before issuing requests168├─ Step 2 fails (content differs from browser, or empty)169│ └─ load references/troubleshooting-brody.md170├─ Step 5 fires (JS interaction required)171│ └─ load references/browser-automation-playwright.md172└─ Crawl exceeds 1,000 URLs / needs resumability / multi-domain173 └─ load scraping-engineering-python skill174```175176## 7. Caching raw HTML177178Lawson's central discipline: **store raw HTML, not just parsed records.** Re-extraction is free; re-crawling is expensive. Use `tools.scraping.DiskCache` or `SQLiteCache` for any non-trivial crawl. Detail in `scraping-engineering-python`.179180## 8. Parser-performance baseline181182Lawson's 1000-iteration benchmark on the same page: regex 5.5 s · lxml 7.0 s · BeautifulSoup pure-Python 42.8 s. Default to `BeautifulSoup(html, "lxml")` (fast + ergonomic). Only profile for `selectolax` (~10× faster than lxml on huge pages) when the parser is the actual bottleneck — network usually dominates.183184## 9. Universal anti-patterns185186- `time.sleep()` as a wait strategy in browser code — use `wait_for_selector`.187- `verify=False` as a habit — only for self-signed dev hosts.188- Indexing by visual position (`soup.find_all('div')[3]`) — use semantic selectors.189- Single giant try/except wrapping the whole crawl — swallows real bugs.190- Re-crawling on every run — cache raw HTML.191- Hard-coded selectors with no graceful "MISSING DATA" fallback.192- Parsing HTML with regex (parser preferred; regex only on extracted text).193- Hammering a server with no delay — use `Throttle`.194- Running a headless browser when a JSON XHR exists.195196## 10. Ship gate (universal)197198- [ ] Decision tree run; the chosen approach is the cheapest one that works.199- [ ] Structured-data shortcuts checked before HTML parsing.200- [ ] `references/politeness-and-ratelimiting.md` rules applied (robots.txt, throttle, identification, backoff).201- [ ] Selectors verified against ≥3 records.202- [ ] Missing fields normalised to `None`; one bad record does not crash the crawl.203- [ ] Errors surfaced by class (4xx vs 5xx vs 429 vs SoftBlock).204- [ ] Raw HTML cached for non-trivial crawls.205- [ ] Output stored as Parquet + JSONL with manifest (`source`, `fetched_at`, `headers`, `selector_versions`, `n_rows`, `n_errors`).206- [ ] If headless was used, the trigger from Step 5 is documented.207208## See also209210- `scraping-engineering-python` — caching, concurrency, dynamic content, Scrapy211- `dataset-discovery-and-analysis` — try this before scraping212- `data-quality-assessment` — score scraped batches on the four-axis model213- `evidence-discipline` — when scraping for OSINT/DD, evidence rules override defaults