完整教程:避坑:如果对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的反爬不是为了刁难你,而是保护服务器不被打垮。理解规则、遵守规则,你的脚本才能活得更久。

代码已开源:https://github.com/your-repo/pubmed-smart-crawler

posted @ 2025-12-07 17:02  yangykaifa  阅读(1)  评论(0)    收藏  举报