Skip to content

add PubMed guideline scraper - #15

Open
zndr27 wants to merge 2 commits into
MedARC-AI:mainfrom
zndr27:scraper/pubmed
Open

add PubMed guideline scraper#15
zndr27 wants to merge 2 commits into
MedARC-AI:mainfrom
zndr27:scraper/pubmed

Conversation

@zndr27

@zndr27 zndr27 commented Jul 29, 2026

Copy link
Copy Markdown

What this does

Adds a PubMed scraper to the datasets scraping pipeline. Same shape as the NICE scraper: discovery and extraction are separate, and everything outputs a normalized ScrapedDocument.

Guideline[pt] AND pubmed pmc open access[filter] AND English[la]

About 3,000 guidelines with full text. The other 40k Guideline records expose an abstract only, under publisher copyright.

Guideline[pt] rather than Practice Guideline[pt] because the former is a strict superset and the 352 records it adds are clinical, not administrative. English[la] drops 185, mostly French CMAJ translations of guidelines already present; every other source here is English-only anyway as a side effect of its entry URL, so this just makes it explicit.

Open question: how should the corpus handle licensing?

The PMC Open Access Subset is not uniformly Creative Commons licensed. Censused over all 2,999 records:

License n % derivatives
CC BY 1,335 44.5% unrestricted
CC0 38 1.3% unrestricted
CC BY-NC 662 22.1% non-commercial only
CC BY-NC-SA 62 2.1% non-commercial only
CC BY-NC-ND 552 18.4% restricted
publisher terms, no CC 350 11.7% case by case

So 46% is unambiguously safe for a converted, chunked corpus, which is arguably a derivative work. In the tail, 112 records have no <license> element at all, just a line like © Springer-Verlag Tokyo 2007, and Elsevier's pandemic-era deposits grant free access under a copyright line still reading "All rights reserved". Being in the subset is not itself a grant to redistribute.

This PR records rather than decides: each document carries license, license_url, license_type, license_statement and copyright_statement, so the corpus can be filtered later without re-scraping. Happy to add a filter, though it probably wants a project-wide answer.

How it works

esearch pages by numeric offset (200 per page) and returns the total; esummary resolves each batch to PMCIDs and citation metadata in one call; efetch returns JATS XML per document. Offset paging maps straight onto list_page(client, page), and an empty page past the end terminates the run.

JATS tags are rewritten to HTML and passed through the shared html_to_markdown helper rather than writing a second serializer. Headings come from <sec> nesting depth. Reference lists and <xref> pointers are dropped as citation apparatus.

Figures and supplementary files are recorded in metadata rather than linked: JATS carries only a bare filename, and the served URL adds a CDN shard and content hash absent from the API response, so a constructed link 404s. Captions stay in the text.

Delay is 0.4s, inside NCBI's documented 3 requests/second. Calls retry with backoff on 429, which a real run does draw. Records PMC holds without a body, 0.6%, are logged and skipped.

CLI

uv run amfv-scrape --source pubmed --documents 1
uv run amfv-scrape --source pubmed --url https://pubmed.ncbi.nlm.nih.gov/42476581/
uv run amfv-scrape --source pubmed --documents 3 -f markdown -o ./pubmed-out/

--source all now includes PubMed alongside NICE. --url takes a PubMed or PMC URL; external_id always prefers the record's own PMID so both routes give the same ID.

Tests

uv run ruff check datasets
uv run ruff format --check datasets
uv run pytest datasets/test

53 tests, offline via httpx.MockTransport. Also read output for 18 guidelines sampled across the result set: median 47k characters, in the same band as a NICE guideline, but the largest is 526k, so retrieval will want chunking here.

Notes for review

zndr27 added 2 commits July 26, 2026 00:05
Taken verbatim from MedARC-AI#10 so the two merge cleanly in either order. A listing
entry is not always a document: PMC holds some open-access records as metadata
only, with no full text deposited. Returning None skips the entry without
consuming the --documents budget, and the delay gate moves from scraped to
attempted so skipped items still rate-limit.

Drop this commit if MedARC-AI#10 lands first.
Scrapes the intersection of PubMed's Guideline publication type with the PMC
Open Access subset, restricted to English: about 3,000 guidelines. That is the
only slice where the full text is both retrievable and openly licensed; the
other 40k Guideline records expose an abstract only, under publisher copyright.

Two scoping choices, both measured rather than assumed:

`Guideline[pt]` rather than `Practice Guideline[pt]`. The former is a strict
superset and the 352 records it adds are clinical, not administrative (the 2025
Korean CPR guidelines and similar), so the narrower tag would drop 11% of the
corpus for nothing.

`English[la]`, which drops 185 records. Every other source in this package is
already English-only as a side effect of its entry URL: CPS is a bilingual site
scraped through its /en/ routes, WHO publishes in six languages and is scraped
through its English listing. PubMed's API returns every language, so the filter
has to be explicit to match. The excluded records are largely French CMAJ
translations of guidelines already in the corpus.

Discovery and extraction use NCBI's E-utilities rather than the rendered pages.
esearch paginates by numeric offset, so the page number maps straight onto
retstart and an empty page past the end terminates the run. esummary resolves a
whole batch of PMIDs to PMCIDs and citation metadata in one request. efetch
returns JATS XML carrying body, section structure and license.

JATS is close enough to HTML that renaming tags and reusing html_to_markdown is
cheaper and less error-prone than a second serializer: table-wrap already
contains genuine XHTML tables, and inline markup maps one to one.

Licensing is recorded per document rather than claimed for the source, because
the Open Access Subset is not uniformly Creative Commons licensed. Censused
over all 2,999 English records present on 2026-07-29:

  CC BY          44.5%    publisher terms, no CC license   11.7%
  CC BY-NC       22.1%    of which: Elsevier COVID grant, no
  CC BY-NC-ND    18.4%              <license> element at all (112),
  CC BY-NC-SA     2.1%              PMC OA "unrestricted re-use"
  CC0             1.3%

By what that permits: 45.8% unrestricted for derivative works, 24.1%
non-commercial only, 18.4% asserting NoDerivatives, 11.7% needing a
case-by-case reading. Presence in the subset is not itself a grant to
redistribute: 112 records carry only a copyright line such as "(c)
Springer-Verlag Tokyo 2007", and Elsevier's pandemic-era deposits grant free
access while still reserving all rights.

The license name is parsed from the Creative Commons URL rather than the
license-type attribute, which the corpus spells 18 different ways. The
copyright statement is captured separately because it is a sibling of
<license>, not a child, and it holds the reservation of rights.

Figures and supplementary files are recorded in metadata rather than linked.
Unlike the HTML sources in MedARC-AI#9 and MedARC-AI#10, which absolutize a real <img src>, JATS
carries only a bare filename; the served URL inserts a CDN shard and content
hash that appear nowhere in the API response, so a constructed link 404s.
Supplementary blocks are pointers too: across 40 sampled guidelines every one
referenced an external .docx or .tif rather than inline content, totalling
0.18% of body text. Recording name, label and caption keeps the evidence
tables findable without re-scraping.

E-utilities calls retry with backoff on 429 and 5xx. One document makes up to
two calls back to back and the rate limit is per source address, so NCBI does
answer with 429 in practice; without a retry that propagates past the skip
handler and kills the whole run.

external_id prefers the PMID from the record itself, so an article reached
from a PMC URL gets the same identifier as one reached from the listing.

Records PMC holds without a deposited body, 0.6% of the corpus, are logged
and skipped rather than aborting the run.
@zndr27
zndr27 marked this pull request as ready for review July 30, 2026 00:24
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