add who guideline scraper with pdf full text extraction - #6
Open
conscioustahoe wants to merge 2 commits into
Open
add who guideline scraper with pdf full text extraction#6conscioustahoe wants to merge 2 commits into
conscioustahoe wants to merge 2 commits into
Conversation
Collaborator
|
I believe this only scrapes the abstract and not the guildines? |
the who landing page only carries a short overview so the guideline body was missing from every scraped document. each document now leads with the overview and continues with the linked pdf converted to markdown. metadata.content_scope records whether the body was recovered. docling was chosen after comparing it against pymupdf4llm and marker on five who guidelines. marker silently dropped rows from a grade evidence table and pymupdf4llm left bare page numbers in the text. the benchmark script and the measurements are committed so the choice can be rechecked. pdf conversion is an optional extra. without it a document falls back to the overview instead of failing so ci stays free of a pdf stack. two sample guideline pdfs and the markdown extracted from them are committed so the conversion can be read in review and tested offline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds a WHO scraper to the datasets scraping pipeline and extracts the actual guideline text out of the PDFs.
The first version of this PR only scraped the HTML Overview from each landing page. Review feedback was that the Overview is an abstract and the guideline itself lives in a linked PDF, so the corpus would have been a pile of one-paragraph summaries. That is fixed here. Each document now leads with the Overview and continues with the full guideline body converted from the PDF.
Shape matches the NICE scraper — discovery and extraction stay separate and everything comes out as a normalized
ScrapedDocument.How discovery works
WHO runs on Sitefinity not Next.js. Listing goes through their OData publications API filtered to the Guidelines publishing office UUID (
c09761c0-ab8e-4cfa-9744-99509c4d306b). That gives ~356 publications with$count=truefor the progress bar.One API quirk worth knowing:
$tophas to stay at 25 or below or WHO dropsDownloadUrlfrom the response entirely. Page size is set to 25 for that reason alone. SinceDownloadUrlis how we reach the PDF this is now load-bearing rather than cosmetic.How extraction works
Each publication page at
/publications/i/item/{id}is fetched and the Overview block undersection.dynamic-content__sectionis converted to markdown with the sharedhtml_to_markdownhelper. Targeting the content container instead of the whole page keeps nav and footer cruft out.Then the PDF. The download URL comes from the API or from the landing page link as a fallback. The PDF is fetched and converted to markdown by the new
scraping/pdf.py, and the result is appended to the Overview.A few details that matter:
(Strong recommendation, moderate-certainty evidence)legitimately repeat a dozen times.metadata.content_scopeisfulloroverview, andpdf_backendandpdf_bytesdescribe the conversion. The keys are always present so the HuggingFace schema stays stable.section_countis now the real section count instead of a hardcoded1.pdfextra. Without it every document degrades tooverviewrather than erroring, which is what keeps CI free of a heavy PDF stack.Choosing a PDF converter
Picking the converter is a correctness decision not a formatting one. A backend that silently drops rows from a GRADE evidence table produces text that reads perfectly and states the wrong thing, which is the worst possible failure for fact verification.
Docling, PyMuPDF4LLM and Marker were compared on excerpts of five WHO guidelines spanning 50 to 270 pages. All five turned out to be born-digital with a clean text layer, so OCR is pure overhead and is disabled.
Marker lost table rows. On the table-dense carbohydrate guideline it recovered 87 rows where Docling and PyMuPDF4LLM both found ~136, dropping outcome-category rows out of a GRADE evidence table. Nothing in the surrounding prose signals the table is now incomplete.
PyMuPDF4LLM is the fastest by a wide margin and its row counts are close, but it emitted 40 bare page-number lines across two documents and corrupted table header rows. Stray page numbers turn into sentence fragments once documents get chunked for retrieval.
Docling converted all five, recovered the most heading structure, left no page furniture behind and kept table rows intact. It runs on CPU so nobody needs a GPU to work on this, and it is MIT licensed. Its 126.5 s first run is one-off model loading, not per document cost.
One honest gap: on
9789240084278PyMuPDF4LLM emitted 42 table rows against Docling's 13 and I did not spot-check which is closer to the source. It did not change the decision since the other two failures are disqualifying on their own, but it is the open question if anyone revisits this.The benchmark script is committed so none of the above has to be taken on faith:
Seeing what comes out
datasets/test/fixtures/pdf/holds two six-page WHO guideline excerpts next to the markdown the scraper produces from each. Open them side by side to judge the conversion without running anything:who_9789240121805_excerpt.pdf→.expected.md— a GRADE recommendation keeping its certainty ratingwho_9789240124233_excerpt.pdf→.expected.md— a recommendation table keeping its rows and columnsHow this is tested
Three layers, arranged so a Docling release can never break the build.
Always runs, including CI. 47 tests covering listing parse, URL normalization, the Overview path, metadata, and the PDF path through an injected stub converter — full text, download URL taken from the page, conversion failure fallback, download failure fallback and full-text disabled. No PDF stack required.
Real conversion, opt-in.
uv sync --group pdfthen pytest runs actual Docling over the committed sample PDFs and asserts the recommendation text, its certainty rating, heading structure, table rows and the absence of running headers survive. It ends with an end-to-endscrape_publicationthat proves acontent_scope: fulldocument. These assert on meaning not bytes so a Docling upgrade that reflows whitespace will not fail them.Byte-exact diff, opt-in.
AMFV_CHECK_GOLDEN=1compares conversion against the committed.expected.mdfiles. Off by default because it is deliberately version sensitive. When Docling does change the output you review the diff and accept it withAMFV_UPDATE_GOLDEN=1, which rewrites the files so the change lands in review instead of passing silently.CI installs
--group devonly, so the PDF tests report as skipped there: 47 passed, 7 skipped in 0.77 s. With the extra installed it is 52 passed in ~35 s, or 54 with the golden check on.CLI
--source allincludes WHO alongside NICE.Licensing
WHO publications since Nov 2016 are CC BY-NC-SA 3.0 IGO. Each document carries
metadata.licenseandmetadata.attribution.Flagging this explicitly: the two fixture PDFs and their extracted markdown are WHO content under CC BY-NC-SA 3.0 IGO, which is more restrictive than this repo's Apache-2.0. Apache-2.0 covers the code and does not extend to those files. Source, page ranges and attribution are recorded in
LICENSE_NOTES.md. Happy to drop them for download-on-demand fixtures if that mix is not wanted.Docling itself is MIT.
Files
Scraper and conversion
datasets/amfv_datasets/scraping/pdf.py— new.pdf_to_markdown,PdfBackend,PdfConversionError, running-header stripping and section counting, with the optional Docling import gated behindHAS_DOCLINGdatasets/amfv_datasets/scraping/who.py— PDF fetch and convert wired intobuild_publication_text, realsection_count, content scope and PDF metadata, fallback on failuredatasets/amfv_datasets/scraping/__init__.py— re-exportsdatasets/pyproject.toml/pyproject.toml—pdfextra pinned todocling>=2.119.0,<3.0.0plus a rootpdfdependency groupEvidence and docs
datasets/benchmarks/pdf_backends.py— runnable backend comparisondatasets/benchmarks/README.md— measurements, the reasoning, and how to verify any of itdatasets/test/fixtures/pdf/— two guideline excerpts and their extracted markdowndatasets/amfv_datasets/scraping/LICENSE_NOTES.md— content scope semantics and fixture attributiondatasets/README.md— the optional extraTests
datasets/test/test_scraping_pdf.py— conversion helpers, header stripping, section countingdatasets/test/test_scraping_pdf_fixtures.py— real Docling over the sample PDFs plus the golden comparisondatasets/test/test_scraping_who.py— PDF path via an injected converter and both fallback routesTest plan
uv run pytestwith dev deps only — 47 passed, 7 skipped, no PDF stack neededuv sync --group pdf && uv run --group pdf pytest— 52 passed, real Docling over the sample PDFsAMFV_CHECK_GOLDEN=1 uv run --group pdf pytest datasets/test/test_scraping_pdf_fixtures.py— 7 passed, output matches committed markdownuv run ruff check .anduv run ruff format --check datasetsuv build --package amfv-datasetsscrape_publicationover a real WHO PDF —content_scope: full, 94 sections, ~54k chars, 0 running-header lines