14 Python 数据采集
15 Python 数据采集
在前面的章节中,我们学习了如何通过数据门户手动下载公共数据。但当你需要从多个网站批量获取数据,或者需要定期更新数据时,手动操作就显得力不从心了。Python 是数据采集领域最流行的编程语言之一,它拥有丰富的第三方库,能够帮助我们高效地从网页和 API 中提取数据。
本章将带你从零开始,学习使用 Python 进行网络数据采集的核心技能。我们会以生态学中的真实场景为例——从 GBIF 获取物种分布记录、从气象 API 获取气候数据——让你在实践中掌握这些工具。
15.1 Python 环境配置
15.1.1 为什么需要独立的 Python 环境?
在实际项目中,不同的任务可能依赖不同版本的 Python 包。例如,你的数据采集脚本需要 requests 2.31,而另一个分析项目需要 requests 2.28。如果所有包都安装在同一个环境中,版本冲突会让你头疼不已。虚拟环境(Virtual Environment)就是为了解决这个问题而设计的——它为每个项目创建一个独立的”沙盒”,互不干扰。
15.1.2 使用 conda 管理环境
如果你在 ?sec-environment-setup 中已经安装了 Miniconda,那么可以直接使用 conda 来创建和管理虚拟环境。
创建课程专用环境:
# 创建名为 "ecology-data" 的环境,指定 Python 3.11
conda create -n ecology-data python=3.11
# 激活环境
conda activate ecology-data安装本章所需的包:
# 网络请求与网页解析
pip install requests beautifulsoup4
# 数据处理与保存
pip install pandas
# (可选)进度条显示,适合批量下载
pip install tqdmconda 是一个通用的包管理器和环境管理器,可以安装 Python 包以及非 Python 的依赖(如 C 库)。pip 是 Python 专用的包管理器,包的数量更多。在 conda 环境中,推荐优先用 conda install 安装包,找不到时再用 pip install。
15.1.3 验证安装
激活环境后,在终端中运行以下命令,确认包已正确安装:
python -c "import requests; import bs4; import pandas; print('所有包安装成功!')"15.2 requests 库:发送 HTTP 请求
15.2.1 什么是 HTTP 请求?
当你在浏览器中输入一个网址并按下回车,浏览器实际上向服务器发送了一个 HTTP 请求(Request),服务器处理后返回一个 HTTP 响应(Response),其中包含了网页的 HTML 内容。Python 的 requests 库让我们可以用代码模拟这个过程。
15.2.2 第一个请求:获取网页内容
import requests
# 向 GBIF 首页发送 GET 请求
url = "https://www.gbif.org"
response = requests.get(url)
# 查看响应状态码(200 表示成功)
print(f"状态码: {response.status_code}")
# 查看返回内容的前 500 个字符
print(response.text[:500])15.2.3 常见的 HTTP 状态码
在数据采集中,你会经常遇到以下状态码:
| 状态码 | 含义 | 应对方式 |
|---|---|---|
| 200 | 请求成功 | 正常处理返回数据 |
| 403 | 禁止访问 | 检查是否需要认证或被反爬机制拦截 |
| 404 | 页面不存在 | 检查 URL 是否正确 |
| 429 | 请求过于频繁 | 降低请求频率,添加延时 |
| 500 | 服务器内部错误 | 稍后重试 |
15.2.4 添加请求头
有些网站会检查请求的来源。通过设置 User-Agent 请求头,我们可以告诉服务器”我是谁”:
headers = {
"User-Agent": "EcologyResearchBot/1.0 (university-course-project)"
}
response = requests.get(url, headers=headers)在请求头中标明你的身份(如课程项目、研究用途)是一种良好的实践。这不仅是礼貌,也有助于网站管理员在出现问题时联系你。
15.3 BeautifulSoup:解析网页内容
requests 帮我们拿到了网页的原始 HTML 代码,但 HTML 是一堆嵌套的标签,直接阅读非常困难。BeautifulSoup 是一个 HTML/XML 解析库,它能将杂乱的 HTML 转化为结构化的 Python 对象,让我们轻松提取所需信息。
15.3.1 基本用法
假设我们想从一个简单的 HTML 页面中提取物种名称:
from bs4 import BeautifulSoup
# 模拟一段包含物种信息的 HTML
html_content = """
<html>
<body>
<h1>华南地区常见鸟类</h1>
<ul class="species-list">
<li>白鹭 (<i>Egretta garzetta</i>)</li>
<li>夜鹭 (<i>Nycticorax nycticorax</i>)</li>
<li>池鹭 (<i>Ardeola bacchus</i>)</li>
</ul>
</body>
</html>
"""
# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_content, "html.parser")
# 提取标题
title = soup.find("h1").text
print(f"标题: {title}")
# 提取所有物种的拉丁名
latin_names = soup.find_all("i")
for name in latin_names:
print(f"拉丁名: {name.text}")输出:
标题: 华南地区常见鸟类
拉丁名: Egretta garzetta
拉丁名: Nycticorax nycticorax
拉丁名: Ardeola bacchus
15.3.2 常用的查找方法
| 方法 | 功能 | 示例 |
|---|---|---|
find("tag") |
查找第一个匹配的标签 | soup.find("h1") |
find_all("tag") |
查找所有匹配的标签 | soup.find_all("li") |
find(class_="name") |
按 CSS 类名查找 | soup.find(class_="species-list") |
find(id="name") |
按 ID 查找 | soup.find(id="header") |
.text |
获取标签内的文本 | tag.text |
.get("attr") |
获取标签的属性值 | tag.get("href") |
15.3.3 实战:从网页表格提取数据
生态学文献中经常有数据表格发布在网页上。以下示例展示如何从 HTML 表格中提取数据:
import requests
from bs4 import BeautifulSoup
import pandas as pd
# 假设我们要解析一个包含物种观测记录的网页表格
html_table = """
<table>
<tr><th>物种</th><th>观测地点</th><th>数量</th></tr>
<tr><td>白鹭</td><td>南宁青秀山</td><td>23</td></tr>
<tr><td>夜鹭</td><td>南宁南湖</td><td>15</td></tr>
<tr><td>池鹭</td><td>南宁相思湖</td><td>8</td></tr>
</table>
"""
soup = BeautifulSoup(html_table, "html.parser")
# 提取表头
headers = [th.text for th in soup.find_all("th")]
# 提取每一行数据
rows = []
for tr in soup.find_all("tr")[1:]: # 跳过表头行
row = [td.text for td in tr.find_all("td")]
rows.append(row)
# 转换为 pandas DataFrame
df = pd.DataFrame(rows, columns=headers)
print(df)输出:
物种 观测地点 数量
0 白鹭 南宁青秀山 23
1 夜鹭 南宁南湖 15
2 池鹭 南宁相思湖 8
15.4 API 数据获取
15.4.1 什么是 API?
API(Application Programming Interface,应用程序编程接口)是服务器提供的一种标准化数据访问方式。与网页爬虫不同,API 直接返回结构化的数据(通常是 JSON 格式),无需解析 HTML。大多数生态学公共数据库都提供了 API 接口,这是获取数据最推荐的方式。
API 相比网页爬虫的优势:
- 返回结构化数据,无需解析 HTML
- 更稳定,不会因网页改版而失效
- 通常有明确的使用条款和速率限制
- 数据质量更有保障
15.4.2 实践一:从 GBIF API 获取物种分布数据
GBIF(全球生物多样性信息网络)提供了功能强大的 RESTful API。以下示例展示如何查询白鹭(Egretta garzetta)在中国的分布记录。
第一步:查询物种的 taxonKey
import requests
# 通过物种名称查询 GBIF 的分类编号
species_url = "https://api.gbif.org/v1/species/match"
params = {"name": "Egretta garzetta"}
response = requests.get(species_url, params=params)
species_data = response.json()
taxon_key = species_data["usageKey"]
print(f"白鹭的 taxonKey: {taxon_key}")
print(f"分类信息: {species_data['scientificName']}")第二步:获取分布记录
# 查询该物种在中国的出现记录
occurrence_url = "https://api.gbif.org/v1/occurrence/search"
params = {
"taxonKey": taxon_key,
"country": "CN", # 中国的 ISO 国家代码
"limit": 20, # 每次返回 20 条记录
"hasCoordinate": True # 只返回有经纬度坐标的记录
}
response = requests.get(occurrence_url, params=params)
data = response.json()
print(f"共找到 {data['count']} 条记录")
print(f"本次返回 {len(data['results'])} 条")
# 查看第一条记录的关键字段
record = data["results"][0]
print(f"观测地点: ({record['decimalLatitude']}, {record['decimalLongitude']})")
print(f"观测日期: {record.get('eventDate', '未记录')}")
print(f"数据来源: {record.get('datasetName', '未知')}")第三步:批量获取并整理为 DataFrame
import pandas as pd
# 提取关键字段
records = []
for r in data["results"]:
records.append({
"物种": r.get("species", ""),
"纬度": r.get("decimalLatitude", None),
"经度": r.get("decimalLongitude", None),
"观测日期": r.get("eventDate", ""),
"国家": r.get("country", ""),
"数据集": r.get("datasetName", "")
})
df = pd.DataFrame(records)
print(df.head())15.4.3 实践二:从 Open-Meteo API 获取气候数据
气候数据是生态学研究的重要背景变量。Open-Meteo 是一个免费的气象 API,无需注册即可使用,非常适合教学和科研。
以下示例获取南宁市(22.82°N, 108.32°E)2024 年的月均温和月降水量:
import requests
import pandas as pd
# Open-Meteo 历史天气 API
url = "https://archive-api.open-meteo.com/v1/archive"
params = {
"latitude": 22.82,
"longitude": 108.32,
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"daily": "temperature_2m_mean,precipitation_sum",
"timezone": "Asia/Shanghai"
}
response = requests.get(url, params=params)
weather_data = response.json()
# 将日数据转换为 DataFrame
df = pd.DataFrame({
"日期": weather_data["daily"]["time"],
"日均温_C": weather_data["daily"]["temperature_2m_mean"],
"日降水量_mm": weather_data["daily"]["precipitation_sum"]
})
# 转换日期格式并计算月均值
df["日期"] = pd.to_datetime(df["日期"])
df["月份"] = df["日期"].dt.month
monthly = df.groupby("月份").agg(
月均温=("日均温_C", "mean"),
月降水量=("日降水量_mm", "sum")
).round(1)
print(monthly)许多 API 需要注册账号并获取 API Key(密钥)才能使用。API Key 是你的身份凭证,不要将其公开分享或上传到 GitHub。Open-Meteo 和 GBIF 的基础查询不需要 API Key,因此非常适合课堂练习。
15.4.4 处理分页:获取大量数据
大多数 API 会限制单次返回的数据量(如 GBIF 每次最多返回 300 条)。要获取全部数据,需要使用分页(Pagination)机制:
import time
all_records = []
offset = 0
limit = 300
while True:
params = {
"taxonKey": taxon_key,
"country": "CN",
"limit": limit,
"offset": offset,
"hasCoordinate": True
}
response = requests.get(occurrence_url, params=params)
data = response.json()
results = data.get("results", [])
if not results:
break # 没有更多数据了
all_records.extend(results)
offset += limit
print(f"已获取 {len(all_records)} / {data['count']} 条记录")
# 礼貌延时:每次请求间隔 1 秒,避免给服务器造成压力
time.sleep(1)
# 安全限制:最多获取 1000 条(课堂练习用)
if len(all_records) >= 1000:
break
print(f"共获取 {len(all_records)} 条记录")15.5 数据保存:CSV 与 JSON
采集到的数据需要保存到本地文件,以便后续分析。最常用的两种格式是 CSV 和 JSON。
15.5.1 保存为 CSV
CSV(Comma-Separated Values)是表格数据最通用的格式,可以直接用 Excel 或 R 打开:
# 将 DataFrame 保存为 CSV
df.to_csv("gbif_egretta_garzetta_cn.csv", index=False, encoding="utf-8-sig")
print("数据已保存为 CSV 文件")保存包含中文的 CSV 文件时,建议使用 encoding="utf-8-sig"。这样用 Excel 打开时不会出现乱码。如果只在 Python 或 R 中使用,utf-8 即可。
15.5.2 保存为 JSON
JSON(JavaScript Object Notation)适合保存嵌套结构的数据,如 API 返回的原始响应:
import json
# 保存原始 API 响应
with open("gbif_raw_response.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print("原始数据已保存为 JSON 文件")15.5.3 CSV 与 JSON 的选择
| 特性 | CSV | JSON |
|---|---|---|
| 适合的数据结构 | 扁平的表格数据 | 嵌套的层级数据 |
| 文件大小 | 较小 | 较大 |
| 可读性 | 用 Excel 直接打开 | 需要代码或专用工具 |
| R/Python 读取 | read.csv() / pd.read_csv() |
jsonlite::fromJSON() / json.load() |
| 推荐场景 | 最终整理好的分析数据 | API 原始响应、元数据 |
15.6 数据采集伦理与 robots.txt
数据采集不仅是技术问题,更涉及法律和伦理。作为研究者,我们有责任以负责任的方式获取数据。
15.6.1 robots.txt 协议
robots.txt 是网站根目录下的一个文本文件,它告诉爬虫哪些页面可以访问、哪些不可以。在采集任何网站之前,你应该先检查它的 robots.txt:
# 查看 GBIF 的 robots.txt
response = requests.get("https://www.gbif.org/robots.txt")
print(response.text)一个典型的 robots.txt 文件内容如下:
User-agent: *
Disallow: /admin/
Disallow: /private/
Crawl-delay: 10
这表示:所有爬虫(*)不得访问 /admin/ 和 /private/ 路径,且每次请求之间应间隔至少 10 秒。
15.6.2 数据采集的基本准则
- 优先使用 API:如果网站提供了 API,优先使用 API 而非爬虫。API 是网站官方认可的数据获取方式。
- 遵守 robots.txt:不要访问 robots.txt 中禁止的路径。
- 控制请求频率:在请求之间添加适当的延时(如
time.sleep(1)),避免对服务器造成过大压力。 - 标明身份:在请求头中设置有意义的
User-Agent,说明你的用途。 - 尊重数据许可:注意数据的使用协议(如 CC BY 4.0),在论文中正确引用数据来源。
- 不采集个人信息:不要采集涉及个人隐私的数据。
未经授权的大规模数据采集可能违反《中华人民共和国网络安全法》和《数据安全法》。在进行任何数据采集之前,请确保你了解并遵守相关法律法规和网站的使用条款。课堂练习中,我们只使用明确允许数据访问的公共数据库。
15.7 完整案例:构建物种分布数据集
下面我们将前面学到的知识串联起来,完成一个完整的数据采集任务:从 GBIF 获取广西壮族自治区三种常见鸟类的分布记录,并保存为 CSV 文件。
import requests
import pandas as pd
import time
def get_taxon_key(species_name):
"""通过物种学名查询 GBIF taxonKey"""
url = "https://api.gbif.org/v1/species/match"
response = requests.get(url, params={"name": species_name}, timeout=10)
response.raise_for_status()
data = response.json()
return data.get("usageKey")
def get_occurrences(taxon_key, country="CN", max_records=100):
"""获取指定物种的分布记录"""
url = "https://api.gbif.org/v1/occurrence/search"
all_records = []
offset = 0
while len(all_records) < max_records:
params = {
"taxonKey": taxon_key,
"country": country,
"hasCoordinate": True,
"limit": min(100, max_records - len(all_records)),
"offset": offset
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
results = response.json().get("results", [])
if not results:
break
all_records.extend(results)
offset += len(results)
time.sleep(0.5) # 礼貌延时
return all_records
# 目标物种列表
species_list = [
"Egretta garzetta", # 白鹭
"Nycticorax nycticorax", # 夜鹭
"Ardeola bacchus" # 池鹭
]
# 批量采集
all_data = []
for species in species_list:
print(f"正在查询: {species}")
key = get_taxon_key(species)
if key is None:
print(f" 未找到 {species} 的分类信息,跳过")
continue
records = get_occurrences(key, max_records=50)
print(f" 获取到 {len(records)} 条记录")
for r in records:
all_data.append({
"species": r.get("species", ""),
"latitude": r.get("decimalLatitude"),
"longitude": r.get("decimalLongitude"),
"event_date": r.get("eventDate", ""),
"locality": r.get("locality", ""),
"dataset": r.get("datasetName", "")
})
# 整理并保存
df = pd.DataFrame(all_data)
df.to_csv("guangxi_birds_gbif.csv", index=False, encoding="utf-8-sig")
print(f"\n采集完成!共 {len(df)} 条记录,已保存至 guangxi_birds_gbif.csv")15.8 课后练习
15.8.1 练习 1:基础 API 调用
使用 GBIF API 查询你感兴趣的一种植物或动物在中国的分布记录。要求:
- 使用
species/match接口获取 taxonKey - 使用
occurrence/search接口获取至少 20 条有坐标的记录 - 将结果保存为 CSV 文件
- 在代码中添加适当的注释
15.8.2 练习 2:气候数据获取
使用 Open-Meteo API 获取你家乡所在城市 2024 年的气温和降水数据。要求:
- 查找你所在城市的经纬度坐标
- 获取全年的日均温和日降水量数据
- 计算月均温和月总降水量
- 将结果保存为 CSV 文件
15.8.3 练习 3(挑战):多物种分布数据集
参考 Section 15.7 的完整案例,选择同一科(Family)的 5 种物种,批量获取它们在中国的分布记录。要求:
- 编写可复用的函数
- 在请求之间添加适当的延时
- 处理可能的异常(如网络错误、物种未找到)
- 将最终数据保存为 CSV,并用几句话描述数据的基本特征(记录数、物种数、坐标范围等)
处理网络请求异常的基本模式:
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status() # 如果状态码不是 200,抛出异常
data = response.json()
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")