拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python爬虫构建医疗知识图谱:Scrapy+Playwright解析ClinicalTrials.gov

简介这是一份面向高校计算机及相关专业学生如人工智能、自动化、电子信息等的医疗数据工程实战资源聚焦ClinicalTrials.gov公开临床试验数据的采集与知识图谱构建解决课程设计、毕业设计及科研入门中缺乏端到端医疗数据处理案例的痛点。压缩包共36个文件含14个核心Python脚本如cerebrovascular_kg.py、scrapy_cerebrovascular/等爬虫与图谱构建模块、9个XML配置与结构化模板、2个Markdown教程文档含项目说明与README、以及requirements.txt、数据库连接脚本和CSV数据集等整体仅1.87MB轻量易部署。已有52人学习下载资源经严格测试可稳定运行提供完整设计文档、分步操作指引与本地-服务器部署支持特别适合零基础学习者快速上手爬虫开发、实体关系抽取及Neo4j图数据库加载全流程亦可作为毕设原型直接扩展功能。1. 用 Python 爬取 ClinicalTrials.gov 构建医疗知识图谱不是“下载数据表”而是打通临床试验实体关系链ClinicalTrials.gov 是全球最大的公开临床试验注册库截至2024年已收录超43万项试验涵盖药物、器械、行为干预等全类型研究。但它的官方 API如 RESTful v2仅支持简单字段检索不返回嵌套的纳入/排除标准、不良事件详情、研究中心地理分布等关键结构化信息网页端又大量依赖 JavaScript 渲染动态表格与分页 iframe——这意味着单纯调用requests.get()拿不到完整试验详情而用 Selenium 全量渲染则吞吐量低、内存泄漏风险高。本项目聚焦真实落地场景从原始 HTML 中精准提取「试验ID→干预措施→疾病靶点→研究中心→结果指标」五类核心实体并建立带语义约束的三元组如(NCT04567890, hasIntervention, Pembrolizumab)最终导入 Neo4j 构成可查询、可推理的医疗知识图谱。适合有 Python 基础、熟悉 HTTP 协议但未实操过医疗领域爬虫的开发者也适合作为高校医学信息学、生物信息学课程的大作业选题——所有代码均基于稳定版 Scrapy Playwright 组合规避了传统 Selenium 的资源开销问题且能绕过 ClinicalTrials.gov 对 headless 浏览器的 UA 指纹检测。2. 为什么必须用 Playwright 驱动 ScrapyClinicalTrials.gov 的反爬机制与动态加载真相ClinicalTrials.gov 的页面结构存在三层动态加载逻辑这是纯静态解析失败的根本原因。理解这三层机制才能合理选择工具链和参数配置。2.1 页面加载的三重动态屏障第一层是初始 HTML 的骨架化主页面仅包含div idcontent/div占位符所有试验摘要数据由main.js通过fetch()请求/ct2/show/NCTxxxxxxx接口填充。该接口返回 JSON但需携带X-Requested-With: XMLHttpRequest头且 URL 路径中的 NCT 编号必须存在于当前页面 DOM 的隐藏 input 中如input typehidden nameid valueNCT04567890。第二层是详情页的 iframe 嵌套点击“Study Details”后实际加载的是https://clinicaltrials.gov/ct2/show/results/NCTxxxxxxx?viewresult该页面内嵌iframe src/ct2/results/...而 iframe 内容需二次触发window.postMessage才开始渲染表格。第三层是分页与筛选的 SPA 行为搜索结果页的“Next Page”按钮不跳转 URL而是调用history.pushState()并触发MutationObserver监听 DOM 变化再异步拉取新批次 JSON 数据。提示若强行用requests模拟请求会因缺失__RequestVerificationTokenCSRF token、ASP.NET_SessionId会话 cookie及X-Forwarded-ForIP 地理位置校验被返回 403 或空 JSON。这些 token 在首次 GET 主页时由服务器 Set-Cookie 发送且有效期仅 15 分钟。2.2 Scrapy Playwright 组合的技术合理性Scrapy 负责调度、去重、管道管理与数据持久化Playwright 负责真实浏览器上下文执行 JS、等待 iframe 加载完成、提取动态 DOM。二者通过scrapy-playwright插件桥接避免了 Selenium 的进程管理缺陷如 ChromeDriver 进程残留导致内存溢出。关键优势在于Playwright 支持wait_for_selector(iframe#results-frame)精确等待 iframe 加载再用frame.content()获取其内部 HTML可设置user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36模拟真实用户规避 User-Agent 黑名单自动处理 cookie 同步Playwright 上下文中的 cookies 会自动注入到 Scrapy 的Request.cookies中无需手动提取。2.3 安装与基础配置确保 Playwright 兼容 ClinicalTrials.gov 的 Chromium 版本# 创建隔离环境推荐 Python 3.9 python -m venv ct_env source ct_env/bin/activate # Linux/macOS # ct_env\Scripts\activate # Windows # 安装核心依赖注意版本约束 pip install scrapy2.11.2 \ scrapy-playwright0.0.12 \ playwright1.43.0 \ lxml4.9.4 \ neo4j5.20.0 # 下载 Playwright 浏览器必须指定 chromiumfirefox 不支持部分 iframe 通信 playwright install chromium注意scrapy-playwright0.0.12是目前唯一兼容 Scrapy 2.11.x 且支持page.frame_locator()的稳定版本。若使用更高版 Playwright如 1.44需降级或修改middlewares.py中的Page.goto()调用方式否则会报AttributeError: Page object has no attribute frame_locator。在settings.py中启用中间件并配置 Playwright# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor PLAYWRIGHT_BROWSER_TYPE chromium PLAYWRIGHT_LAUNCH_OPTIONS { headless: True, # 生产环境必须设为 True args: [ --no-sandbox, --disable-setuid-sandbox, --disable-gpu, --disable-dev-shm-usage, --disable-blink-featuresAutomationControlled, # 关键绕过自动化检测 ], }3. 实现 ClinicalTrials.gov 详情页的精准解析从 NCT ID 到结构化三元组本节以单个试验页面如https://clinicaltrials.gov/ct2/show/NCT04567890为例展示如何用 Playwright 提取嵌套 iframe 中的结构化数据并转换为知识图谱所需的实体-关系格式。3.1 定义 Spider声明 Playwright 驱动与等待策略# spiders/ct_spider.py import scrapy from scrapy_playwright.page import PageMethod class ClinicalTrialsSpider(scrapy.Spider): name ct_spider allowed_domains [clinicaltrials.gov] def start_requests(self): # 示例从搜索页获取前10个NCT ID实际项目中应从 CSV 或数据库读取 nct_ids [NCT04567890, NCT04321996, NCT04281314] for nct_id in nct_ids: url fhttps://clinicaltrials.gov/ct2/show/{nct_id} yield scrapy.Request( urlurl, meta{ playwright: True, playwright_page_methods: [ # 等待主页面加载完成关键等待右侧导航栏出现 PageMethod(wait_for_selector, nav#study-nav), # 等待 iframe 加载并切换上下文 PageMethod(wait_for_selector, iframe#results-frame), PageMethod(frame_locator, iframe#results-frame), PageMethod(wait_for_selector, table.results-table), # 等待结果表格渲染 ], playwright_include_page: True, }, callbackself.parse_trial, ) def parse_trial(self, response): # 获取 Playwright Page 对象 page response.meta[playwright_page] # 提取主页面静态字段试验标题、状态、招募日期 title response.css(h1.title::text).get(default).strip() status response.css(div.status::text).re_first(rStatus:\s*(\w)) or Unknown # 从 iframe 中提取结果数据需在 Playwright 上下文中执行 results_html page.frame_locator(iframe#results-frame).inner_html(table.results-table) # 解析 results_html 为结构化字典见 3.2 节 results_data self.parse_results_table(results_html) # 构建三元组列表 triples self.build_triples(response.url, title, status, results_data) yield { nct_id: response.url.split(/)[-1], title: title, status: status, triples: triples, }3.2 解析 results-table用 lxml 处理 iframe 内部 HTML 的健壮方案ClinicalTrials.gov 的 results-table 包含多级嵌套tbody和tr且列名如 “Measure”, “Time Frame”, “Outcome”可能跨行合并。直接用 CSS 选择器易漏数据推荐用 lxml 的 XPath 定位# utils/parser.py from lxml import html import re def parse_results_table(html_content: str) - dict: 解析 results-table HTML返回标准化字典 返回结构{ measures: [{name: PFS, time_frame: 12 months, outcome: Improved}], adverse_events: [{term: Neutropenia, frequency: 15%}] } tree html.fromstring(html_content) # 提取 Measures 表格通常第一个 tbody measures [] measure_rows tree.xpath(//tbody[1]//tr[not(classheader-row)]) for row in measure_rows: cells row.xpath(./td/text() | ./td/div/text()) if len(cells) 3: measures.append({ name: re.sub(r\s, , cells[0]).strip(), time_frame: re.sub(r\s, , cells[1]).strip(), outcome: re.sub(r\s, , cells[2]).strip(), }) # 提取 Adverse Events 表格通常第二个 tbody adverse_events [] ae_rows tree.xpath(//tbody[2]//tr[not(classheader-row)]) for row in ae_rows: cells row.xpath(./td/text() | ./td/div/text()) if len(cells) 2: adverse_events.append({ term: re.sub(r\s, , cells[0]).strip(), frequency: re.sub(r\s, , cells[1]).strip(), }) return {measures: measures, adverse_events: adverse_events}3.3 构建知识图谱三元组定义医疗领域语义关系知识图谱的价值在于关系的可计算性。ClinicalTrials.gov 数据需映射为带业务语义的三元组而非简单字段拼接。以下是本项目采用的核心关系模式符合 SNOMED CT 与 UMLS 的语义约定主体Subject关系Predicate客体Object说明NCT04567890hasInterventionPembrolizumab干预措施名称需标准化为 RxNorm IDNCT04567890targetsDiseaseNSCLC疾病名称需映射至 MeSH IDNCT04567890reportsOutcomePFS结果指标缩写需链接至 LOINC 术语NCT04567890conductedAtMD Anderson Cancer Center研究中心名称需地理编码为 GeoNames ID# utils/kg_builder.py def build_triples(nct_id: str, title: str, status: str, results_data: dict) - list: triples [] # 1. 试验-干预关系从标题和干预章节提取 interventions extract_interventions(title) # 自定义函数正则匹配 Pembrolizumab、Nivolumab 等 for drug in interventions: triples.append((nct_id, hasIntervention, drug)) # 2. 试验-疾病关系从条件字段提取 diseases extract_diseases(title) # 如 NSCLC, Metastatic Breast Cancer for disease in diseases: triples.append((nct_id, targetsDisease, disease)) # 3. 试验-结果指标关系从 results-table 解析 for measure in results_data.get(measures, []): if measure[name]: triples.append((nct_id, reportsOutcome, measure[name])) # 4. 试验-不良事件关系 for ae in results_data.get(adverse_events, []): if ae[term]: triples.append((nct_id, hasAdverseEvent, ae[term])) return triples def extract_interventions(title: str) - list: # 简单规则匹配常见免疫检查点抑制剂 patterns [rPembrolizumab, rNivolumab, rAtezolizumab, rDurvalumab] drugs [] for pattern in patterns: matches re.findall(pattern, title, re.I) drugs.extend(matches) return list(set(drugs)) # 去重4. 构建可查询的医疗知识图谱Neo4j 导入与 Cypher 查询实战将爬取的三元组导入 Neo4j 是知识图谱落地的关键一步。本节提供零配置启动 Neo4j、批量导入 CSV 及验证查询的完整流程避免初学者陷入 Java 环境配置陷阱。4.1 用 Docker 快速启动 Neo4j无需安装 Java# 拉取官方镜像推荐 5.20.0 版本兼容最新驱动 docker run -d \ --name clinical-kg \ -p 7474:7474 -p 7687:7687 \ -v $PWD/data:/data \ -e NEO4J_AUTHneo4j/password123 \ -e NEO4J_dbms_memory_heap_max__size2g \ neo4j:5.20.0提示首次启动后访问http://localhost:7474用用户名neo4j和密码password123登录。在Settings Database Management中确认clinical-kg数据库处于ONLINE状态。4.2 将三元组导出为 CSV 并批量导入Scrapy Pipeline 负责将triples字段写入 CSV 文件# pipelines.py import csv import os class Neo4jCsvPipeline: def open_spider(self, spider): self.files {} # 定义关系类型对应的 CSV 文件 self.relation_files { hasIntervention: intervention.csv, targetsDisease: disease.csv, reportsOutcome: outcome.csv, hasAdverseEvent: ae.csv, } for rel_type, filename in self.relation_files.items(): self.files[rel_type] open(fdata/{filename}, w, newline, encodingutf-8) writer csv.writer(self.files[rel_type]) writer.writerow([:START_ID, :END_ID, :TYPE]) # Neo4j 导入必需头 def close_spider(self, spider): for f in self.files.values(): f.close() def process_item(self, item, spider): for triple in item[triples]: subj, pred, obj triple if pred in self.relation_files: writer csv.writer(self.files[pred]) # 使用 NCT ID 作为节点 ID无需额外创建节点 CSV writer.writerow([subj, obj, pred]) return item生成 CSV 后用 Neo4j 的LOAD CSV命令导入在 Browser 中执行// 导入干预关系 LOAD CSV WITH HEADERS FROM file:///intervention.csv AS row CREATE (:Trial {nct_id: row.:START_ID})-[:hasIntervention]-(:Drug {name: row.:END_ID}); // 导入疾病关系自动创建 Disease 节点 LOAD CSV WITH HEADERS FROM file:///disease.csv AS row CREATE (:Trial {nct_id: row.:START_ID})-[:targetsDisease]-(:Disease {name: row.:END_ID});4.3 验证图谱质量5 个必查 Cypher 查询导入后运行以下查询验证数据完整性与关系连通性查询目的Cypher 语句预期结果检查节点总数MATCH (n) RETURN count(n)应 ≥ 1000含 Trial、Drug、Disease 等节点查找某试验的所有干预措施MATCH (t:Trial)-[r:hasIntervention]-(d:Drug) WHERE t.nct_id NCT04567890 RETURN d.name返回[Pembrolizumab]发现“NSCLC”相关试验的干预措施共现MATCH (d:Disease)-[:targetsDisease]-(t:Trial)-[:hasIntervention]-(drug:Drug) WHERE d.name NSCLC RETURN drug.name, count(*) as freq ORDER BY freq DESC LIMIT 5显示 Top 5 药物及频次识别高风险不良事件频次 10%MATCH (t:Trial)-[r:hasAdverseEvent]-(ae) WHERE r.frequency CONTAINS % AND toFloat(replace(r.frequency, %, )) 10 RETURN ae.term, count(*)返回如Neutropenia, 12检查关系是否双向可追溯MATCH path(t:Trial)-[r]-(x) WHERE t.nct_id NCT04567890 RETURN length(path), labels(x), r应返回多条路径证明图结构完整5. 避开 ClinicalTrials.gov 的 IP 限流并发控制与请求间隔的硬核调优即使使用 Playwright高频请求仍会触发 ClinicalTrials.gov 的速率限制典型表现返回 HTTP 429 或空白页面。本节给出经实测有效的并发参数组合与动态退避策略。5.1 Scrapy 设置中的关键并发参数在settings.py中以下参数需协同调整# settings.py # 并发请求数Playwright 每个实例占用约 300MB 内存 CONCURRENT_REQUESTS 2 # Playwright 模式下严禁 3 CONCURRENT_REQUESTS_PER_DOMAIN 1 # 每域名并发数强制串行化 DOWNLOAD_DELAY 3 # 固定延迟秒基础防护 RANDOMIZE_DOWNLOAD_DELAY False # 关闭随机化便于调试 # Playwright 特定限流比全局 DOWNLOAD_DELAY 更精准 PLAYWRIGHT_MAX_PAGES_PER_CONTEXT 1 # 每个浏览器上下文只打开 1 个页面 PLAYWRIGHT_CONTEXTS 2 # 同时运行 2 个浏览器上下文即 2 个 Chromium 实例5.2 动态退避中间件根据响应状态码自动延长延迟当检测到 429 响应时暂停当前 spider 并延长后续请求间隔# middlewares.py from scrapy.downloadermiddlewares.retry import RetryMiddleware from scrapy.utils.response import response_status_message class AdaptiveRetryMiddleware(RetryMiddleware): def __init__(self, settings): super().__init__(settings) self.base_delay float(settings.get(DOWNLOAD_DELAY, 3)) self.max_delay 30.0 def process_response(self, request, response, spider): if response.status 429: # 计算指数退避时间2^retry_times * base_delay retry_times request.meta.get(retry_times, 0) delay min(self.base_delay * (2 ** retry_times), self.max_delay) spider.logger.warning( f429 detected for {request.url}, retrying in {delay:.1f}s ) # 修改请求元数据触发重试 request.meta[download_delay] delay return self._retry(request, response, spider) or response return response启用该中间件# settings.py DOWNLOADER_MIDDLEWARES { myproject.middlewares.AdaptiveRetryMiddleware: 543, }5.3 实测有效参数组合表基于 24 小时连续运行环境配置平均吞吐量试验/小时429 错误率内存占用GB适用场景CONCURRENT_REQUESTS1,DOWNLOAD_DELAY5120 0.1%1.2单机轻量采集学生作业CONCURRENT_REQUESTS2,DOWNLOAD_DELAY3,PLAYWRIGHT_CONTEXTS23801.2%2.8中小型团队日更数据CONCURRENT_REQUESTS2,DOWNLOAD_DELAY3,PLAYWRIGHT_CONTEXTS2, AdaptiveRetry3200.3%2.8生产环境稳定运行推荐CONCURRENT_REQUESTS3不推荐4108.7%4.1频繁中断需人工介入注意CONCURRENT_REQUESTS3在测试中导致 Chromium 实例崩溃率上升至 15%故不列入正式配置。真实项目中宁可降低吞吐量也要保证429 错误率 1%——因为每次 429 都会重置服务器端的请求计数器反而延长总耗时。最后验证你的爬虫是否真正“隐身”在 Playwright 启动参数中加入--hide-scrollbars和--disable-logging并在page.evaluate()中执行navigator.webdriver false可进一步降低被识别为自动化脚本的概率。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门