4、数据获取与预处理:API调用、爬虫、数据抓取、格式转换、去重归纳总结
药物图谱的质量,很大程度上取决于源头数据。我自己就踩过不少坑——从公开 API 抓回来的药物结构,格式千奇百怪;爬虫抓的中文文献,编码乱得一塌糊涂。所以这一章,我重点讲讲怎么把数据“洗”干净,变成图谱能用的样子。
4.1 常用的数据来源
做药物图谱,数据来源其实挺明确的。我用几个大方向归纳一下:
| 来源类型 | 举例 | 说明 |
|---|---|---|
| 公共药物数据库 | DrugBank、ChEMBL、PubChem | 结构化好,通过 API 或下载包获取 |
| 文献与专利 | PubMed、CNKI、Google Patents | 通常需要爬虫或批量下载 |
| 临床数据 | ClinicalTrials.gov | 结构化 XML/JSON,适合解析 |
| 商业/内部数据库 | 内部实验记录、药企合作数据 | 多为 Excel、CSV、SQL 导出 |
我个人习惯,优先走 API。为什么呢?稳定性高,更新及时。但有些数据源没有 API,那就得用爬虫。这两种方式,我接下来分别说。
4.2 API 调用:结构化数据的直通车
API 是最省事的。你想想看,只要调对参数,返回的就是 JSON 或 XML,省去大量解析工作。以 PubChem 的 REST API 为例,我经常这么用:
4.2.1 PubChem API 示例:按药物名称查询 CID
import requests
drug_name = "Ibuprofen"
url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{drug_name}/cids/JSON"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
cid = data["IdentifierList"]["CID"][0]
print(f"CID: {cid}")
else:
print(f"请求失败: {response.status_code}")
4.2.2 批量查询与数据缓存
批量查 CID 再查属性,是常见操作。但要注意频率限制。PubChem 建议每秒不超过 5 个请求。我习惯加个调度器:
import time
import requests
cid_list = ["3672", "3673", "3674"] # 示例CID
base_url = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{}/property/MolecularFormula,MolecularWeight/JSON"
for cid in cid_list:
url = base_url.format(cid)
resp = requests.get(url)
if resp.status_code == 200:
print(resp.json())
else:
print(f"CID {cid} 查询失败")
time.sleep(0.3) # 限速
4.3 爬虫抓取:应对非结构化数据
API 搞不定的时候,就得爬虫出场了。比如说抓取某中文药品说明书的网页。我曾在某个项目中,需要从几十个网站抓取药品不良反应数据。嗯,这里要注意——爬虫不是万能药,合规是第一位的。
4.3.1 简单爬虫示例:抓取药品通用名
import requests
from bs4 import BeautifulSoup
url = "https://example.com/drug/ibuprofen"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
# 假设药品通用名在 class="drug-name" 的 h1 里
name_tag = soup.find("h1", class_="drug-name")
if name_tag:
drug_name = name_tag.get_text(strip=True)
print(f"药品名称: {drug_name}")
else:
print("未找到药品名称")
4.3.2 多页抓取与数据清洗
多页抓取时,我一般用循环控制。抓下来的数据经常带有 HTML 标签、空格、换行。这时候用正则或 BeautifulSoup 的 get_text() 清理。
import re
raw_text = " Ibuprofen\n(Ibuprofen)\n"
cleaned = re.sub(r"\s+", " ", raw_text).strip()
print(cleaned)
4.4 格式转换:统一为图谱能识别的样子
数据进了系统,格式五花八门。最常见的是 JSON、XML、CSV。我建议全部转为 JSON,因为 RDF 和 Neo4j 的导入工具对 JSON 支持最好。
| 源格式 | 转换目标 | 常用工具 |
|---|---|---|
| CSV | JSON | pandas.to_json() |
| XML | JSON | xmltodict + json |
| Excel | JSON | pandas.read_excel() + to_json() |
举个例子,CSV 转 JSON 特别简单:
import pandas as pd
df = pd.read_csv("drug_list.csv")
df.to_json("drug_list.json", orient="records", indent=2, force_ascii=False)
怎么确认转对了?我会选一条数据,看看键值是不是对应。我曾经遇到过 CSV 编码是 GBK,忘了指定 encoding,结果中文全变乱码。所以强烈建议:明确指定编码。
4.5 去重与归纳:保证图谱的唯一性
数据合并时,重复是常态。比如同一个药物在 DrugBank 和 PubChem 里都出现,但名字写法不同——“Ibuprofen”和“布洛芬”。去重分两步:
- 精确去重:基于唯一标识符(如 CAS 号、CID)
- 模糊去重:基于名称相似度(如编辑距离、Jaccard)
4.5.1 精确去重:用 CID 作为主键
import json
with open("drug_list.json", "r", encoding="utf-8") as f:
drugs = json.load(f)
seen_cids = set()
unique_drugs = []
for drug in drugs:
cid = drug.get("cid")
if cid and cid not in seen_cids:
seen_cids.add(cid)
unique_drugs.append(drug)
print(f"去重前: {len(drugs)} 条, 去重后: {len(unique_drugs)} 条")
4.5.2 模糊去重:名称相似度匹配
对于没有 CID 的数据,我用 difflib 做快速匹配:
from difflib import SequenceMatcher
name_pairs = [("Aspirin", "Acetylsalicylic acid"), ("Ibuprofen", "布洛芬")]
for a, b in name_pairs:
similarity = SequenceMatcher(None, a.lower(), b.lower()).ratio()
print(f"{a} <-> {b}: 相似度 {similarity:.2f}")
4.6 归纳总结:为图谱构建做准备
去重之后,数据就进入“归纳”阶段。说白了,就是把数据从“扁平清单”变成“实体-关系”的初步结构。
我常用的归纳步骤:
- 实体提取:从字段中识别出药物、靶点、疾病等实体
- 关系推断:根据字段关联(如药物-靶点结合)生成关系
- 属性标准化:统一单位、日期格式、命名规范
比如,一个 CSV 行代表“药物A 作用于 靶点B”,我们就可以提取出:
- 实体:药物A、靶点B
- 关系:作用于
- 属性:作用强度、来源文献
4.7 本章知识体系
下面这张图,就是整个数据获取与预处理的流程。我画成 SVG,方便你对照着看。
数据获取与预处理这部分,我讲得比较细。因为后面所有图谱质量的根基,都在这。你把这些工作做到位了,图谱的更新维护自然就稳了。