做医药数据相关开发这几年,我越来越觉得 DailyMed 是个被低估的宝藏数据源。很多人一上手药物情报抓取,第一反应就是扑向 openFDA,因为它的接口直观,返回的是 JSON,文档也花哨;但真正跑起来做药品说明书结构化、标签对比、药物警戒监控之后,你会慢慢发现 openFDA 的 label 字段是经过二次整理的,有些原始说明书内容被丢弃,更新存在滞后,字段粒度也不够细。后来我把主数据源切到了 DailyMed——它由 NIH 下属的 NLM(美国国家医学图书馆)运营,提供的是官方审核后、可直接下载的 SPL(Structured Product Label)结构化产品标签,稳定、免费、不需要鉴权,而且在药品标签完整性上比 openFDA 细得多。这篇文章就把我做药物情报检索这段时间实践 DailyMed API 的完整过程拆出来,包括我怎么设计查询链路、怎么解析 SPL XML、中途踩过哪些坑,以及怎么把简单的 API 调用升级成一个可持续更新的药物标签知识库。适合正在做药品数据中台、说明书抽取、用药安全监测或者药学知识图谱的开发者参考。
1. 为什么我建议药物情报优先接 DailyMed,而不是只盯着 openFDA
1.1 DailyMed 在 NIH/NLM 药品标签体系中的位置
DailyMed 本质上不是"一个 API",而是 NLM 面向公众提供的美国上市药品标签发布平台。它的数据源头是 FDA 审核通过的 SPL 文件,经过 NLM 的加工、索引和在线发布,最终呈现在你面前。换句话说,DailyMed 是"FDA 审完的说明书"与"可以机器读取的结构化数据"之间的桥梁。
这里有个容易被忽略的点:FDA 是审核方,NLM 是发布方。DailyMed 挂在 NLM 域名下,NLM 又是 NIH 的下属机构,所以你在标题里看到 NIH/NLM 这个组合,指的是数据基础设施的归属,而不是数据来源本身。理解这一点非常关键,因为它决定了这套数据的权威性——你拿到的是美国药政体系里的"官方最终版标签",不是某个爬虫从 PDF 里 OCR 出来的野数据。
DailyMed 的覆盖范围也很值得说:它不仅包含处方药,还囊括非处方药、部分生物制剂、疫苗等,临床相关的说明标签都能在这里找到。每条标签对应唯一的 setid,同一药品的修订历史、包装规格、NDC 编码全部挂在同一个文档体系下,这对做历史版本回溯和包装层级分析来说,是难得的干净数据。
1.2 DailyMed 与 openFDA 的数据差异:一个原始标签,一个二次整理
很多人会问:openFDA 的 drug/label 接口也能拿说明书,为什么还要费劲去解析 SPL XML?我直接用实际体验来对比。
| 对比维度 | DailyMed | openFDA |
|---|---|---|
| 数据性质 | NLM 发布的原始 SPL 标签全文 | FDA 整理的 API 化 JSON,字段经过再分组 |
| 数据模型 | HL7 V3 XML,保留说明书原始章节结构和 XHTML 文本 | 扁平 JSON,章节字段有限,部分原始文本被裁剪 |
| 标签完整性 | 高,保留完整 section、子 section、表格、格式信息 | 中,部分字段如"患者信息"存在缺失 |
| 历史版本 | 较完整,同一 setid 下保留修订轨迹 | 相对有限,不利于版本差异分析 |
| 更新时效 | 标签审批后较快同步,越接近数据链源头 | 有一定处理链路,存在延迟场景 |
| 鉴权方式 | 无强制鉴权,合理限速即可 | 推荐申请 key,不强制但限流不同 |
| 适用场景 | 深度说明书解析、知识图谱、标签对比 | 快速查询、跨库统计、接口联调Demo |
最简单粗暴的类比是:openFDA 是"别人嚼过的精加工食品",DailyMed 是"从原产地直接发货的原材料"。如果你只是想知道某个药品的适应症大致写了什么,openFDA 够用;但如果你要做不良反应词条的结构化抽取、比较同一通用名下不同厂家的禁忌症差异、或者监控某款药说明书某一段落的修订历史,DailyMed 的 SPL 全文几乎是唯一能给你完整证据链的地方。
1.3 DailyMed API 的定位:适合谁、解决什么问题
把 DailyMed API 说成一个"查询接口"其实是低估了它。它更准确的角色应该是:一个面向药品标签全文的结构化数据分发服务。它解决的问题不是"给我几个字段",而是"把整个说明书变成可编程、可检索、可对比的结构化文档"。
适合用 DailyMed 的核心场景我列一下:
- 药品说明书结构化:从 SPL XML 里抽取适应症、禁忌症、不良反应、用法用量等标准 section;
- 药品情报检索:按通用名、商品名、NDC 反查标签,做竞品分析或管线调研;
- 药物警戒监控:定期拉取标签更新,比对版本号,发现说明书里警告内容的变化;
- 药学知识图谱:以 setid 为主键,把药品、成分、剂型、制造商、NDC 关联成网络;
- 临床决策支持:为院内用药系统提供结构化说明书数据。
我这里先说明一点:DailyMed API 是没有强制 API Key 的,这也是它经常被忽视的原因之一。没有 key 意味着上手成本低,但也意味着你需要自己控制调用频率,别把它当成不受限的公共资源那么薅。
2. 不了解 SPL,就谈不上结构化查询:先读懂药品标签的数据骨架
2.1 SPL 标准本质:HL7 V3 文档,不是普通 PDF
SPL 的全称是 Structured Product Label,它是 HL7 V3 标准下的一种 XML 文档类型。FDA 要求药企以 SPL 格式递交说明书,所以每一份美国上市药品的官方说明书,本质上都是一份结构化的 XML 文件,而 DailyMed 就是把这些文件标准化托管并对外分发。
你要把它和 PDF 说明书区分开。PDF 是给人看的,SPL 是给机器看的。PDF 里一段"禁忌症"可能是一行纯文本,SPL 里它会落在<section>节点的<text>子节点里,并且关联一个标准编码。解析 PDF 你需要 OCR、版面分析、清洗,解析 SPL 你只需要 XPath 和命名空间处理。这也是我坚持用 DailyMed 的原因——把文本抽取正确率从"看运气"提升到"看标准"。
一个典型的 SPL XML 根节点是这样的:
<document xmlns="urn:hl7-org:v3"> <id root="2.16.840.1.113883.4.699" extension="1a2b3c4d-xxxx-xxxx-xxxx-xxxxxxxxxxxx"/> <effectiveTime value="20240115"/> <versionNumber value="12"/> <component> <structuredBody> <!-- 这里的 component 里才是各种 section --> </structuredBody> </component> </document>看到这个结构你就该明白,用正则去抓说明书是下下策。正确做法是先把命名空间和节点层级弄清楚,再写提取逻辑。
2.2 SPL 的 section 编码体系:如何精准定位说明书章节
SPL 文档结构里,说明书正文由若干<section>组成,每个 section 都带有<title>和<code>。title 是给人看的章节名,code 是给机器用的标准代码。这套代码体系与 LOINC 有映射关系,在 DailyMed 的 XML 里通常能直接看到。
举个例子,一个 section 节点大致长这样:
<component> <section> <code code="34090-1" codeSystem="2.16.840.1.113883.6.1"/> <title>INDICATIONS AND USAGE</title> <text> <paragraph>Ibuprofen is a nonsteroidal anti-inflammatory drug...</paragraph> </text> </section> </component>注意这里的<text>节点内部不是纯文本,而是 XHTML 片段,可能有<paragraph>、<list>、<table>、<link>等各种子节点。这意味着你要抽取 section 内容时,不能简单调.text,得把整个 XHTML 子树里的文本全部拼接出来。
解析策略就很清晰了:先定位 section,再按 code 或 title 判断是哪个章节,最后把 text 子树里的文本完整提取。这套逻辑比 openFDA 的"固定字段名"更通用,因为无论说明书怎么改版,section 的标题和编码都是标准化的。
2.3 一条 SPL 标签里的关键节点地图
拿到一份 SPL XML 后,我建议先按下面的节点地图做一次人工扫描,再写代码:
| 节点路径 | 含义 | 为什么重要 |
|---|---|---|
/document/id | setid | 标签的唯一标识,所有版本共享同一个 setid |
/document/effectiveTime | 生效时间 | 判断标签版本新旧的关键 |
/document/versionNumber | 版本号 | 同一 setid 下修订次数的体现 |
manufacturedProduct | 生产商/产品信息 | 可提取 NDC、厂家名称、商品名 |
activeIngredient | 活性成分 | 提取成分名、剂量、强度 |
component/section | 说明书章节 | 适应症、禁忌症、不良反应等核心内容 |
实际解析时你会发现,一份 SPL 文件内部嵌套很深,尤其manufacturedProduct经常出现多层嵌套,activeIngredient的结构也可能因药企的书写习惯而异。不要指望一份 XPath 通吃所有标签,正确的姿势是先写一个递归遍历工具,把整个 XML 树的结构 dump 出来,人工确认一遍再固化提取逻辑。
3. DailyMed Services v2 端点实战拆解:从搜索路由到参数规范
3.1 调用方式与"无鉴权"的正确理解
DailyMed 的 REST 服务基础地址是https://dailymed.nlm.nih.gov/dailymed/services/v2。你可以直接在浏览器里打开这个路径,会看到可用端点的文档和示例。它没有强制要求 API Key,但这绝不意味着你可以无限制并发调用。
我的建议是:在请求头里声明一个可识别的 User-Agent,标明你的项目名称和用途,比如:
curl -H "User-Agent: my-drug-monitor/1.0 (contact: dev@example.com)" \ "https://dailymed.nlm.nih.gov/dailymed/services/v2/druglist.json?drug_name=ibuprofen"这既是工程礼仪,也是减少被限流的有效方式。绝大多数公共数据服务不会明文写"你必须带 User-Agent",但一旦出现反常流量,没有标识的请求最先被限制。
3.2 druglist:按药品名搜索的入口
拿到一个药品名后,第一件事通常是用druglist端点搜索它对应的drug_id。请求长这样:
curl "https://dailymed.nlm.nih.gov/dailymed/services/v2/druglist.json?drug_name=amoxicillin"返回结果里会包含符合条件的药品列表,核心字段是drug_id,这个 id 是 DailyMed 内部维护主键,后续获取详情都靠它。搜索逻辑对大小写不敏感,支持模糊匹配,但你要注意:drug_name匹配的是商品名和通用名,并不保证只返回一个结果。比如搜索 "ibuprofen",你会同时拿到不同规格、不同厂家的多条记录。
此时不要盲目取第一条,建议把返回结果里的drug_name、dosage_form、route打印出来,人工或代码过滤出目标剂型。很多医药数据项目最后跑偏,都是从"没过滤剂型,拿注射液说明书当口服片说明书用"开始的。
3.3 drug 详情与 NDC 反向检索
拿到drug_id后,访问https://dailymed.nlm.nih.gov/dailymed/services/v2/drug/{drug_id}.json可以获取这条药品记录的详情,包括生产商、剂量形式、给药途径、活性成分等等。
但实际工作中,我更多时候是先拿到一个 NDC(National Drug Code,国家药品代码),然后再反查它对应的标签。NDC 是美国药品流通环节里的"身份证号",在业务系统、供应链数据、保险理赔数据里非常常见。要把 NDC 和说明书关联起来,DailyMed 提供了spls端点:
curl "https://dailymed.nlm.nih.gov/dailymed/services/v2/spls.json?ndc=55111-0673-01"返回结果里最核心的是setid,拿到 setid 就等于拿到了这份标签的身份证,后续下载全文都靠它。这里有个细节:NDC 的格式并不统一,有的带连字符,有的没有,有的 10 位,有的 11 位,查询前最好先做标准化处理,否则很容易查不到。
3.4 spls 端点:用 setid 下载 SPL 全文
setid是 UUID 格式,是 SPL 标签在整个生命周期里不变的标识。拿到 setid 之后,用下面这个端点就能下载完整 XML:
curl "https://dailymed.nlm.nih.gov/dailymed/services/v2/spls/{setid}.xml"你也可以尝试在同路径下换成.json后缀,DailyMed 部分场景会返回 JSON 格式的标签结构,但我个人强烈建议直接用 XML。原因很简单:JSON 版本是经过映射的,section 树的结构可能被拍平,丢失嵌套,而 XML 是最原汁原味的 SPL 文档,解析逻辑一旦跑通,所有药品说明书都能复用同一套代码。
另外提醒一句,SPL 文件动辄几百 KB,包含大量 XHTML 文本,下载时不要用太短的超时时间,网络波动时会断。
3.5 端点速查表
| 端点 | 用途 | 关键参数 |
|---|---|---|
/druglist.json | 按药品名搜索药品列表 | drug_name |
/drug/{drug_id}.json | 获取药品详情 | 路径参数 |
/drug/{drug_id}/ndc.json | 获取该药品下 NDC 列表 | 路径参数 |
/spls.json | 按 NDC 或 setid 检索 SPL | ndc、setid |
/spls/{setid}.xml | 下载原始 SPL XML | 路径参数 |
/spls/{setid}.json | 下载 SPL 的 JSON 对照版本 | 路径参数 |
这几个端点加起来,已经能覆盖从"药品名"到"说明书全文"的完整检索链路。当然,DailyMed 服务端偶尔会调整参数和返回字段,用之前建议先打开基础地址看一眼官方示例,避免字段名发生偏移。
4. 一个完整的药物情报检索链路:从药品名到标签全文的 Python 实现
4.1 场景设定:帮我找布洛芬口服片的所有说明书章节
下面我用一个具体场景把整条链路串起来:我想找所有含布洛芬(ibuprofen)的口服片剂说明书,并抽取其中"适应症""禁忌症""不良反应"三段正文。
这个场景在药物情报检索里非常典型——输入一个通用名,输出一堆结构化标签章节。它既涉及检索,又涉及文本抽取,做完之后基本就能迁移到其他药品上。
4.2 第一步:按药品名搜索并过滤剂型
import requests import time BASE = "https://dailymed.nlm.nih.gov/dailymed/services/v2" HEADERS = { "User-Agent": "my-drug-monitor/1.0 (contact: dev@example.com)" } def search_drug_by_name(drug_name: str) -> list: resp = requests.get( f"{BASE}/druglist.json", params={"drug_name": drug_name}, headers=HEADERS, timeout=30, ) resp.raise_for_status() data = resp.json() # 兼容不同返回结构:可能是列表,也可能是带 drugList 字段的对象 if isinstance(data, list): return data return data.get("drugList") or data.get("results") or [] def filter_by_dosage_form(drugs: list, keywords=("tablet", "oral")) -> list: result = [] for d in drugs: name = (d.get("drug_name") or "").lower() form = (d.get("dosage_form") or "").lower() if "ibuprofen" in name and any(k in form for k in keywords): result.append(d) return result这一步的要点是先搜宽、再过滤。不要指望drug_name=ibuprofen只返回目标结果,因为 DailyMed 的搜索是按包含关系匹配的。拿到列表后,用dosage_form字段过滤掉注射液、胶囊、混悬液,只保留片剂和口服相关剂型。
4.3 第二步:取 drug_id 后按 NDC 或 setid 跳转 SPL
过滤后的记录里会带drug_id,但我们需要的是说明书全文,所以下一步是拿到这条记录的 NDC 列表,再用 NDC 去反查 setid:
def get_ndc_list(drug_id: str) -> list: resp = requests.get(f"{BASE}/drug/{drug_id}/ndc.json", headers=HEADERS, timeout=30) resp.raise_for_status() data = resp.json() if isinstance(data, list): return [item.get("ndc") or item.get("ndc11") for item in data if item.get("ndc")] return data.get("ndcList") or [] def get_spl_by_ndc(ndc: str) -> dict: resp = requests.get( f"{BASE}/spls.json", params={"ndc": ndc}, headers=HEADERS, timeout=30, ) resp.raise_for_status() data = resp.json() if isinstance(data, list): return data[0] if data else {} return data.get("splList") or {}这里有一个非常重要的工程判断:为什么不直接用drug_id拿一个 setid,而是要多绕一道 NDC?因为一个drug_id下可能对应多个包装规格、多个 NDC,每个 NDC 对应的 SPL 版本可能不同。如果你只取第一个,很可能拿到的是一个特定包装的说明书,而不是该药品的完整标签信息。用 NDC 反查能让你的数据更贴近业务侧的真实使用场景。
4.4 第三步:解析 SPL XML,按 section 抽取正文
拿到 setid 后,下载 SPL XML:
def fetch_spl_xml(setid: str) -> str: resp = requests.get(f"{BASE}/spls/{setid}.xml", headers=HEADERS, timeout=60) resp.raise_for_status() return resp.text接下来是最核心的解析部分。需要注意两点:第一,必须带 HL7 命名空间;第二,section 的text节点是 XHTML 树,不能直接.text取值。
import xml.etree.ElementTree as ET HL7_NS = "{urn:hl7-org:v3}" def extract_section_text_by_keywords(root, keywords): """ 遍历所有 section,按 title 或 code 匹配关键词, 返回 {title: text} 的映射。 """ results = {} for section in root.iter(f"{HL7_NS}section"): title_el = section.find(f"{HL7_NS}title") title = title_el.text.strip() if title_el is not None and title_el.text else "" if not any(kw.lower() in title.lower() for kw in keywords): continue text_el = section.find(f"{HL7_NS}text") if text_el is None: continue # itertext() 会把整个 XHTML 子树里的所有文本拼接起来 content = "".join(text_el.itertext()).strip() if content: results[title] = content return results def parse_spl_label(xml_text: str): root = ET.fromstring(xml_text) setid_el = root.find(f"{HL7_NS}id") version_el = root.find(f"{HL7_NS}versionNumber") # 提取活性成分,注意层级用递归查,避免不同药企结构差异 ingredients = [] for ing in root.iter(f"{HL7_NS}activeIngredient"): names = [n.text for n in ing.iter(f"{HL7_NS}name") if n.text] if names and names[0].strip(): ingredients.append(names[0].strip()) sections = extract_section_text_by_keywords( root, ["INDICATIONS AND USAGE", "CONTRAINDICATIONS", "ADVERSE REACTIONS"], ) return { "setid": setid_el.get("extension") if setid_el is not None else None, "version": version_el.get("value") if version_el is not None else None, "ingredients": list(set(ingredients)), "sections": sections, }这段代码我实际跑过很多次,最坑的地方就是activeIngredient的层级。有的标签是activeIngredient -> ingredient -> name,有的中间还夹了别的节点,直接用固定路径很容易漏。用iter递归找所有name节点是相对稳妥的降级方案,虽然偶尔会多抓一两个非成分名,但至少不会返回空。
4.5 组装结果:把抽取数据落成 JSON
最后把上述环节串起来:
def build_label_monitor(ingredient: str, dosage_keywords=("tablet", "oral")): drugs = search_drug_by_name(ingredient) filtered = filter_by_dosage_form(drugs, dosage_keywords) for drug in filtered[:5]: # 示例限制 5 条 drug_id = drug.get("drug_id") if not drug_id: continue ndc_list = get_ndc_list(drug_id) for ndc in ndc_list: spl_info = get_spl_by_ndc(ndc) setid = spl_info.get("setid") if not setid: continue xml_text = fetch_spl_xml(setid) parsed = parse_spl_label(xml_text) parsed["ndc"] = ndc parsed["drug_name"] = drug.get("drug_name") parsed["dosage_form"] = drug.get("dosage_form") parsed["fetched_at"] = time.strftime("%Y-%m-%d %H:%M:%S") yield parsed time.sleep(1) # 限速,别太猛跑一遍这个生成器,你就得到了布洛芬口服片剂的说明书结构化结果。每一段都被切分到对应的 section 标题下,后面做全文检索、知识图谱、标签对比,全是水到渠成的事。
5. 实测踩坑记录:NDC 格式、空字段、429 限流与更新延迟
5.1 NDC 格式的连环坑:10 位、11 位、连字符
NDC 是我在 DailyMed 实战里遇到的第一个大坑。标准 NDC 是 10 位数字,分成三段,比如55111-0673-01;但很多系统里存的是 11 位数字,比如55111067301,开头补了一个 0。DailyMed 的spls.json?ndc=参数对格式很敏感,你传 10 位和传 11 位可能查出来的结果不同。
我的处理办法是统一做规范化:
def normalize_ndc(ndc: str) -> str: digits = "".join(ch for ch in ndc if ch.isdigit()) # 在 DailyMed 检索时优先用带连字符的 10 位形式 if len(digits) == 11: digits = digits[1:] # 去掉前导补零 return f"{digits[:5]}-{digits[5:9]}-{digits[9:]}"当然,不同业务系统对 NDC 的规范不一样,这个函数只能作为参考。关键是你要意识到:同一个药品在不同系统里的 NDC 表现形态可能不同,查询前必须统一格式,否则你会花大量时间查"查不到"的问题。
5.2 SPL 的 text 是 XHTML,直接.text会丢一半内容
我在第一次解析 SPL 的时候犯过一个经典错误:用section.find('text').text去取正文,结果发现好多段落是空的,只有第一行文字。后来才意识到,text节点底下还有<paragraph>、<list>、<table>这些子元素,.text只会拿到直接子节点的文本,不会递归拼接。
正确做法是我上一节代码里写的"".join(text_el.itertext())。这行代码会把 XHTML 子树里所有文本节点拼成一句完整的话,包括表格里的数字、列表里的条目。代价是丢掉了段落和表格的结构信息,但绝大多数药物情报检索场景里,正文内容比排版结构重要得多。
如果你连表格结构也要保留,那就要再写一个 XHTML 遍历器,把<table>转成二维数组。这个复杂度会明显上升,建议按需引入。
5.3 空字段与缺失 section 的防御
不是每份说明书都包含所有 section。OTC 药品和处方药的结构差异很大,有些非处方药的说明书里根本没有"非临床毒理学",甚至"不良反应"都写得很简略。
解析代码里必须对 section 缺失做防御性处理,否则你会得到大量None。我的做法是在extract_section_text_by_keywords里直接跳过没有 text 的 section,保留一个空字符串作为占位。这样后面的下游分析只需要判断if sections.get("ADVERSE REACTIONS")即可,不会因为字段缺失而崩溃。
5.4 标签更新延迟与版本号判断
DailyMed 的数据虽然有较高的更新频率,但它毕竟不是实时的。你拉回来的标签可能存在两种情况:一是某药品的说明书刚刚修订,DailyMed 还没同步;二是你已经拉到了新版本,但下游业务系统还在用旧版本。
解决思路是在每次抓取时记录两个关键字段:effectiveTime和versionNumber。setid不变,但每次修订都会让versionNumber增加。我通常会在数据库里建一个唯一约束(setid, versionNumber),用INSERT ... ON CONFLICT DO NOTHING做幂等写入,这样重复拉取不会产生脏数据。
5.5 429 限流与重试策略
DailyMed 没有强制鉴权,但内部当然有速率限制。我曾经写过一个很激进的多线程抓取脚本,一口气跑了 200 个 NDC,结果被连续返回 429 状态码。当时的教训是:不要用无脑多线程去打公共数据服务。
后来的稳定方案是单线程 + 固定延时 + 指数退避重试:
import time def request_with_retry(url, params=None, max_retry=5): for attempt in range(max_retry): resp = requests.get(url, params=params, headers=HEADERS, timeout=60) if resp.status_code == 200: return resp if resp.status_code == 429: wait = 2 ** attempt print(f"429, wait {wait}s") time.sleep(wait) continue resp.raise_for_status() raise RuntimeError("max retry exceeded")这个重试逻辑虽然简单,但在实际抓取中非常管用。官方没有给一个明确的 QPS 上限,我自己的经验是控制在每秒 1 次左右,基本不会触发限流。如果你确实需要批量拉全量数据,DailyMed 还提供 FTP 方式的 SPL 批量文件下载,那才是为大数据量设计的通道,API 更适合做增量查询和在线检索。
6. 从查询升级到情报:把 SPL 变成可复用的本地知识库
6.1 数据表设计:以 setid 为主键,以版本号为修订刻度
把 DailyMed API 跑通只是第一步,真正的价值在于你能否把药品标签沉淀成自己的数据资产。我最终落地的方案是在 PostgreSQL 里建了两张核心表。
第一张表存药品主档:
CREATE TABLE drug_label_main ( setid UUID PRIMARY KEY, ndc VARCHAR(20) NOT NULL, drug_name TEXT NOT NULL, dosage_form TEXT, active_ingredients JSONB DEFAULT '[]', manufacturer TEXT, effective_time DATE, version_number INT, raw_xml_path TEXT, last_fetched_at TIMESTAMP DEFAULT now() );第二张表存按 section 切分的文本:
CREATE TABLE label_section ( id BIGSERIAL PRIMARY KEY, setid UUID NOT NULL, ndc VARCHAR(20) NOT NULL, version_number INT NOT NULL, section_code VARCHAR(20), section_title TEXT, content_text TEXT, created_at TIMESTAMP DEFAULT now(), UNIQUE (setid, ndc, version_number, section_title) );这样设计的好处是:主档保持每一条标签的最新状态,label_section则记录每次修订下每个章节的内容快照。后续你要做说明书差异对比,只需要按(setid, version_number)分组,比较content_text是否变化。
6.2 结合 RxNorm/RxClass 做药品名标准化
DailyMed 里的药品名是厂商递交的原生名称,同一通用名可能拼写不同,商品名更是五花八门。如果你要做跨库关联,强烈建议引入 NLM 的另一套服务 RxNav,把药品名映射到 RXCUI(RxNorm 概念唯一标识)。
调用方式非常简单:
curl "https://rxnav.nlm.nih.gov/REST/rxcui.json?name=ibuprofen"拿到 RXCUI 之后,再配合 DailyMed 的 NDC 反查,就能实现"业务系统里的药品名 -> RXCUI -> NDC -> DailyMed setid -> SPL 全文"的无缝链路。这一步做完,你的数据就不只是"说明书文本库",而是真正的药物情报网络。
6.3 用标签 section 做说明书差异对比
有了结构化的label_section表,你能做很多有价值的事情。举一个我实际做过的例子:比较同一通用名(比如布洛芬)下不同厂家说明书的"禁忌症"差异。
SQL 写出来很直白:
SELECT drug_name, setid, content_text FROM label_section WHERE section_title ILIKE '%contraindications%' AND setid IN ( SELECT setid FROM drug_label_main WHERE drug_name ILIKE '%ibuprofen%' );拿到结果后,你可以用文本相似度算法或直接 diff,找出"这个厂家写了、那个厂家没写"的禁忌条目。这类信息在药品采购决策、说明书合规审查、竞品安全情报里都很有价值。
6.4 持续更新的增量方案
DailyMed 是持续更新的,标签会随着 FDA 审批节奏不断修订。我在生产环境用的是一个定时任务,每天跑一次增量更新:先通过 DailyMed 的更新列表或直接按 NDC 列表逐个重查,重点是拿spls.json返回里的published_date或versionNumber做比较,只有版本号变化才重新下载 XML。
这里有一个需要注意的取舍:全量重查最省事,但会消耗大量请求配额;只查增量最经济,但需要你维护一份历史版本号和日期的表。我最终选择的是折中方案——每周做一次全量巡检,每天只对高关注的药品做增量探测。
把流程稳定跑起来之后,DailyMed 就不再是"偶尔查一下的接口",而是一个可以持续产出药物情报的数据基座。我个人在实际操作中最深的体会是:DailyMed 的价值不是某一个端点,而是它把药品说明书从"PDF 文档"变成了"可以程序化操作的数据结构"。最后再分享一个小技巧:每次抓下来的 SPL XML 记得存一份原始文件,命名用setid_version.xml,别只存解析后的 JSON。很多历史问题排查到最后,都是靠原始 XML 定位的——数据可以加工,但原始凭证不能丢。