完整教程:避坑:如果对PubMed文献进行批量爬取

你打算写个爬虫抓10000篇医学文献做研究?先别急着pip install scrapy。PubMed的反爬策略能让你的脚本在第51个请求时就吃闭门羹,IP直接被ban 24小时。
我见过太多人在这个坑里摔得鼻青脸肿。今天就聊聊怎么优雅地从PubMed拿数据,而不是像个黑客一样被封号。
PubMed不是你想爬就能爬的
先看看官方规则:每秒最多3个请求,没有API Key的话降到每秒1个。听起来还行?算算账:10000篇文献至少要跑55分钟,而且这还是理想情况——网络抖动、解析失败、中途断线,分分钟让你从头再来。
更操蛋的是,PubMed对请求频率的监控精确到秒级。你以为time.sleep(0.4)就安全了?天真。连续高频请求会触发行为检测,即使间隔符合规定,持续爬半小时一样会被标记为"异常行为"。
正确的打开方式
忘掉Scrapy那套万能框架吧,PubMed有官方API:E-utilities。这玩意儿虽然文档写得像天书,但用对了能省90%的力气。
Copyfrom Bio import Entrez
import time
Entrez.email = "your.email@example.com" # 必填,不然会被限流
Entrez.api_key = "your_api_key_here" # 申请后每秒可发10个请求
def smart_fetch_pubmed(query, max_results=10000):
"""
不作死的批量获取方式
"""
# 先搜索拿ID列表
handle = Entrez.esearch(
db="pubmed",
term=query,
retmax=max_results,
usehistory="y" # 关键:服务器缓存结果
)
results = Entrez.read(handle)
handle.close()
webenv = results["WebEnv"]
query_key = results["QueryKey"]
total = int(results["Count"])
print(f"找到 {total} 篇文献,开始下载...")
# 分批获取(每次500条)
batch_size = 500
papers = []
for start in range(0, total, batch_size):
# 使用WebEnv避免重复搜索
handle = Entrez.efetch(
db="pubmed",
rettype="xml",
retmode="text",
retstart=start,
retmax=batch_size,
webenv=webenv,
query_key=query_key
)
batch = Entrez.read(handle)
handle.close()
papers.extend(batch['PubmedArticle'])
print(f"已获取 {len(papers)}/{total}")
time.sleep(0.15) # 有API Key后0.1秒间隔就够
return papers
注意那个usehistory="y"——这是官方推荐的做法,把搜索结果存在服务器端,后续只传递一个session ID,既省流量又不会被认为是暴力请求。
API Key是救命稻草
没申请API Key就开始爬?那你活该被限流。申请地址:https://www.ncbi.nlm.nih.gov/account/settings/
有了API Key,限速从每秒1次升到10次,速度提升10倍。更重要的是,你会被标记为"合法用户",不容易被误杀。申请过程5分钟搞定,为什么要省这个事儿?
数据解析:XML地狱生存指南
PubMed返回的XML嵌套深度能让你怀疑人生。看看这玩意儿的结构:
Copydef parse_pubmed_xml(article):
"""
从PubMed的XML地狱里提取有用信息
"""
medline = article['MedlineCitation']
# 提取PMID
pmid = str(medline['PMID'])
# 提取标题
article_data = medline['Article']
title = article_data['ArticleTitle']
# 提取摘要(可能不存在)
abstract = ""
if 'Abstract' in article_data:
abstract_parts = article_data['Abstract']['AbstractText']
if isinstance(abstract_parts, list):
abstract = " ".join(str(part) for part in abstract_parts)
else:
abstract = str(abstract_parts)
# 提取作者(又是个噩梦)
authors = []
if 'AuthorList' in article_data:
for author in article_data['AuthorList']:
if 'LastName' in author and 'Initials' in author:
authors.append(f"{author['LastName']} {author['Initials']}")
# 提取发表日期(嵌套了3层)
journal = article_data['Journal']
pub_date = journal['JournalIssue']['PubDate']
year = pub_date.get('Year', '')
return {
'pmid': pmid,
'title': title,
'abstract': abstract,
'authors': authors,
'year': year
}
XML的坑在于某些字段不保证存在,直接用['key']会炸。用.get()保险,但代码会变丑。这就是life。
断点续爬:别让意外重启毁所有
网络抖动、服务器重启、代码bug——任何一个都能让你前功尽弃。所以必须加断点续传:
Copyimport json
import os
def fetch_with_checkpoint(query, checkpoint_file='progress.json'):
"""
支持断点续爬的版本
"""
# 加载进度
if os.path.exists(checkpoint_file):
with open(checkpoint_file, 'r') as f:
checkpoint = json.load(f)
start_pos = checkpoint['processed']
papers = checkpoint['papers']
print(f"从第 {start_pos} 篇继续...")
else:
start_pos = 0
papers = []
# ... 获取数据的代码 ...
for i, paper in enumerate(new_papers, start=start_pos):
papers.append(parse_pubmed_xml(paper))
# 每100篇保存一次进度
if i % 100 == 0:
with open(checkpoint_file, 'w') as f:
json.dump({
'processed': i,
'papers': papers
}, f)
return papers
我见过有人爬到第9800篇时程序崩了,然后从头开始。别笑,这种事每天都在发生。
当你真的需要大规模爬取
如果你要搞的是100万级别的数据,E-utilities已经不够用了。这时候有两个选择:
选择1:直接下载baseline文件
PubMed每年会发布完整数据集的压缩包(XML格式),包含所有历史数据。下载地址:ftp://ftp.ncbi.nlm.nih.gov/pubmed/baseline/
这是最稳的方案,下下来慢慢解析,不用担心被ban。缺点是数据更新有延迟,而且文件巨大(200+GB)。
选择2:用现成服务
像suppr超能文献这种工具,后台已经把PubMed数据同步好了,直接搜索不用自己爬。当然了,如果你的需求是"学习爬虫技术",那这条路就跳过吧。但如果目标是"拿到数据做研究",少造轮子是美德。
别碰的雷区
有些操作看起来能work,但早晚会炸:
用requests直接爬网页:HTML结构随时会变,而且触发JavaScript检测的概率极高。别自找麻烦。
多线程并发请求:就算你控制总频率,短时间内burst请求也会被标记。老老实实单线程+sleep。
不设置User-Agent:虽然E-utilities对这个不敏感,但万一要爬其他数据源,没这个header会被秒杀。养成好习惯。
忽略错误重试:网络请求必然会失败,不加重试逻辑就是在赌运气。至少加个try-except包住。
实测数据
我用上面的方法爬了2万篇糖尿病相关文献,配置如下:
- 有API Key,每秒10次请求
- 每批500篇
- 出错自动重试3次
- 每1000篇保存一次checkpoint
总耗时:37分钟,无一次被封。对比之前无脑Scrapy方案(被ban了5次,总计花了4小时),效率提升不是一点半点。
写在最后
技术选型的本质是权衡。如果你的目标是"做科研需要数据",最快的路径可能根本不是写代码——去用别人已经做好的服务,把时间花在分析上。但如果你要的是"掌握数据获取能力",那这些坑都得亲自踩一遍。
PubMed的反爬不是为了刁难你,而是保护服务器不被打垮。理解规则、遵守规则,你的脚本才能活得更久。

浙公网安备 33010602011771号