Skip to content

feat: --discover mode for full-site seed enumeration - #9

Merged
Hydaspex merged 5 commits into
mainfrom
feat/seed-discovery
Aug 8, 2026
Merged

feat: --discover mode for full-site seed enumeration#9
Hydaspex merged 5 commits into
mainfrom
feat/seed-discovery

Conversation

@Hydaspex

@Hydaspex Hydaspex commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Follow-up to the "scrape the entire website" question: full-site runs previously required a hand-maintained --seed URL=REGION per trust (~137 acute providers). --discover enumerates them from the live site itself. Follows on from #8.

How it works

The site is structured /<region-slug>/<trust>/. Discovery scrapes 8 pages total — the homepage plus 7 region pages — and emits the (url, region) seed pairs run_pipeline already consumes, then the normal pipeline (preflight → crawl → extract) runs unchanged.

nhs-scraper --discover --output output/full.csv
# discovered 137 trust seeds across the site
# run <id>: wrote N records to output/full.csv

What's included

pipeline/discover.py — two pure parsers + one thin async orchestrator:

  • discover_region_urls(html) — homepage anchors → region URLs. An allowlist of the 7 known region slugs (not "any single path segment") keeps utility pages like /find-my-hospital/ out; deduped, host-filtered, document order preserved
  • discover_trust_seeds(region_url, html) — region page anchors → (trust_url, region) seeds, where a trust link is exactly one path level below the region: excludes self links, specialty pages (two levels down), and other regions' trusts. Unknown slugs fall back to the slug as the region name — a newly added region still works, just unpretty until the map is updated
  • discover_seeds(backend) — orchestrates the scrapes; raises on empty discovery (no regions / no trusts) because a silent empty seed list would produce a silently empty CSV — the exact failure mode the preflight probe exists to prevent

cli.py--discover flag, mutually exclusive with --seed. A failed discovery exits 1 with a clear message and never falls back to DEFAULT_SEEDS — silent degradation from a full-site run to a one-trust run would be worse than failing.

Fixtures + tests (13) — synthetic homepage (7 regions + duplicate/external/utility-link traps) and South East region page (2 trusts + duplicate/self/too-deep/wrong-region traps). Tests cover both parsers, orchestration via a fake backend, empty-discovery errors, the CLI conflict path, the no-fallback failure path, and a full --discover run producing 8 golden records from 2 discovered seeds.

Verification

Parser verified in the sandbox against every trap pattern in the fixtures before pushing (dedupe, host filter, allowlist, self/deep/wrong-region exclusion, slug fallback, relative-URL resolution).

Design decisions to review

  1. 8 targeted scrapes, not a BFS crawl — discovery fetches exactly homepage + 7 region pages; a crawl() from the homepage would fetch every trust and specialty page just to throw the content away.
  2. Region-slug allowlist with fallback — trades "discovers new regions automatically with a pretty name" for "never mistakes a utility page for a region". The preflight probe backstops anything the allowlist gets wrong.
  3. Discovery reuses scrape, not crawl — keeps the retry policy (from feat: transient-error retry policy with failure telemetry #8) applied to discovery requests for free.

- pipeline/discover.py: discover_region_urls (homepage -> region URLs,
  allowlist of the 7 known region slugs, deduped, host-filtered) and
  discover_trust_seeds (region page -> (trust_url, region) seeds,
  exactly one path level below the region); discover_seeds orchestrates
  both scrapes
- cli.py: --discover replaces --seed; discovery failures abort with a
  clear message and exit 1 (never silently fall back to DEFAULT_SEEDS)
- fixtures: synthetic homepage + South East region page exercising
  duplicates, external hosts, self links, too-deep specialty links and
  wrong-region links
- tests/test_discover.py: 13 offline tests covering both parsers,
  orchestration via a fake backend, CLI flag conflicts, the discovery
  failure path, and a full --discover run writing 4 golden records
Split the over-long comprehension in test_traps_are_excluded.
Ground-truthed against myplannedcare.nhs.uk: Midlands is /mids/ and
North East and Yorkshire is /ney/ (e.g. /ney/north-cumbria/), not the
/midlands/ and /neast/ slugs assumed earlier. The other five slugs
(east, london, nwest, seast, swest) are confirmed correct. Without
this, discovery would silently miss 2 of 7 regions on the live site.
…cal records

Both discovered seeds serve the same fixture HTML, so the second
trust's 4 records are exact duplicates of the first and
normalise_records collapses them: 4 records, not 8. Assert that
(dedupe across seeds is the correct behaviour) and add a distinct-
records case so multi-seed accumulation stays covered.
The "weeks longer" replacement broke the waiting-time number parser,
so Oxford contributed zero records (4 total, not 8). Two different
URLs serving identical HTML dedupe to the same records, so the dedupe
key's provider comes from the page <h1> — prepending text inside the
h1 tag changes the provider (and thus the dedupe key) without
touching any parsed numbers, and works regardless of the exact
heading text.
@Hydaspex
Hydaspex merged commit 723ae9e into main Aug 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant