跳转至

Web 爬虫与自动化数据获取

一句话说明:用 Python 自动从网页和 API 抓取论文、基因信息、生信资源等数据,省掉手动复制粘贴的重复劳动。


为什么生信人要学爬虫

场景手动做爬虫做
下载 100 篇 PubMed 论文摘要逐个搜索、复制、粘贴,2 小时脚本 30 秒跑完
批量获取 NCBI 基因注释一个一个查,容易漏循环请求,结果自动存表
追踪 GitHub 生信工具更新每天手动刷页面定时脚本 + 邮件通知
收集数据库样本元数据手动翻页、截图自动翻页、结构化存储

白话总结:爬虫就是让电脑模拟你打开浏览器、找到数据、复制回来这个过程。你教它一次,它能重复跑一万次。


核心概念白话版

1. HTTP 请求:浏览器和服务器的对话

你(浏览器)→ "我要看这个页面" → 服务器
服务器 → "给你,页面内容" → 你(浏览器)
  • GET 请求:只是"看"数据(比如打开网页)
  • POST 请求:要"提交"数据(比如登录、搜索)
  • 状态码:200 = 成功,404 = 页面不存在,403 = 被拒绝,500 = 服务器出错

2. HTML 结构:网页的骨架

HTML 就像一棵树,每个标签是一个节点:

<html>
  <body>
    <div class="article">
      <h1>论文标题</h1>        <!-- h1 标签 = 大标题 -->
      <p class="abstract">摘要内容</p>  <!-- p 标签 = 段落 -->
      <a href="https://...">链接</a>    <!-- a 标签 = 链接 -->
    </div>
  </body>
</html>

白话:HTML 就是把网页内容用标签包起来。爬虫的工作就是找到你要的那个标签,把里面的文字掏出来。

3. CSS 选择器:精准定位元素

选择器含义示例
div标签名所有 div
.abstractclass 名class="abstract" 的元素
#titleid 名id="title" 的元素
div.article p嵌套article 类 div 下的所有 p
a[href]属性有 href 属性的 a 标签

4. XPath:另一种定位方式

XPath 用路径表达式定位,像文件路径一样:

//div[@class="abstract"]/p      # 找 class=abstract 的 div 下的 p
//a/@href                        # 找所有 a 标签的 href 属性值
//h1/text()                      # 找 h1 标签的文本内容

白话:CSS 选择器和 XPath 都是"地址",告诉程序去网页的哪个位置取数据。CSS 选择器更简洁,XPath 更灵活。初学者用 CSS 选择器就够了。

5. 反爬机制:网站的防御

网站不希望被大量自动请求,常见防御手段:

  • 频率限制:短时间请求太多直接封 IP
  • 验证码:弹出图片验证,机器难以通过
  • User-Agent 检测:检查是不是浏览器发的请求
  • 登录墙:必须登录才能看内容
  • 动态渲染:数据由 JavaScript 生成,直接请求拿不到

6. robots.txt:网站的"告示牌"

每个网站根目录下的 robots.txt 文件声明了哪些路径允许爬、哪些不允许:

# https://pubmed.ncbi.nlm.nih.gov/robots.txt
User-agent: *
Disallow: /account/       # 不允许爬账户页面
Allow: /                  # 其他页面允许
Crawl-delay: 1            # 每次请求间隔 1 秒

白话:robots.txt 是网站的"规矩",爬之前先看一眼,遵守别人的规则。

7. API vs 爬虫

对比API爬虫
数据格式结构化 JSON需要从 HTML 解析
稳定性高,有文档低,页面改版就挂
速度慢(要解析 HTML)
合规性官方提供,合规灰色地带,需谨慎
限制有频率限制和 API KeyIP 可能被封

原则:能用 API 就用 API,API 拿不到的再考虑爬虫。


requests 库教程

安装

pip install requests          # 安装 requests 库
pip install beautifulsoup4    # 安装 BeautifulSoup(后面要用)
pip install lxml              # 安装更快的 HTML 解析器

GET 请求:获取网页

import requests  # 导入 requests 库

# === 最基本的 GET 请求 ===
url = "https://httpbin.org/get"  # 测试网址(会返回你的请求信息)
response = requests.get(url)     # 发送 GET 请求

print(response.status_code)  # 打印状态码(200 表示成功)
print(response.text)         # 打印返回的文本内容
print(response.json())       # 如果返回的是 JSON,直接解析为字典

POST 请求:提交数据

import requests  # 导入 requests 库

# === POST 请求(模拟表单提交) ===
url = "https://httpbin.org/post"  # 测试网址
data = {                           # 要提交的数据(字典格式)
    "query": "metagenomics",       # 搜索关键词
    "page": 1                      # 页码
}
response = requests.post(url, data=data)  # 发送 POST 请求
print(response.json())                     # 打印返回结果

设置请求头 Headers

import requests  # 导入 requests 库

# === 自定义 Headers(伪装成浏览器) ===
headers = {
    # User-Agent 告诉服务器你是什么浏览器,不设置的话默认是 python-requests
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/120.0.0.0 Safari/537.36",
    # Accept 告诉服务器你能接受什么格式的响应
    "Accept": "text/html,application/json",
    # Accept-Language 告诉服务器你偏好的语言
    "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8"
}

url = "https://httpbin.org/headers"      # 测试网址
response = requests.get(url, headers=headers)  # 带 headers 发请求
print(response.json())                          # 查看服务器收到的 headers

Session:保持会话状态

import requests  # 导入 requests 库

# === Session 会话(自动保持 Cookie) ===
# 白话:Session 就像你登录了浏览器,后续请求自动带着登录状态
session = requests.Session()  # 创建一个会话对象

# 设置全局 headers,这个 session 发出的所有请求都会带上
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/120.0.0.0 Safari/537.36"
})

# 第一次请求(比如登录页面),Cookie 会自动保存
response1 = session.get("https://httpbin.org/cookies/set/token/abc123")
# 第二次请求会自动带上之前存的 Cookie
response2 = session.get("https://httpbin.org/cookies")
print(response2.json())  # 可以看到 Cookie 被自动携带了

代理设置

import requests  # 导入 requests 库

# === 设置代理(当 IP 被封时用) ===
proxies = {
    "http": "http://代理IP:端口",    # HTTP 代理地址
    "https": "http://代理IP:端口"    # HTTPS 代理地址
}

# 注意:这里用的是示例地址,实际使用要替换成真实代理
# response = requests.get("https://example.com", proxies=proxies, timeout=10)

# === 超时设置(防止程序卡死) ===
try:
    response = requests.get(
        "https://httpbin.org/delay/2",  # 这个网址会延迟 2 秒响应
        timeout=5                        # 最多等 5 秒,超时就报错
    )
    print(response.status_code)  # 打印状态码
except requests.exceptions.Timeout:  # 捕获超时异常
    print("请求超时,稍后重试")
except requests.exceptions.ConnectionError:  # 捕获连接错误
    print("连接失败,检查网络或 URL")

BeautifulSoup 教程

解析 HTML

from bs4 import BeautifulSoup  # 从 bs4 库导入 BeautifulSoup

# === 示例 HTML(模拟一个论文列表页面) ===
html_doc = """
<html>
<body>
  <div class="results">
    <div class="article" id="art1">
      <h2 class="title">Gut microbiome in T2D patients</h2>
      <span class="authors">Zhang et al.</span>
      <p class="abstract">We analyzed gut microbiome composition...</p>
      <a href="https://pubmed.ncbi.nlm.nih.gov/12345678/">Full text</a>
    </div>
    <div class="article" id="art2">
      <h2 class="title">Metagenomic analysis of diabetes</h2>
      <span class="authors">Li et al.</span>
      <p class="abstract">Shotgun metagenomic sequencing revealed...</p>
      <a href="https://pubmed.ncbi.nlm.nih.gov/87654321/">Full text</a>
    </div>
  </div>
</body>
</html>
"""

# 创建 BeautifulSoup 对象,'lxml' 是解析器(比默认的快)
soup = BeautifulSoup(html_doc, "lxml")

查找元素

# === find():找第一个匹配的元素 ===
first_title = soup.find("h2", class_="title")  # 找第一个 class=title 的 h2
print(first_title.text)  # 输出: Gut microbiome in T2D patients

# === find_all():找所有匹配的元素(返回列表) ===
all_titles = soup.find_all("h2", class_="title")  # 找所有 class=title 的 h2
for title in all_titles:        # 遍历每个标题
    print(title.text)           # 打印标题文本

# === select():用 CSS 选择器查找 ===
abstracts = soup.select("div.article p.abstract")  # div.article 下的 p.abstract
for ab in abstracts:            # 遍历每个摘要
    print(ab.text)              # 打印摘要内容

# === 获取属性值 ===
links = soup.find_all("a")     # 找所有 a 标签
for link in links:              # 遍历
    href = link.get("href")    # 获取 href 属性(链接地址)
    text = link.text           # 获取链接文本
    print(f"{text} -> {href}")  # 打印:文本 -> 链接

提取数据(结构化)

from bs4 import BeautifulSoup  # 导入 BeautifulSoup

# 假设 soup 已经创建好了(见上面的代码)
articles = []  # 用列表存所有论文信息

# 找到所有 class=article 的 div(每个 div 是一篇论文)
for div in soup.find_all("div", class_="article"):
    article = {  # 每篇论文存为一个字典
        "id": div.get("id"),                           # 论文 ID(从 id 属性取)
        "title": div.find("h2").text.strip(),          # 标题(h2 标签的文本)
        "authors": div.find("span", class_="authors").text.strip(),  # 作者
        "abstract": div.find("p", class_="abstract").text.strip(),   # 摘要
        "link": div.find("a").get("href")              # 链接(a 标签的 href)
    }
    articles.append(article)  # 加入列表

# 打印结果
for art in articles:
    print(f"标题: {art['title']}")
    print(f"作者: {art['authors']}")
    print(f"摘要: {art['abstract'][:50]}...")  # 摘要只显示前 50 字
    print(f"链接: {art['link']}")
    print("---")

实战案例

案例 1:爬取 PubMed 论文摘要

import requests                     # HTTP 请求库
from bs4 import BeautifulSoup       # HTML 解析库
import time                         # 时间库(用于请求间隔)
import csv                          # CSV 文件操作库

def search_pubmed(query, max_results=10):
    """
    搜索 PubMed 并返回论文摘要列表

    参数:
        query: 搜索关键词(如 "metagenomics T2D")
        max_results: 最多返回多少条(默认 10)
    返回:
        论文信息列表(字典列表)
    """
    # --- 第一步:通过 NCBI E-utilities API 搜索,获取 PMID 列表 ---
    # E-utilities 是 NCBI 官方提供的 API,比直接爬网页更稳定合规
    search_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
    search_params = {
        "db": "pubmed",          # 搜索数据库:PubMed
        "term": query,           # 搜索关键词
        "retmax": max_results,   # 最多返回条数
        "retmode": "json",       # 返回 JSON 格式
        "sort": "relevance"      # 按相关性排序
    }

    print(f"正在搜索 PubMed: {query}")
    response = requests.get(search_url, params=search_params, timeout=15)
    response.raise_for_status()  # 如果状态码不是 200,抛出异常

    data = response.json()  # 解析 JSON 响应
    pmid_list = data["esearchresult"]["idlist"]  # 提取 PMID 列表
    print(f"找到 {len(pmid_list)} 篇论文")

    if not pmid_list:  # 如果没找到论文
        return []

    # --- 第二步:用 PMID 批量获取论文详细信息 ---
    fetch_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
    fetch_params = {
        "db": "pubmed",                  # 数据库
        "id": ",".join(pmid_list),       # 用逗号拼接所有 PMID
        "rettype": "abstract",           # 返回摘要
        "retmode": "xml"                 # 返回 XML 格式(方便解析)
    }

    time.sleep(0.5)  # 间隔 0.5 秒,遵守 NCBI 频率限制(每秒最多 3 次)
    response = requests.get(fetch_url, params=fetch_params, timeout=30)
    response.raise_for_status()

    # --- 第三步:解析 XML 结果 ---
    soup = BeautifulSoup(response.text, "lxml-xml")  # 用 lxml-xml 解析 XML
    articles = []  # 存放结果的列表

    for article in soup.find_all("PubmedArticle"):  # 遍历每篇论文
        # 提取 PMID
        pmid = article.find("PMID").text if article.find("PMID") else "N/A"

        # 提取标题
        title_tag = article.find("ArticleTitle")
        title = title_tag.text.strip() if title_tag else "无标题"

        # 提取摘要(可能有多个段落,拼接起来)
        abstract_parts = article.find_all("AbstractText")
        abstract = " ".join(part.text for part in abstract_parts) if abstract_parts else "无摘要"

        # 提取作者列表
        authors = []
        for author in article.find_all("Author"):
            last = author.find("LastName")    # 姓
            first = author.find("ForeName")   # 名
            if last and first:
                authors.append(f"{last.text} {first.text}")
        author_str = ", ".join(authors) if authors else "未知作者"

        # 提取期刊名
        journal_tag = article.find("Journal")
        journal = ""
        if journal_tag:
            journal_title = journal_tag.find("Title")
            journal = journal_title.text if journal_title else "未知期刊"

        # 提取发表年份
        year_tag = article.find("PubDate")
        year = year_tag.find("Year").text if year_tag and year_tag.find("Year") else "未知年份"

        articles.append({
            "pmid": pmid,
            "title": title,
            "authors": author_str,
            "journal": journal,
            "year": year,
            "abstract": abstract
        })

        print(f"  已获取: PMID {pmid} - {title[:50]}...")

    return articles

# === 使用示例 ===
if __name__ == "__main__":
    results = search_pubmed("gut metagenomics type 2 diabetes", max_results=5)

    # 保存到 CSV 文件
    if results:
        with open("pubmed_results.csv", "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=results[0].keys())
            writer.writeheader()       # 写表头
            writer.writerows(results)  # 写数据行
        print(f"\n结果已保存到 pubmed_results.csv,共 {len(results)} 篇")

案例 2:爬取 NCBI 基因信息

import requests  # HTTP 请求库
import time      # 延时库
import json      # JSON 处理库

def get_gene_info(gene_symbol, organism="human"):
    """
    通过 NCBI E-utilities API 获取基因信息

    参数:
        gene_symbol: 基因符号(如 "BRCA1", "TP53")
        organism: 物种(默认 human)
    返回:
        基因信息字典
    """
    # --- 第一步:搜索基因,获取 Gene ID ---
    search_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
    search_params = {
        "db": "gene",                                    # 搜索 Gene 数据库
        "term": f"{gene_symbol}[Gene Name] AND {organism}[Organism]",  # 搜索条件
        "retmode": "json"                                # 返回 JSON
    }

    print(f"正在查询基因: {gene_symbol} ({organism})")
    resp = requests.get(search_url, params=search_params, timeout=15)
    resp.raise_for_status()  # 检查请求是否成功

    ids = resp.json()["esearchresult"]["idlist"]  # 提取 Gene ID 列表
    if not ids:  # 如果没找到
        print(f"未找到基因: {gene_symbol}")
        return None

    gene_id = ids[0]  # 取第一个(最相关的)
    print(f"找到 Gene ID: {gene_id}")

    # --- 第二步:获取基因详细摘要 ---
    time.sleep(0.4)  # 间隔 0.4 秒
    summary_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
    summary_params = {
        "db": "gene",         # 数据库
        "id": gene_id,        # Gene ID
        "retmode": "json"     # 返回 JSON
    }

    resp = requests.get(summary_url, params=summary_params, timeout=15)
    resp.raise_for_status()

    result = resp.json()["result"]    # 解析结果
    gene_data = result.get(gene_id, {})  # 取对应 ID 的数据

    # 提取关键信息
    info = {
        "gene_id": gene_id,
        "symbol": gene_data.get("name", gene_symbol),           # 基因符号
        "full_name": gene_data.get("description", "N/A"),       # 基因全名
        "organism": gene_data.get("organism", {}).get("scientificname", organism),  # 物种
        "chromosome": gene_data.get("chromosome", "N/A"),       # 染色体位置
        "map_location": gene_data.get("maplocation", "N/A"),    # 图谱位置
        "gene_type": gene_data.get("genetictype", "N/A"),       # 基因类型
        "summary": gene_data.get("summary", "无摘要")            # 功能摘要
    }

    return info

# === 批量查询示例 ===
if __name__ == "__main__":
    # 生信常关注的基因列表
    genes = ["BRCA1", "TP53", "EGFR", "INS", "TNF"]
    all_results = []  # 存放所有结果

    for gene in genes:
        info = get_gene_info(gene)  # 查询每个基因
        if info:
            all_results.append(info)
            print(f"  {info['symbol']}: {info['full_name']}")
            print(f"  染色体: {info['chromosome']}, 位置: {info['map_location']}")
            print()
        time.sleep(0.5)  # 每次查询间隔 0.5 秒

    # 保存为 JSON 文件
    with open("gene_info.json", "w", encoding="utf-8") as f:
        json.dump(all_results, f, ensure_ascii=False, indent=2)  # 中文不转义,缩进 2 格
    print(f"结果已保存到 gene_info.json,共 {len(all_results)} 个基因")

案例 3:爬取生信工具文档

import requests                  # HTTP 请求库
from bs4 import BeautifulSoup    # HTML 解析库

def scrape_tool_docs(url):
    """
    爬取生信工具的在线文档页面,提取标题和正文内容
    以 BioPython 官方教程页面为例

    参数:
        url: 文档页面的 URL
    返回:
        文档内容字典
    """
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) "
                      "Chrome/120.0.0.0 Safari/537.36"
    }

    print(f"正在获取文档: {url}")
    response = requests.get(url, headers=headers, timeout=20)
    response.raise_for_status()
    response.encoding = response.apparent_encoding  # 自动检测编码,防止中文乱码

    soup = BeautifulSoup(response.text, "lxml")  # 解析 HTML

    # 提取页面标题
    title = soup.find("title")
    page_title = title.text.strip() if title else "无标题"

    # 提取所有标题(h1-h4)和对应段落
    sections = []  # 存放各节内容
    for heading in soup.find_all(["h1", "h2", "h3", "h4"]):
        section = {
            "level": heading.name,             # 标题级别(h1/h2/h3/h4)
            "title": heading.text.strip(),     # 标题文本
            "content": ""                      # 对应内容
        }

        # 收集标题后面的所有段落,直到遇到下一个标题
        content_parts = []
        for sibling in heading.find_next_siblings():  # 遍历后面的兄弟元素
            if sibling.name in ["h1", "h2", "h3", "h4"]:  # 遇到下一个标题就停
                break
            if sibling.name in ["p", "li", "pre", "code"]:  # 只取段落/列表/代码
                content_parts.append(sibling.text.strip())

        section["content"] = "\n".join(content_parts)  # 拼接内容
        sections.append(section)

    # 提取所有代码块
    code_blocks = []
    for code in soup.find_all("pre"):  # pre 标签通常包裹代码块
        code_blocks.append(code.text.strip())

    return {
        "title": page_title,
        "url": url,
        "sections": sections,
        "code_blocks": code_blocks
    }

# === 使用示例 ===
if __name__ == "__main__":
    # 爬取 Biopython 教程页面(选一个实际存在的文档页面)
    doc = scrape_tool_docs("https://biopython.org/wiki/SeqIO")

    print(f"\n文档标题: {doc['title']}")
    print(f"共 {len(doc['sections'])} 个章节")
    print(f"共 {len(doc['code_blocks'])} 个代码块")

    # 打印前 3 个章节
    for sec in doc["sections"][:3]:
        print(f"\n[{sec['level']}] {sec['title']}")
        if sec["content"]:
            print(f"  {sec['content'][:100]}...")  # 只显示前 100 字

案例 4:爬取 GitHub 项目信息

import requests  # HTTP 请求库
import time      # 延时库
import json      # JSON 处理库

def get_github_repos(query, sort="stars", max_results=10):
    """
    通过 GitHub API 搜索生信相关项目

    参数:
        query: 搜索关键词(如 "metagenomics pipeline")
        sort: 排序方式(stars/forks/updated)
        max_results: 最多返回条数
    返回:
        项目信息列表
    """
    # GitHub 提供官方 REST API,不需要爬网页
    url = "https://api.github.com/search/repositories"
    headers = {
        "Accept": "application/vnd.github.v3+json",  # 指定 API 版本
        # 如果有 GitHub Token 可以提高频率限制(每小时 5000 次 vs 60 次)
        # "Authorization": "token YOUR_GITHUB_TOKEN"
    }
    params = {
        "q": query,              # 搜索关键词
        "sort": sort,            # 排序方式
        "order": "desc",         # 降序
        "per_page": max_results  # 每页结果数
    }

    print(f"正在搜索 GitHub: {query}")
    response = requests.get(url, headers=headers, params=params, timeout=15)
    response.raise_for_status()

    data = response.json()  # 解析 JSON
    repos = []  # 存放结果

    for item in data.get("items", []):  # 遍历搜索结果
        repo = {
            "name": item["full_name"],                  # 仓库全名(用户名/仓库名)
            "description": item.get("description", ""), # 项目描述
            "url": item["html_url"],                    # GitHub 页面链接
            "stars": item["stargazers_count"],           # Star 数
            "forks": item["forks_count"],                # Fork 数
            "language": item.get("language", "N/A"),     # 主要编程语言
            "updated": item["updated_at"][:10],          # 最后更新日期
            "license": item.get("license", {}).get("spdx_id", "N/A") if item.get("license") else "N/A"
        }
        repos.append(repo)
        print(f"  {repo['name']} - Stars: {repo['stars']}, Language: {repo['language']}")

    return repos

# === 使用示例 ===
if __name__ == "__main__":
    # 搜索宏基因组相关工具
    results = get_github_repos("metagenomics pipeline bioinformatics", max_results=10)

    # 保存结果
    with open("github_bioinfo_tools.json", "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    print(f"\n共找到 {len(results)} 个项目,已保存到 github_bioinfo_tools.json")

API 调用教程

API 和爬虫的区别

爬虫是"强行"从网页上扣数据,API 是网站"主动"提供的数据接口。

爬虫流程: 请求网页 → 拿到 HTML → 解析 HTML → 提取数据
API 流程:  请求接口 → 拿到 JSON → 直接用

REST API 基础

REST API 的核心思想:用 URL 定位资源,用 HTTP 方法操作资源。

HTTP 方法用途示例
GET获取数据获取论文信息
POST创建数据提交分析任务
PUT更新数据修改配置
DELETE删除数据删除任务

用 requests 调 API 的通用模板

import requests  # 导入 requests 库
import json      # JSON 处理

def call_api(base_url, endpoint, params=None, headers=None, method="GET"):
    """
    通用 API 调用函数

    参数:
        base_url: API 基础地址(如 "https://api.example.com")
        endpoint: 接口路径(如 "/v1/search")
        params: 查询参数(字典)
        headers: 请求头(字典)
        method: 请求方法(GET/POST)
    返回:
        JSON 响应数据
    """
    url = f"{base_url}{endpoint}"  # 拼接完整 URL

    # 默认请求头
    default_headers = {
        "Accept": "application/json",     # 期望返回 JSON
        "Content-Type": "application/json" # 发送的数据也是 JSON
    }
    if headers:  # 如果有自定义 headers,合并进去
        default_headers.update(headers)

    try:
        if method.upper() == "GET":
            resp = requests.get(url, params=params, headers=default_headers, timeout=15)
        elif method.upper() == "POST":
            resp = requests.post(url, json=params, headers=default_headers, timeout=15)
        else:
            raise ValueError(f"不支持的方法: {method}")

        resp.raise_for_status()  # 检查状态码
        return resp.json()       # 返回解析后的 JSON

    except requests.exceptions.HTTPError as e:
        print(f"HTTP 错误: {e.response.status_code} - {e.response.text[:200]}")
    except requests.exceptions.Timeout:
        print("请求超时")
    except requests.exceptions.ConnectionError:
        print("连接失败")

    return None  # 出错时返回 None

# === 调用 NCBI E-utilities API 示例 ===
if __name__ == "__main__":
    result = call_api(
        base_url="https://eutils.ncbi.nlm.nih.gov",
        endpoint="/entrez/eutils/esearch.fcgi",
        params={
            "db": "pubmed",
            "term": "CRISPR metagenomics",
            "retmode": "json",
            "retmax": 5
        }
    )

    if result:
        ids = result["esearchresult"]["idlist"]
        print(f"找到 PMID: {ids}")

数据存储

保存为 CSV

import csv  # Python 内置 CSV 库

data = [
    {"pmid": "12345", "title": "Paper A", "year": "2024"},
    {"pmid": "67890", "title": "Paper B", "year": "2023"},
]

# --- 写入 CSV ---
# encoding="utf-8-sig" 解决 Excel 打开中文乱码问题(加 BOM 头)
with open("output.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["pmid", "title", "year"])  # 指定列名
    writer.writeheader()       # 写入表头
    writer.writerows(data)     # 写入所有数据行

# --- 读取 CSV ---
with open("output.csv", "r", encoding="utf-8-sig") as f:
    reader = csv.DictReader(f)  # 按字典方式读
    for row in reader:          # 遍历每行
        print(row["title"])     # 按列名取值

保存为 JSON

import json  # Python 内置 JSON 库

data = [
    {"gene": "BRCA1", "function": "DNA repair"},
    {"gene": "TP53", "function": "Tumor suppressor"},
]

# --- 写入 JSON ---
with open("output.json", "w", encoding="utf-8") as f:
    json.dump(
        data,                   # 要写入的数据
        f,                      # 文件对象
        ensure_ascii=False,     # 中文不转义(不然会变成 \u4e2d 这种)
        indent=2                # 缩进 2 格(方便阅读)
    )

# --- 读取 JSON ---
with open("output.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)       # 读取并解析
    for item in loaded:
        print(f"{item['gene']}: {item['function']}")

保存到 SQLite 数据库

import sqlite3  # Python 内置 SQLite 库(不用额外安装)

# --- 创建数据库和表 ---
conn = sqlite3.connect("biodata.db")  # 连接数据库(文件不存在会自动创建)
cursor = conn.cursor()                 # 创建游标

# 创建表(如果不存在)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS papers (
        pmid TEXT PRIMARY KEY,          -- 主键,PMID
        title TEXT NOT NULL,            -- 标题,不能为空
        authors TEXT,                   -- 作者
        abstract TEXT,                  -- 摘要
        year INTEGER                    -- 年份
    )
""")

# --- 插入数据 ---
papers = [
    ("12345", "Gut microbiome study", "Zhang et al.", "We analyzed...", 2024),
    ("67890", "Metagenomics analysis", "Li et al.", "Shotgun...", 2023),
]

# INSERT OR IGNORE:如果 PMID 已存在就跳过,不会报错
cursor.executemany(
    "INSERT OR IGNORE INTO papers VALUES (?, ?, ?, ?, ?)",
    papers
)
conn.commit()  # 提交事务(写入磁盘)

# --- 查询数据 ---
cursor.execute("SELECT pmid, title, year FROM papers WHERE year >= 2023")
for row in cursor.fetchall():  # 获取所有结果
    print(f"PMID: {row[0]}, 标题: {row[1]}, 年份: {row[2]}")

conn.close()  # 关闭数据库连接

反爬应对策略

1. 请求延时

import time    # 时间库
import random  # 随机数库

# 固定延时
time.sleep(1)  # 每次请求后等 1 秒

# 随机延时(更自然,不容易被检测到固定模式)
delay = random.uniform(0.5, 2.0)  # 0.5 到 2 秒之间随机
time.sleep(delay)

2. 随机 User-Agent

import random  # 随机数库

# User-Agent 池(模拟不同浏览器)
UA_LIST = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
]

def get_random_headers():
    """生成随机请求头"""
    return {
        "User-Agent": random.choice(UA_LIST),  # 随机选一个 UA
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive"
    }

3. 代理池(简易版)

import random    # 随机数库
import requests  # HTTP 请求库

# 代理池(实际使用时需要替换成真实可用的代理)
PROXY_POOL = [
    "http://proxy1.example.com:8080",
    "http://proxy2.example.com:8080",
    "http://proxy3.example.com:8080",
]

def request_with_proxy(url, max_retries=3):
    """
    使用代理池发送请求,失败自动重试

    参数:
        url: 目标网址
        max_retries: 最大重试次数
    返回:
        Response 对象或 None
    """
    for attempt in range(max_retries):  # 最多重试 max_retries 次
        proxy = random.choice(PROXY_POOL)  # 随机选一个代理
        proxies = {"http": proxy, "https": proxy}

        try:
            resp = requests.get(
                url,
                proxies=proxies,
                headers=get_random_headers(),  # 随机 UA
                timeout=10
            )
            resp.raise_for_status()
            return resp  # 成功就返回
        except Exception as e:
            print(f"第 {attempt + 1} 次尝试失败 (代理: {proxy}): {e}")
            continue

    print("所有重试均失败")
    return None

法律与道德注意事项

  1. 先查 robots.txt:访问 网站域名/robots.txt,遵守 Disallow 和 Crawl-delay 规则
  2. 优先用官方 API:NCBI E-utilities、GitHub API、UniProt API 都有官方接口
  3. 控制请求频率:NCBI 要求每秒不超过 3 次请求(有 API Key 可以放宽到 10 次)
  4. 不爬个人隐私数据:不爬用户邮箱、手机号、个人信息
  5. 不爬付费/版权内容:不爬需要付费订阅的全文论文
  6. 注明数据来源:在论文或报告中注明数据是从哪里获取的
  7. 仅用于学术研究:不用于商业目的或恶意用途
  8. 遵守网站 ToS:阅读网站的使用条款(Terms of Service)

常见报错与解决

1. ConnectionError:连接失败

requests.exceptions.ConnectionError: Max retries exceeded

原因:网络问题、URL 错误、或 IP 被封

解决

# 检查网络 → 检查 URL 拼写 → 加重试机制
from requests.adapters import HTTPAdapter   # 请求适配器
from urllib3.util.retry import Retry        # 重试策略

session = requests.Session()
retry = Retry(total=3, backoff_factor=1)    # 重试 3 次,间隔递增
adapter = HTTPAdapter(max_retries=retry)    # 创建适配器
session.mount("http://", adapter)           # 挂载到 session
session.mount("https://", adapter)

2. Timeout:请求超时

requests.exceptions.ReadTimeout: Read timed out

原因:服务器响应太慢或网络不好

解决:增大 timeout 值,或加 try-except 重试

response = requests.get(url, timeout=(5, 30))  # 连接超时 5 秒,读取超时 30 秒

3. HTTPError 403:被拒绝

requests.exceptions.HTTPError: 403 Client Error: Forbidden

原因:没有设置 User-Agent,或被反爬机制拦截

解决:添加 User-Agent 请求头,降低请求频率

4. UnicodeDecodeError:编码错误

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe4

原因:网页编码不是 UTF-8

解决

response.encoding = response.apparent_encoding  # 自动检测编码
# 或者手动指定
response.encoding = "gbk"  # 中文网站可能是 GBK 编码

5. AttributeError: NoneType

AttributeError: 'NoneType' object has no attribute 'text'

原因:find() 没找到元素,返回了 None

解决:加判空检查

tag = soup.find("h1")
text = tag.text if tag else "未找到"  # 如果 tag 是 None 就用默认值

6. JSONDecodeError:JSON 解析失败

json.decoder.JSONDecodeError: Expecting value

原因:返回的不是 JSON 格式(可能是 HTML 错误页面)

解决

if response.headers.get("Content-Type", "").startswith("application/json"):
    data = response.json()   # 确认是 JSON 再解析
else:
    print(f"返回的不是 JSON: {response.text[:200]}")  # 打印前 200 字排查


速查表

requests 常用操作

操作代码
GET 请求requests.get(url)
POST 请求requests.post(url, data=dict)
带 Headersrequests.get(url, headers=dict)
带参数requests.get(url, params=dict)
设超时requests.get(url, timeout=10)
设代理requests.get(url, proxies=dict)
获取状态码response.status_code
获取文本response.text
获取 JSONresponse.json()
获取二进制response.content
检查错误response.raise_for_status()

BeautifulSoup 常用操作

操作代码
创建对象soup = BeautifulSoup(html, "lxml")
找第一个soup.find("tag", class_="name")
找所有soup.find_all("tag", class_="name")
CSS 选择器soup.select("div.class > p")
获取文本tag.texttag.get_text()
获取属性tag.get("href")tag["href"]
按 ID 找soup.find(id="my_id")
按属性找soup.find("div", attrs={"data-type": "article"})

NCBI E-utilities API

接口用途端点
ESearch搜索获取 ID 列表/entrez/eutils/esearch.fcgi
EFetch获取完整记录/entrez/eutils/efetch.fcgi
ESummary获取摘要信息/entrez/eutils/esummary.fcgi
EInfo查看数据库信息/entrez/eutils/einfo.fcgi

基础 URL:https://eutils.ncbi.nlm.nih.gov


延伸资源

官方文档

  • requests 文档:https://docs.python-requests.org/
  • BeautifulSoup 文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc/
  • NCBI E-utilities 文档:https://www.ncbi.nlm.nih.gov/books/NBK25497/
  • GitHub REST API 文档:https://docs.github.com/en/rest

进阶学习方向

方向工具适用场景
动态页面Selenium / PlaywrightJavaScript 渲染的页面
大规模爬取Scrapy 框架需要爬取大量页面
异步请求aiohttp / httpx需要高并发
生信专用Biopython EntrezNCBI 数据获取的封装库
数据管道pandas + SQLAlchemy数据清洗和存储

Biopython Entrez 模块(推荐替代方案)

from Bio import Entrez  # Biopython 的 NCBI 接口封装

# 设置邮箱(NCBI 要求提供邮箱,用于联系)
Entrez.email = "your_email@example.com"

# 搜索 PubMed
handle = Entrez.esearch(db="pubmed", term="metagenomics T2D", retmax=5)
record = Entrez.read(handle)   # 解析结果
pmids = record["IdList"]       # 获取 PMID 列表
print(f"PMID 列表: {pmids}")

# 获取论文详情
handle = Entrez.efetch(db="pubmed", id=pmids, rettype="abstract", retmode="xml")
records = Entrez.read(handle)  # 解析 XML 结果

白话总结:如果你主要和 NCBI 打交道,Biopython 的 Entrez 模块比自己写 requests 更方便——它把 API 调用封装好了,你只管调函数就行。


学习路径建议

第 1 步:跑通 requests + BeautifulSoup 基础例子
第 2 步:用 E-utilities API 爬 PubMed / NCBI Gene
第 3 步:学会数据存储(CSV / JSON / SQLite)
第 4 步:了解反爬策略和法律合规
第 5 步(进阶):学 Scrapy 框架或 Selenium 处理动态页面