Search PubMed (citations/abstracts) and PMC (full text) programmatically
with NCBI E-utilities — the stable HTTP interface to Entrez. The core pattern
is two steps: ESearch returns matching record IDs (PMIDs), then EFetch (or
ESummary) downloads the records. The Entrez History server (usehistory=y)
lets you chain the two without re-sending thousands of IDs.
E-utilities are public. No key is required, but a free API key raises your limit from 3 to 10 requests/second and is strongly recommended for batch work.
For ClinicalTrials.gov use searching-clinicaltrials; this skill is for the
published literature.
Base URL: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/. JSON for ESearch/
ESummary via retmode=json; EFetch returns text or XML (no JSON for PubMed).
import requests, time
BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
API_KEY = None # set to your free NCBI key to get 10 req/s instead of 3
def _params(**kw):
if API_KEY:
kw["api_key"] = API_KEY
return kw
def esearch(term: str, retmax: int = 50) -> dict:
"""Find PMIDs; usehistory=y stores them on the Entrez History server."""
r = requests.get(f"{BASE}/esearch.fcgi", params=_params(
db="pubmed", term=term, retmax=retmax,
usehistory="y", retmode="json"), timeout=30)
r.raise_for_status()
res = r.json()["esearchresult"]
return {"count": int(res["count"]), "ids": res["idlist"],
"webenv": res["webenv"], "query_key": res["querykey"]}
def efetch_abstracts(webenv: str, query_key: str, retmax: int = 50) -> str:
"""Pull abstracts by reference to the stored result set (no ID list needed)."""
r = requests.get(f"{BASE}/efetch.fcgi", params=_params(
db="pubmed", WebEnv=webenv, query_key=query_key,
retmax=retmax, rettype="abstract", retmode="text"), timeout=60)
r.raise_for_status()
return r.text
hits = esearch('("type 2 diabetes"[MeSH]) AND metformin AND 2023:2025[pdat]')
print(hits["count"], "papers")
abstracts = efetch_abstracts(hits["webenv"], hits["query_key"])
Equivalent cURL (search then fetch one PMID's abstract):
curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=metformin&retmode=json"
curl "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=38000000&rettype=abstract&retmode=text"
When you need titles/authors/journal/date as JSON (not the full abstract), use ESummary — it returns one record per ID:
def esummary(ids: list[str]) -> dict:
r = requests.get(f"{BASE}/esummary.fcgi", params=_params(
db="pubmed", id=",".join(ids), retmode="json"), timeout=30)
r.raise_for_status()
return r.json()["result"] # keyed by PMID: title, pubdate, source, authors…
For PMC full text, repeat with db=pmc and EFetch rettype=""/retmode=xml
(JATS XML). Respect each article's license before redistributing full text.
"<disease>"[MeSH] AND <drug>[tiab] AND 2020:2025[pdat]. Use
[tiab] (title/abstract), [au] (author), [pdat] (publication date).usehistory=y to capture WebEnv + query_key and the count.openmed.analyze_text to extract diseases, drugs,
genes, and oncology entities for downstream synthesis.openmed.analyze_text(note) yields Disease,
Pharmaceutical, Genomics, and Oncology entities. Turn the top spans into the
ESearch term (optionally grounded: ICD-10 label, RxNorm ingredient, gene
symbol) to retrieve targeted evidence.openmed.analyze_text(abstract, model_name="disease_detection_superclinical")
(or a Genomics/Oncology model) to structure the literature into entities for
evidence tables or knowledge-graph edges.api_key, throttle, and retry with backoff. NCBI also requests a
tool= and email= parameter identifying your application.retmode=text (human-readable) or
retmode=xml (PubMedArticle XML) and parse XML for structured fields.WebEnv/query_key are session-scoped — fetch promptly
after searching, or re-run ESearch.retstart/retmax (or history) rather than
pulling everything at once; cap total fetches.[tiab]
term variants so you do not miss them.