
爬虫程序入门:用 Python 抓取网页并保存数据
网页上的信息常常需要整理成表格,方便检索、分析或归档。手动复制少量内容很直接,但如果数据分布在多页网页里,重复操作就容易耗时且出错。用 Python 写一个小型爬虫,可以自动请求网页、提取需要的字段,并把结果保存为 CSV 等格式。
爬虫并不是“能访问就可以随便抓”。抓取前需要确认数据来源、网站规则和使用权限;抓取时要控制请求频率、范围和数量;保存数据时也要考虑隐私与安全。本文会从一个可运行的示例开始,介绍这些基本实践。
一、爬虫适合做什么
爬虫适合处理公开、允许自动访问、结构相对稳定的网页数据,例如:
- 定期整理公开公告、活动信息或公开目录。
- 抓取自己维护的网站,用于内容迁移、质量检查或搜索索引。
- 采集明确允许使用的公开样例数据,供个人学习或分析。
- 对少量网页做重复性检查,例如检查链接是否失效。
如果网站提供 API、数据下载文件或 RSS,通常应优先使用这些接口。接口的字段和调用规则一般更明确,也比解析页面结构稳定。网页爬虫更适合没有合适接口、且站点允许自动访问的场景。
爬虫不适合绕过登录、验证码、付费墙或访问控制,也不应该用来收集他人隐私、批量发送请求造成服务压力,或规避网站明确设置的限制。公开可见不等于可以无限制收集、保存或再分发;具体允许范围需要结合网站条款、适用法律和数据用途判断。
二、准备环境
示例使用 Python 3、requests 发送 HTTP 请求、beautifulsoup4 解析 HTML。建议在虚拟环境中安装依赖:
python -m venv .venv
# macOS 或 Linux
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\Activate.ps1
python -m pip install requests beautifulsoup4
如果系统中同时安装了多个 Python 版本,可将命令中的 python 换成对应的解释器命令,例如 python3。安装完成后,可以用下面的代码检查依赖是否可用:
python -c "import requests; from bs4 import BeautifulSoup; print('环境正常')"
三、理解抓取流程
一个基础爬虫通常包含四步:
- 请求:向指定 URL 发送 HTTP 请求,获取响应。
- 检查:确认请求成功,并检查响应类型等基本信息。
- 解析:从 HTML 中找到目标元素,提取需要的字段。
- 保存:将数据写入 CSV、JSON 或数据库。
下面以 https://quotes.toscrape.com/ 作为练习目标。它是专门用于网页抓取练习的网站,页面展示引言、作者和标签。示例只抓取有限页数,不需要登录,也不尝试访问受限内容。运行程序仍需能够连接外网;如果该练习站点不可访问,可以在自己有权限的网站或本地测试页面上替换目标地址和解析规则。
四、先发起一个受控请求
下面先请求首页并检查状态码。代码设置了连接和读取超时,避免请求无限期等待;同时设置了描述用途的 User-Agent。超时并不意味着请求一定能在指定的总时长内完成:requests 的连接超时与读取超时分别限制连接阶段和等待响应数据的时间。
import requests
url = "https://quotes.toscrape.com/"
headers = {
"User-Agent": "LearningCrawler/1.0 (educational example)"
}
response = requests.get(
url,
headers=headers,
timeout=(5, 20),
)
response.raise_for_status()
print("状态码:", response.status_code)
print("内容类型:", response.headers.get("Content-Type"))
print(response.text[:300])
raise_for_status() 会对 HTTP 4xx、5xx 响应抛出异常,让程序不要把错误页面误当成正常数据继续处理。注意,HTTP 200 只表示服务器返回了成功状态,并不能保证拿到的就是预期内容:页面可能是空的、结构已经变化,或者返回了提示页。因此仍要检查内容类型和解析结果。
五、用 Beautiful Soup 提取字段
网页通常由 HTML 元素组成。可以先用浏览器开发者工具检查页面,再根据元素的标签、属性或 CSS 类选择器定位数据。下面的函数解析一页引言,并返回字典列表:
from bs4 import BeautifulSoup
def parse_quotes(html):
soup = BeautifulSoup(html, "html.parser")
results = []
for item in soup.select("div.quote"):
quote_node = item.select_one("span.text")
author_node = item.select_one("small.author")
tag_nodes = item.select("a.tag")
# 结构缺失时跳过该条,避免访问 None 的属性。
if quote_node is None or author_node is None:
continue
results.append({
"quote": quote_node.get_text(" ", strip=True),
"author": author_node.get_text(" ", strip=True),
"tags": ", ".join(
node.get_text(" ", strip=True) for node in tag_nodes
),
})
return results
select() 使用 CSS 选择器,返回匹配到的多个元素;select_one() 返回第一个匹配项,没有匹配时返回 None。因此在读取文本前应检查元素是否存在。get_text(" ", strip=True) 会提取文本、去除首尾空白,并用空格连接内部文本节点。
选择器依赖页面结构。网站改版后,类名或层级可能变化,程序就可能抓不到内容。调试时可以先打印匹配数量和少量样本,而不是马上把结果当作完整数据:
soup = BeautifulSoup(response.text, "html.parser")
print("页面标题:", soup.title.get_text(strip=True) if soup.title else "无标题")
print("匹配到的引言数:", len(soup.select("div.quote")))
六、遍历有限页数并保存 CSV
练习站点的下一页链接指向类似 /page/2/ 的地址。下面的完整示例最多请求 3 页,逐页解析后写入 UTF-8 编码的 CSV。它还包含请求超时、HTTP 状态检查、异常提示和请求间隔。
import csv
import time
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
START_URL = "https://quotes.toscrape.com/"
MAX_PAGES = 3
OUTPUT_FILE = "quotes.csv"
HEADERS = {
"User-Agent": "LearningCrawler/1.0 (educational example)"
}
def parse_quotes(html):
soup = BeautifulSoup(html, "html.parser")
rows = []
for item in soup.select("div.quote"):
quote_node = item.select_one("span.text")
author_node = item.select_one("small.author")
tag_nodes = item.select("a.tag")
if quote_node is None or author_node is None:
continue
rows.append({
"quote": quote_node.get_text(" ", strip=True),
"author": author_node.get_text(" ", strip=True),
"tags": ", ".join(
node.get_text(" ", strip=True) for node in tag_nodes
),
})
return soup, rows
def main():
session = requests.Session()
session.headers.update(HEADERS)
current_url = START_URL
all_rows = []
for page_number in range(1, MAX_PAGES + 1):
# 示例只允许请求练习站点的同一主机。
parsed_url = urlparse(current_url)
if parsed_url.scheme != "https" or parsed_url.hostname != "quotes.toscrape.com":
raise ValueError(f"拒绝访问非预期地址:{current_url}")
print(f"正在请求第 {page_number} 页:{current_url}")
try:
response = session.get(current_url, timeout=(5, 20))
response.raise_for_status()
except requests.RequestException as exc:
print(f"请求失败,停止抓取:{exc}")
break
content_type = response.headers.get("Content-Type", "")
if "text/html" not in content_type.lower():
print(f"响应不是 HTML,停止抓取:{content_type}")
break
soup, rows = parse_quotes(response.text)
if not rows:
print("没有解析到记录,可能是页面结构变化,停止抓取。")
break
all_rows.extend(rows)
next_link = soup.select_one("li.next a")
if next_link is None:
print("没有下一页,抓取结束。")
break
current_url = urljoin(current_url, next_link.get("href", ""))
# 控制请求频率;实际间隔应依网站规则和服务承受能力调整。
time.sleep(1)
if not all_rows:
print("没有可保存的数据。")
return
fieldnames = ["quote", "author", "tags"]
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(all_rows)
print(f"已保存 {len(all_rows)} 条记录到 {OUTPUT_FILE}")
if __name__ == "__main__":
main()
将代码保存为 scrape_quotes.py,在命令行运行:
python scrape_quotes.py
程序成功完成后,会在当前工作目录生成 quotes.csv。用表格软件打开时,utf-8-sig 有助于部分 Windows 环境正确识别 UTF-8 编码;它会在文件开头写入 UTF-8 BOM。如果数据需要被其他程序严格按 UTF-8 读取,也可以改为 encoding="utf-8"。
示例中的 MAX_PAGES 是程序主动设置的页数上限,不是网站或 requests 的固定限制。它用于避免练习程序意外抓取过多页面。实际项目应按照授权范围和明确需求设置上限,不能把“能翻到多少页”当作可抓取范围。
七、网页抓取的重要边界
1. 检查规则和授权
抓取前阅读网站服务条款、数据使用说明以及相关接口文档,并确认自己的用途在允许范围内。网站的 robots.txt 文件可表达站点管理者对自动抓取的偏好或路径规则,程序可以读取并尊重这些规则;但它本身不是完整的法律意见,也不能替代授权、合同或适用法律的判断。反过来,某个路径没有被 robots.txt 禁止,也不自动意味着对该路径及其数据拥有使用权。
如果站点要求不要自动访问、限制频率,或者明确禁止特定用途,应遵守相应规则。遇到不确定情况,联系网站运营方取得许可,比尝试绕过限制更稳妥。
2. 控制请求频率和抓取范围
每一个页面请求都会占用服务器和网络资源。请求间隔、并发数、总页数应从小开始,并根据网站明确的速率限制调整。不要通过增加线程、代理轮换或伪造身份来规避限制;收到 429(请求过多)等响应时,不应继续加速请求。生产程序还应设置最大运行时间、最大记录数和重试次数。
示例中的 time.sleep(1) 只是演示如何在请求之间留出间隔,不代表所有网站都允许每秒一次,也不保证一定足够温和。应以网站公布的要求为准;若网站明确要求更长间隔,就采用更长间隔。
3. 注意个人信息和数据用途
姓名、联系方式、账号信息等可能属于个人信息。即使网页无需登录就能看到,也不代表可以无限制收集、建立画像、公开传播或用于商业决策。应遵循适用的数据保护要求,限定收集字段、用途和保存期限,并采取必要的访问控制和删除措施。学习示例尽量选用专门提供的练习网站或明确开放的数据集。
4. 保护自己的运行环境
不要把用户输入的任意 URL 直接交给爬虫请求。若程序部署在服务器上,访问内网地址、云服务元数据地址或本机管理接口,可能造成服务器端请求伪造(SSRF)等风险。应将目标主机限制在明确的允许列表中,并在每次跳转后重新验证最终 URL;还要考虑 DNS 解析、重定向和代理配置带来的风险。示例检查了初始请求地址,但没有实现完整的重定向安全策略,因此不应直接扩展成接受任意目标的线上服务。
从网页提取的文本也应视为不可信输入。如果之后把抓取内容写入网页、执行命令或拼接 SQL,需要分别进行适当的 HTML 转义、参数化查询或输入校验。不要因为数据来自网页就默认它安全。
八、常见错误与排查方法
请求超时或连接失败
可能是网络不可用、域名无法解析、目标站点暂时故障,或程序运行环境无法访问外网。先用浏览器访问目标站点,再检查代理、防火墙和 DNS 配置。示例中的异常处理会停止后续抓取;对于临时性错误,可以设计有限次数的重试,并加入递增等待,但不要无限重试或对拒绝请求的站点持续施压。
出现 403、429 等响应
403 表示服务器拒绝当前请求,429 表示请求过多。状态码背后的具体策略因网站而异。不要把更换代理、伪造浏览器指纹或绕过登录当成默认修复办法。应停止或降低请求,检查站点规则和授权;如果确实需要数据,优先联系网站或使用其提供的接口。
程序没有异常,但 CSV 为空
这通常是选择器不匹配、页面结构变化、内容由 JavaScript 动态加载,或者目标响应不是预期页面。先查看 response.status_code、Content-Type 和响应正文的开头,再用浏览器开发者工具确认元素结构。可以临时把 HTML 保存到文件进行检查,但要避免把包含敏感信息的页面提交到公开代码仓库。
网页内容看起来不全
requests 获取的是服务器返回的 HTTP 响应,不会像浏览器那样自动执行页面 JavaScript。若数据是浏览器端脚本请求 API 后生成的,静态 HTML 中可能没有目标内容。先确认网站是否提供公开 API 或数据下载方式。不要因为页面由 JavaScript 渲染,就直接尝试规避访问控制;任何自动访问都仍需符合网站规则和授权范围。
中文乱码或 CSV 在表格软件中显示异常
确认响应的编码与网页实际编码相符。requests 会根据响应头等信息推断编码,但服务器配置错误时推断可能不准确,可以检查 response.encoding 和 HTML 中声明的编码后再处理。写 CSV 时明确指定编码;如果目标软件对 UTF-8 识别不佳,可尝试示例中的 utf-8-sig。
字段中有逗号,CSV 列被拆开
不要用字符串拼接手工写 CSV。示例使用 Python 标准库的 csv.DictWriter,它会按 CSV 格式处理字段中的逗号、引号和换行。读取时也应使用 CSV 解析库,而不是简单按逗号切分。
九、让小型爬虫更可靠
本文示例适合学习,不是完整的生产系统。实际使用时可以逐步加入以下能力:
- 明确配置:将起始地址、页数上限、输出路径和请求间隔集中配置,并在启动时检查其有效性。
- 记录日志:记录请求页面、状态码、解析条数和失败原因,避免只靠终端输出排查问题。
- 限制数据量:设置最大页数和最大记录数,防止分页异常或链接循环导致长时间运行。
- 处理重复记录:依据稳定字段设计去重规则,并保留必要的来源 URL 和抓取时间,便于核查。
- 验证输出:检查字段是否为空、记录数量是否合理,以及文件能否被下游程序读取。
- 安全管理:不要将密码、令牌或个人数据硬编码到脚本或提交到版本库。
网页结构不是稳定的数据接口。若抓取任务需要长期运行,应在每次运行后监控结果数量和字段质量,一旦页面改版或数据异常,及时暂停并检查,而不是继续把错误结果写入业务系统。
十、总结
一个入门爬虫的核心并不复杂:用 requests 请求网页,用 Beautiful Soup 定位元素,再用 csv 保存结果。真正需要认真对待的,是数据是否允许使用、请求是否克制、解析是否经过验证,以及程序是否限制了访问范围。
建议先从少量、公开且明确适合练习的数据开始,确认每一步输出符合预期,再考虑扩展。能运行只是爬虫的起点;尊重网站规则、保护数据和控制运行风险,才是可持续使用它的前提。








