|
| 1 | +# NHS Conditions Scraper |
| 2 | + |
| 3 | +A Python scraper for all [NHS Health A to Z condition |
| 4 | +pages](https://www.nhs.uk/health-a-to-z/conditions/). It discovers every |
| 5 | +condition listed on the index (198 conditions), crawls all subpages for |
| 6 | +each condition (254 pages total), and saves the content as clean |
| 7 | +Markdown to `data/`. |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +- **[uv](https://docs.astral.sh/uv/)** — install with: |
| 12 | + |
| 13 | + ``` bash |
| 14 | + curl -LsSf https://astral.sh/uv/install.sh | sh |
| 15 | + ``` |
| 16 | + |
| 17 | +- **Python ≥ 3.11** — uv manages this automatically. |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +``` bash |
| 22 | +cd nhs-crawler |
| 23 | +uv sync |
| 24 | + |
| 25 | +# Scrape ALL conditions (198 conditions, 254 pages) |
| 26 | +uv run nhs-crawler |
| 27 | + |
| 28 | +# Scrape a single condition (discovers subpages automatically) |
| 29 | +uv run nhs-crawler --url https://www.nhs.uk/conditions/type-2-diabetes/ |
| 30 | + |
| 31 | +# Scrape a single specific page |
| 32 | +uv run nhs-crawler --page https://www.nhs.uk/conditions/type-2-diabetes/treatment/ |
| 33 | + |
| 34 | +# Show all options |
| 35 | +uv run nhs-crawler --help |
| 36 | +``` |
| 37 | + |
| 38 | +## Output |
| 39 | + |
| 40 | +Markdown files are saved to `data/<condition-slug>/<subpage>.md`: |
| 41 | + |
| 42 | + data/ |
| 43 | + ├── asthma/ |
| 44 | + │ └── index.md # Inline hub page — no subpages |
| 45 | + ├── type-2-diabetes/ |
| 46 | + │ ├── what-is-type-2-diabetes.md |
| 47 | + │ ├── symptoms.md |
| 48 | + │ ├── treatment.md |
| 49 | + │ ├── complications.md |
| 50 | + │ └── support.md |
| 51 | + ├── covid-19/ |
| 52 | + │ ├── covid-19-symptoms-and-what-to-do.md |
| 53 | + │ ├── how-to-avoid-catching-and-spreading-covid-19.md |
| 54 | + │ └── treatments-for-covid-19.md |
| 55 | + └── ... # 198 conditions, 254 Markdown files |
| 56 | + |
| 57 | +Each file starts with a title and source URL header: |
| 58 | + |
| 59 | +``` markdown |
| 60 | +# Asthma |
| 61 | + |
| 62 | +> Source: https://www.nhs.uk/conditions/asthma/ |
| 63 | +
|
| 64 | +Asthma is a common condition that affects your breathing... |
| 65 | +``` |
| 66 | + |
| 67 | +### Reports |
| 68 | + |
| 69 | +After a full crawl, two files are written to `reports/`: |
| 70 | + |
| 71 | +- **`condition_urls.json`** — ordered list of all discovered condition |
| 72 | + hub URLs. |
| 73 | +- **`summary.json`** — aggregate statistics for the run: |
| 74 | + |
| 75 | +``` json |
| 76 | +{ |
| 77 | + "run_at": "2026-06-20T18:38:08.937007+00:00", |
| 78 | + "index_url": "https://www.nhs.uk/health-a-to-z/conditions/", |
| 79 | + "conditions_discovered": 198, |
| 80 | + "pages_discovered": 254, |
| 81 | + "pages_scraped": 254, |
| 82 | + "error_count": 0, |
| 83 | + "total_markdown_chars": 1136966, |
| 84 | + "empty_pages": [], |
| 85 | + "errors": [] |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +### Logs |
| 90 | + |
| 91 | +A detailed crawl log is appended to `logs/crawl.log` on every run. |
| 92 | + |
| 93 | +> **Note:** The `data/`, `logs/`, and `reports/` directories are created |
| 94 | +> automatically at runtime by `src/constants.py`. They do not need to |
| 95 | +> exist beforehand. Consider adding `logs/` and `reports/` to |
| 96 | +> `.gitignore` if you don’t want to track generated output. |
| 97 | +
|
| 98 | +## How It Works |
| 99 | + |
| 100 | +1. **Discover conditions** — fetches the A-to-Z index page and extracts |
| 101 | + all `/conditions/<slug>/` links using a regex filter. |
| 102 | +2. **Discover subpages** — for each condition hub page, looks for a |
| 103 | + `<ul class="nhsuk-hub-key-links">` navigation element to find |
| 104 | + subpages. Some conditions (like asthma) are single-page with inline |
| 105 | + content — the hub page itself is saved as `index.md`. Others (like |
| 106 | + type-2-diabetes) have dedicated subpages for Symptoms, Treatment, |
| 107 | + etc. — in this case only the subpages are saved (the hub is excluded |
| 108 | + since the subpages contain all the content). |
| 109 | +3. **Fetch & convert** — each page is fetched and its main content |
| 110 | + element (`<div class="nhsuk-grid-column-two-thirds">` inside |
| 111 | + `<article>`) is converted to clean Markdown using a custom NHS-aware |
| 112 | + converter that handles: |
| 113 | + - **Care cards** → blockquotes with emoji prefixes (🚨 ⚠️ 📋) |
| 114 | + - **Do/don’t lists** → bullet lists with ✅/❌ markers |
| 115 | + - **Inset text** → blockquotes with 💡 prefix |
| 116 | + - **Clutter removal** — nav, breadcrumbs, feedback banners, SVGs, |
| 117 | + etc. |
| 118 | +4. **Save** — Markdown is written to `data/<condition>/<subpage>.md` |
| 119 | + with a title and source URL header. |
| 120 | +5. **Report** — `reports/summary.json` with aggregate statistics and |
| 121 | + `reports/condition_urls.json` with the full condition URL list. |
| 122 | + |
| 123 | +### HTTP Strategy |
| 124 | + |
| 125 | +All requests go through a shared `requests.Session` with: |
| 126 | + |
| 127 | +- **Retry**: up to 4 retries with exponential backoff (factor 1.2) on |
| 128 | + HTTP 429, 500, 502, 503, 504. |
| 129 | +- **Rate limiting**: 0.5s delay before each request (politeness). |
| 130 | +- **Connection pooling**: 10 connections, 20 max pool size. |
| 131 | +- **User-Agent**: identifies the scraper with a reference to nhs.uk. |
| 132 | +- **Concurrency**: pages are fetched in parallel using a thread pool (4 |
| 133 | + workers by default). |
| 134 | + |
| 135 | +## CLI Reference |
| 136 | + |
| 137 | + usage: nhs-crawler [-h] [--url URL] [--page PAGE] |
| 138 | + |
| 139 | + NHS Conditions scraper |
| 140 | + |
| 141 | + options: |
| 142 | + -h, --help show this help message and exit |
| 143 | + --url URL Scrape a single condition URL (including subpages). |
| 144 | + If omitted, scrapes all conditions from the A-to-Z index. |
| 145 | + --page PAGE Scrape a single specific page URL (no subpage discovery). |
| 146 | + |
| 147 | +## Configuration |
| 148 | + |
| 149 | +Key settings in `src/constants.py`: |
| 150 | + |
| 151 | +| Constant | Default | Description | |
| 152 | +|---------------|---------|---------------------------------------| |
| 153 | +| `DELAY` | `0.5` | Seconds between requests (politeness) | |
| 154 | +| `TIMEOUT` | `30` | HTTP request timeout (seconds) | |
| 155 | +| `MAX_WORKERS` | `4` | Parallel threads for page fetching | |
| 156 | + |
| 157 | +## Development |
| 158 | + |
| 159 | +``` bash |
| 160 | +uv sync --all-extras |
| 161 | +uv run ruff format src/ |
| 162 | +uv run ruff check src/ |
| 163 | +uv run pyright src/ |
| 164 | +``` |
0 commit comments