Python爬虫实战:从抓包到反爬的完整数据采集方案
这次直接上一套能落地的 Python 网络爬虫完整方案:抓包、构造请求、解析数据、应对反爬、接代理、防封防限流,最后再串成一个带批量导出能力的实战项目。很多人学爬虫失败不是因为某个环节学不会,而是每块知识都是割裂的:只讲 requests 不讲抓包,只讲 XPath 不讲反爬,最后换一个真实网站照样寸步难行。这篇文章会把整条链路串起来,从浏览器开发者工具定位数据来源开始,一直做到 CSV 批量导出。
先给结论:这套方案不需要 GPU,不需要高配服务器,一台普通办公电脑就够了。核心依赖是requests、beautifulsoup4、lxml,全部是 Python 生态里最成熟的库。全程采用命令行方式运行,方便调试、方便移植、也方便后续封装成接口服务。文章里所有代码都可以直接复制,示例站点选的是专门的爬虫练习站点,跑起来没有合规风险,非常适合验证整个流程。
文章会覆盖五条主线:抓包与请求构造、HTML/JSON/JSON-LD 数据解析、常见反爬机制与合规应对思路、IP 代理池设计与轮换、以及包含随机延迟和重试机制的稳定性工程。最后用一个综合实战项目,把前面所有内容串成一条完整流水线。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 技术路线 | Python + requests + BeautifulSoup + lxml + re,本地命令行运行 |
| 核心功能 | 浏览器抓包、HTTP 请求、HTML/接口数据解析、反爬识别、代理池、并发与重试 |
| 运行环境 | Python 3.9 或更高版本,Windows / macOS / Linux 均可 |
| 硬件要求 | 无显卡要求,普通办公电脑即可 |
| 启动方式 | 直接运行.py脚本,或把模块 import 到自己的项目里 |
| 是否支持 API | 不是内置服务,但可以封装成 FastAPI / Flask 接口供业务调用 |
| 是否支持批量任务 | 支持,可批量抓取列表页和详情页,输出 CSV / Excel / 数据库 |
| 第三方依赖 | requests、beautifulsoup4、lxml、pandas(可选) |
| 典型场景 | 公开数据采集、价格监控、搜索聚合、数据分析和模型训练数据准备 |
| 使用边界 | 遵守目标站点 robots.txt 与授权要求,不碰登录后数据和隐私数据 |
这套流程的最大特点是可维护:抓包结果要记录、代理要定时校验、请求要加延迟和重试、数据要分阶段落盘。很多人的爬虫跑一次能用、跑两次就报错,本质上就是少了这些工程化环节。
2. 适用场景与使用边界
爬虫技术本身是中性的,重点看你怎么用。先说要解决什么问题:如果你需要定期采集公开商品价格、公开公告、论文摘要、公开赛事数据,或者想给自己做一套聚合搜索的数据源,这套流程完全够用。它也能用于构建本地测试数据集,为 NLP 或数据分析项目准备语料。
不适合的场景也很明确。第一类是登录后才能访问的页面,这类数据通常已经超出“公开数据”的范畴,直接抓取采集授权风险很大。更稳妥的做法是走官方 API,或者和平台方确认授权后,再使用账号会话完成采集。第二类是付费内容、会员专属内容,这类内容不能绕过付费机制去抓。第三类是包含大量个人隐私信息的数据,比如手机号、身份证、聊天记录,除非有明确的法律授权和平台书面授权,否则不能采集。
关于“反爬绕过”,这里必须说清楚边界:本篇文章只讲合规的请求策略,比如补全请求头、对 User-Agent 做轮换、控制访问频率、通过代理分散请求压力,这些是降低单 IP 触发限流概率的常规做法。但像破解验证码、绕过 WAF 校验、模拟伪造登录态这类行为,已经属于安全规避范畴,不仅不稳定,而且违法风险极高,这里不展开、不教授、也不鼓励。实践中的正确路径是:能走 API 就走 API,能申请授权就申请授权,实在不行才考虑爬虫,并且始终把目标站点源站稳定性放在第一位。
还要提醒一点:爬取到的数据不等于你可以随意使用。公开数据可能仍受著作权、数据库权利或平台规则约束。如果你要二次发布、商用或训练模型,需要自己确认授权链条,不能因为“技术上能拿到”就默认“法律上可以用”。
3. 环境准备与前置条件
爬虫项目的环境准备非常轻,只需要三件事:Python 解释器、虚拟环境、几个 pip 包。
建议使用 Python 3.9 及以上版本,Python 3.10 和 3.11 目前生态兼容性最好。如果你机器上同时存在多个 Python 版本,请务必在虚拟环境里操作,避免污染全局依赖。
创建虚拟环境并安装依赖:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install requests beautifulsoup4 lxml如果后面要做数据分析或导出 Excel,可以再把 pandas 装上:
pip install pandas openpyxl安装完成后做一次快速验证:
python -c "import requests, bs4, lxml; print('依赖安装成功')"环境准备这块没有太多坑,常见问题通常出在 pip 下载源上。如果你在国内网络环境下安装缓慢,可以临时使用国内 PyPI 镜像:
pip install requests beautifulsoup4 lxml -i https://pypi.tuna.tsinghua.edu.cn/simple代码示例里的目标站点我选了专门的爬虫练习站books.toscrape.com,这个站点是公开的、允许爬取学习的假书店,结构简单,适合验证抓包、解析、翻页三个核心环节。另一个常用的练习站是quotes.toscrape.com,用于练练文本引用抓取。httpbin.org则可以用来验证请求头和代理是否生效。
4. 抓包请求:浏览器、抓包工具与 requests
4.1 用浏览器开发者工具定位数据来源
写爬虫的第一步不是写代码,而是先搞清楚目标页面的数据到底是从哪里来的。打开 Chrome 或 Edge,进入目标页面后按F12打开开发者工具,切到“网络”(Network)面板,刷新页面,观察请求列表。
关键看两类请求:
Document类型:返回完整 HTML,说明数据在服务端渲染后直接输出。XHR/Fetch类型:返回 JSON 或局部 HTML,说明页面是前端异步加载数据。
以books.toscrape.com为例,刷新后能看到一个 Document 请求,响应 HTML 里直接包含了每本书的标题、价格和评分。这种情况下,只要解析 HTML 就能拿到全部数据。如果换成一些单页应用,比如 Vue 或 React 项目,你会发现 HTML 里没有数据,必须去 XHR 里找接口。哪一步判断错了,后面所有代码都是白写。
在开发者工具的“请求”面板里,你还能看到完整的请求头、Cookie、请求参数和响应内容。这里有一个效率很高的技巧:在某个请求上右键,选择“复制为 cURL”,然后粘贴到curlconverter这类工具里,能快速转成 Python requests 代码。这样构造出来的请求头最接近真实浏览器,不容易在第一步就被拒绝。
4.2 用 requests 构造基础请求
掌握了请求头之后,用 requests 构造请求就非常简单。一个最基础的 GET 请求:
import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } url = "https://books.toscrape.com/" resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() print(resp.status_code, len(resp.text))一定不要省略headers和timeout。没有 User-Agent 的请求非常容易被识别成异常请求;没有 timeout 的请求在网络波动时可能一直阻塞,把整个爬虫卡死。
resp.raise_for_status()也很重要,当服务器返回 4xx 或 5xx 时会主动抛出异常,而不是让你在一个错误页面上继续解析。
4.3 用 Session 保持会话状态
很多网站会把登录状态、操作记录写入 Cookie。requests 里的Session对象会自动保存 Cookie,并在后续请求中携带,非常适合需要连续翻页的场景。
import requests session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", }) resp = session.get("https://books.toscrape.com/catalogue/page-1.html", timeout=10) print(resp.status_code)Session的另一个好处是连接复用。在同一个会话里连续发送多个请求时,底层会复用 TCP 连接,整体效率更高。
4.4 移动端与 HTTPS 抓包工具
浏览器开发者工具只能看到浏览器进程内的请求。如果你要分析的是自己的 App 测试环境,或者需要抓取移动端应用的调试流量,可以使用 Fiddler 或 Charles。需要说明的是,这类工具只应该用于调试你自己拥有或有明确授权的应用与设备,安装证书、配置代理都是在本地设备上操作,不要在未授权的情况下对第三方 App 做中间人抓包。
从技术角度看,抓包工具的原理是本地启动一个 HTTP 代理,客户端把流量转发到这个代理端口,工具再帮你查看和修改请求。实际爬虫工程中,大部分 Web 场景用浏览器开发者工具就足够了,抓包工具主要用于移动端或需要修改请求响应的场景。
5. 数据解析:BeautifulSoup、XPath、re 与结构化数据
拿到 HTML 或 JSON 之后,下一步是抽数据。这一步做不好,前面全白费。下面按推荐优先级介绍四种解析方式。
5.1 BeautifulSoup 与 CSS 选择器
BeautifulSoup 是入门首选,容错能力强,即使 HTML 标签不规范也能解析。配合 CSS 选择器,代码可读性非常高。
from bs4 import BeautifulSoup html = """ <article class="product_pod"> <h3><a href="a.html" title="A Light in the Attic">A Light in the Attic</a></h3> <p class="price_color">£51.77</p> <p class="star-rating Three"></p> </article> """ soup = BeautifulSoup(html, "html.parser") article = soup.select_one("article.product_pod") title = article.h3.a.get("title") price = article.select_one("p.price_color").text rating_class = article.select_one("p.star-rating").get("class", []) # <p class="star-rating Three"> 的 class 是 ["star-rating", "Three"] rating = rating_class[1] if len(rating_class) > 1 else "" print(title, price, rating)这里提一个解析的通用思维:任何 HTML 解析的本质都是“先定位到目标节点,再从节点里抽字段”。比如标题在h3 > a的title属性里,价格在p.price_color的文本里,评分在p.star-rating的 class 里。先写一个最小 HTML 样本测试解析逻辑,再应用到整个页面,能省掉大量调试时间。
5.2 lxml 与 XPath
当 HTML 结构复杂、层级较深时,XPath 的表达式比 CSS 选择器更灵活。lxml 解析速度也更快,适合批量页面处理。
from lxml import etree html = resp.text tree = etree.HTML(html) titles = tree.xpath("//article[contains(@class,'product_pod')]//h3/a/@title") prices = tree.xpath("//article[contains(@class,'product_pod')]//p[@class='price_color']/text()") print(titles[:5]) print(prices[:5])//表示相对路径查找,@title表示取属性,text()表示取文本。XPath 的调试可以借助 Chrome 开发者工具的$x函数,直接在 Console 里验证路径是否正确。
5.3 正则表达式 re
正则适合从文本中提取特定模式的碎片,比如价格、日期、编号。它不适合解析嵌套结构,但处理扁平文本很有用。
import re html_text = resp.text pattern = re.compile(r"£(\d+\.\d{2})") prices = pattern.findall(html_text) print(prices[:10])注意:正则写得太严格会让解析结果为空,写得太宽松会匹配到无关内容。建议先取一小段文本测试,再放到完整页面里跑。
5.4 JSON 接口数据解析
当数据来自 XHR 接口时,响应体通常是 JSON,直接用json()方法解析,不要用正则去抠字符串。
import requests resp = requests.get("https://httpbin.org/json", timeout=10) data = resp.json() print(data.get("slideshow", {}).get("title"))接口字段一般比 HTML 稳定,也更容易做字段映射。优先抓接口、拿 JSON,是效率比较高的爬虫策略。
5.5 JSON-LD 结构化数据解析
现在很多站点会在 HTML 里嵌入application/ld+json类型的script标签,用schema.org标准描述商品、文章、事件等实体。这类结构化数据的价值在于,它本身就是字段清晰的 JSON,比 CSS 选择器稳定得多。
import json from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "html.parser") script = soup.select_one('script[type="application/ld+json"]') if script: data = json.loads(script.string) print(data.get("name")) print(data.get("offers", {}).get("price"))从数据解析趋势看,搜素引擎和 AI 搜索的核心机制已经不是纯字符串匹配,而是依托schema.org这类语义化标记做实体识别。对爬虫开发者来说,优先解析结构化标记是顺应趋势的做法,字段准确率往往比手工选择器更高。以 4D 毫米波雷达数据解析为例,无论底层数据是 JSON、二进制还是 HD Map,思路都是一样的:先找到结构化协议,再按字段定义抽取目标信息,而不是靠肉眼在文本里找规律。
6. 反爬机制与合规应对策略
站点为什么要反爬?本质上是为了保护服务器资源、防止恶意刷接口和确保数据使用符合平台规则。从爬虫方的角度,我们要做的是“通过合理的请求方式降低对服务器的压力”,而不是“暴力对抗安全机制”。下面是一些常见反爬现象和对应的合规策略。
6.1 常见反爬有哪些表现
- 请求缺少
User-Agent或 UA 高频重复,返回 403。 - 短时间请求频率过高,触发限流,返回 429 或页面跳转验证。
- 缺少
Referer、Origin等浏览器行为头,被判定为跨站请求。 - 最近 IP 上下线过多,被防火墙自动封禁一段时间。
- 爬虫访问顺序过于规律,比如固定间隔 1 秒,被行为检测识别。
6.2 补全请求头与 UA 轮换
最简单的改进是把它当作浏览器来对待。把浏览器开发者工具里的请求头完整复制过来,至少包含User-Agent、Accept、Accept-Language。如果需要从上个页面跳转过来,还要补上Referer。
UA 轮换可以用一个列表维护多组字符串,每次请求随机取一个。
import random USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0", ] def get_headers(): return { "User-Agent": random.choice(USER_AGENTS), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", }这里要说明,UA 轮换只是模仿真实浏览器的基础操作,不是用来对抗安全策略的。真实目标如果对浏览器指纹、TLS 指纹做识别,靠 requests 很难模拟,这时候应该考虑 Playwright 等浏览器自动化方案,并且依然要遵守目标站点的访问规则。
6.3 验证码与登录鉴权怎么处理
这是很多人踩过的坑。我的建议是:不要在验证码对抗上花时间。自动识别验证码通常涉及突破验证流程,不仅不稳定,在多数场景下还有合规风险。正确的做法是:
- 如果能走官方 API,直接使用 API Key 和 Token。
- 如果必须登录,通过正常方式获取会话,并保持低频访问。
- 如果平台强制要求验证码,接入人工验证流程,或与平台协商授权合作。
登录鉴权同理,不要尝试伪造 Session 或绕过权限判断。正确做法是获取真实授权后再采集,并且及时续期、及时清理。
7. IP 代理与代理池设计
7.1 为什么要用 IP 代理
IP 代理最常见的用途是分散请求流量,降低单一 IP 因高频访问触发限流的概率。在目标网站只允许单一 IP 固定频率访问的情况下,通过代理池把请求分散到多个出口 IP,可以明显降低封禁概率。
注意,代理不是万能的。免费代理的可用率通常很低,速度和稳定性也无法保证,而且很多免费代理来路不明,数据经过第三方转发有泄露风险。实际项目中,建议优先使用正规代理服务商提供的住宅代理或数据中心代理。
7.2 requests 设置代理
requests 设置代理非常简单,传入proxies参数即可:
proxies = { "http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890", } resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10) print(resp.json())httpbin.org/ip会返回当前出口 IP,可以用来验证代理是否生效。注意:代理地址中的 IP 和端口需要替换为你实际可用的代理服务地址。
7.3 设计一个简单代理池
一个可用的代理池至少需要三个能力:存储代理、校验代理、轮换代理。下面是一个示例,使用列表存储代理,循环调用校验函数剔除失效代理。
import random import requests PROXY_POOL = [ {"http": "http://1.2.3.4:8080", "https": "http://1.2.3.4:8080"}, {"http": "http://5.6.7.8:8080", "https": "http://5.6.7.8:8080"}, ] def check_proxy(proxy, test_url="https://httpbin.org/ip", timeout=5): try: resp = requests.get(test_url, proxies=proxy, timeout=timeout) return resp.status_code == 200 except requests.RequestException: return False def get_working_proxy(): random.shuffle(PROXY_POOL) for proxy in PROXY_POOL: if check_proxy(proxy): return proxy return None这里只是一个演示结构。更完整的代理池会把代理列表存到 Redis 或 SQLite,每隔几分钟从代理服务商 API 拉取新代理,再定时校验,把失效代理剔除,把可用代理按响应速度排序。核心要点是:不要每次请求都直接使用同一个代理,而是先校验再使用,并设置失败自动换下一个。
在 IP 代理池的搭建上,还应该把代理的延迟、可用时间、成功率三个指标记录到日志里。长期运行后,你就能根据数据挑出最稳定的一批代理,有效提升采集效率。
8. 防封避坑:稳定性、断点、日志与并发
8.1 随机延迟,拒绝机器人节律
如果你固定每隔 1 秒访问一次,这个规律非常容易被侦测到。正确做法是使用随机延迟,让访问间隔呈自然分布。
import random import time # 在每次请求之间调用 time.sleep(random.uniform(0.5, 2.0))延迟区间需要根据目标站点的实际负载调整。如果站点响应很慢,可以把下限调到 1 秒、上限调到 3 秒。总之,宁可慢一点,也不要因为请求过频导致整个 IP 被封。
8.2 重试机制与指数退避
网络波动、服务器偶发 5xx,都不应该成为爬虫崩溃的理由。加一个带指数退避的重试装饰器,能显著提高任务完成率。
import time from functools import wraps def retry(max_retries=3, base_wait=2): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except requests.RequestException as exc: print(f"第 {attempt + 1} 次请求失败: {exc}") if attempt == max_retries - 1: raise time.sleep(base_wait * (2 ** attempt)) return wrapper return decorator @retry(max_retries=3) def safe_get(session, url): resp = session.get(url, timeout=10) resp.raise_for_status() return resp指数退避的核心是先等 2 秒,再等 4 秒,再等 8 秒。这样即使目标站点临时抖动,也能给服务器留出恢复时间。
8.3 日志与断点续爬
长时间运行的爬虫,最怕中途崩溃后从头再来。日志要记录到文件中,断点续爬要依赖“已完成 URL 集合”和“待处理任务队列”。
import json import os DONE_FILE = "done_urls.json" def load_done(): if os.path.exists(DONE_FILE): with open(DONE_FILE, "r", encoding="utf-8") as f: return set(json.load(f)) return set() def save_done(done): with open(DONE_FILE, "w", encoding="utf-8") as f: json.dump(list(done), f, ensure_ascii=False, indent=2)每次处理完一个 URL 就立刻写入 done 集合,下次启动时跳过已完成项。这种方式在数据量不大时完全够用;数据量特别大时,可以改为处理完后写一条记录到单独的“进度文件”或数据库。
8.4 控制并发
并发能提升抓取效率,但并发数过高也容易触发限流。建议用ThreadPoolExecutor加信号量控制最大并发数。
from concurrent.futures import ThreadPoolExecutor, as_completed import threading semaphore = threading.Semaphore(5) # 最多 5 个并发请求 def limited_fetch(url): with semaphore: return safe_get(session, url) urls = ["https://books.toscrape.com/catalogue/page-{}.html".format(i) for i in range(1, 51)] with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(limited_fetch, url): url for url in urls} for future in as_completed(futures): try: resp = future.result() print(resp.status_code, futures[future]) except Exception as exc: print(f"抓取失败: {exc}")并发不是越高越好。建议先用并发 1 验证流程,再逐渐提高到 3、5、8,观察响应速度和封禁情况。服务器压力过大时,优先降低并发而不是加代理。
9. 综合实战项目:把整套流程串起来
现在把前面的所有内容组合成一个完整的爬虫项目。目标站点是books.toscrape.com,这是一个专门为爬虫练习设计的假书店,页面结构简单,且对爬虫友好。我们会抓取前几页的书籍信息,解析标题、价格、评分,最后写入 CSV。
第一步:查看目标站点是否允许爬虫。在浏览器访问https://books.toscrape.com/robots.txt,可以看到该站点未明确禁止爬虫,适合做学习测试。实际项目中,请务必先看 robots.txt,同时阅读目标站点的服务条款。
第二步:编写完整爬虫脚本,包含请求头、Session、随机延迟、翻页解析和 CSV 导出。
import csv import random import time import requests from bs4 import BeautifulSoup BASE_URL = "https://books.toscrape.com/" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } def fetch_page(session, url): resp = session.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() return BeautifulSoup(resp.text, "html.parser") def parse_books(soup): books = [] for article in soup.select("article.product_pod"): title = article.h3.a.get("title") price_text = article.select_one("p.price_color").text price = float(price_text.replace("£", "")) rating_classes = article.select_one("p.star-rating").get("class", []) rating = rating_classes[1] if len(rating_classes) > 1 else "" books.append({"title": title, "price": price, "rating": rating}) return books def parse_next_url(soup): next_li = soup.select_one("li.next > a") if next_li: return BASE_URL + next_li.get("href") return None def main(): session = requests.Session() url = BASE_URL all_books = [] while url: print(f"[抓取] {url}") soup = fetch_page(session, url) books = parse_books(soup) all_books.extend(books) print(f"[解析] 当前页获取到 {len(books)} 本书") url = parse_next_url(soup) time.sleep(random.uniform(0.8, 1.8)) output_file = "books_output.csv" with open(output_file, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "price", "rating"]) writer.writeheader() writer.writerows(all_books) print(f"[完成] 共抓取 {len(all_books)} 本书,写入 {output_file}") if __name__ == "__main__": main()第三步:运行脚本,等待执行完成。正常情况下,控制台会输出每一页的抓取信息和解析数量,最终在项目目录生成books_output.csv。
第四步:用 pandas 快速验证结果。
import pandas as pd df = pd.read_csv("books_output.csv") print(df.head()) print(f"总行数: {len(df)}") print(df.groupby("rating").size())判断成功标准很直接:CSV 里每一行的标题非空、价格是数值、评分字段在 One 到 Five 之间,并且总行数等于所有页面书籍数之和。如果出现价格解析为空或评分字段缺失,优先检查 HTML 结构是否变化,再检查解析选择器。
这个项目没有做断点续爬和代理池,但它把你前面学到的抓包、请求头、解析、翻页、CSV 导出都串起来了。后续扩展方向非常清晰:把parse_books改成通用字段解析,把 CSV 输出改成写入 MySQL 或 SQLite,再加上done_urls去重,就能作为一个可长期运行的采集任务。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 请求返回 403 | 请求头缺失,UA 被识别 | 在浏览器控制台比较请求头差异 | 复制完整 headers,使用 Session 保持会话 |
| 请求超时 | 网络波动或代理不可用 | 单独测试 URL 连通性 | 设置 timeout,增加重试机制 |
| SSL 证书错误 | 目标站点证书不被信任 | 查看错误信息中的域名 | 使用verify=False前先评估风险,正常情况不要关闭证书验证 |
| 中文乱码 | 编码识别错误 | 查看响应头中的 charset | resp.encoding = resp.apparent_encoding |
| 解析结果为空 | HTML 结构变化或选择器写错 | 打印一小段 HTML 检查 | 用 Chrome$x或soup.select_one逐层定位 |
| 代理无效 | 代理失效或未生效 | 请求 httpbin.org/ip 验证 | 设计代理池定时校验,失败自动换下一个 |
| IP 被封 | 请求频率过高 | 观察 429 状态码 | 降低频率,增加随机延迟,使用代理分散流量 |
| 运行中途崩溃 | 未处理异常和断点 | 查看日志文件 | 加入 try/except、重试、断点续爬 |
| 数据重复 | 没有去重机制 | 检查导出结果 | 维护 done_urls 集合,或使用数据库唯一索引 |
这些排查步骤的通用思路是:先复现,再看日志,再缩小范围。不要一上来就改代码逻辑,先确认是网络问题、请求问题还是解析问题,效率会高很多。
11. 最佳实践与合规提醒
爬虫工程化的核心不是“能抓到数据”,而是“抓得稳、可维护、可扩展”。下面是一些值得长期坚持的习惯。
第一,运行前检查目标站点的robots.txt和访问条款。即使技术上能抓,也不代表被允许。优先使用官方 API 或申请白名单,这是最稳妥的路径。
第二,所有请求都要带上明确的 User-Agent 和超时时间。最好在 UA 中注明用途或联系方式,方便站点管理员在需要时和你联系。
第三,访问频率宁慢勿快。小型站点建议延迟 1 到 3 秒,批量任务建议用队列加限流,而不是并发直接拉满。稳定比速度更重要,因为封禁一次 IP 的损失远超节省下来的那几分钟。
第四,输出数据要分级落盘,不要只存在内存里。第一级保存原始 HTML,第二级保存解析后的 JSON,第三级再汇总到 CSV 或数据库。这样即使解析规则写错了,也能从原始 HTML 重新解析,不用重新抓一遍。
第五,涉及图片、视频、文档等多类型资源时,要单独设计下载器,并检查磁盘空间和下载失败重试机制。
第六,涉及个人数据、人脸、声音、版权内容时,必须确认授权链条。如果你的爬虫会采集到用户头像、昵称、联系方式等个人信息,要特别注意最小化收集,不要额外获取和存储无关字段。内容要二次发布或商用前,也要确认著作权授权。
第七,代理服务要选择正规服务商,不要使用来源不明的免费代理,避免数据在传输过程中被第三方截获。
第八,保存代码和配置文件到版本管理工具中,数据 schema 变化时能快速回溯。
12. 从零到一之后的下一步
这套流程跑通后,你最应该验证的第一件事是:对任意一个公开站点,先用 F12 找准数据来源,再写最小请求脚本,最后做解析。这三个动作能在 10 分钟内完成,意味着你已经掌握了爬虫的核心链路。
最容易踩的坑有三个:请求头补不齐导致 403、解析字段错位导致空数据、访问频率过快导致封 IP。这三类问题占了日常爬虫排错的绝大多数。
如果你想把能力延伸下去,建议按这个顺序学习:
- 用 Scrapy 把项目改造成框架化结构,利用 Item Pipeline 和中间件管理请求。
- 用 Playwright 处理 JavaScript 渲染页面,解决 SPA 站点数据不在 HTML 里的问题。
- 把爬虫封装成 FastAPI 接口,支持外部系统提交任务、查询进度、下载结果。
- 引入消息队列,把抓取任务和解析任务解耦,搭建分布式采集系统。
爬虫技术真正有价值的地方,是它逼着你把网络协议、数据解析、工程稳定性、合规边界全部打通。技术不难,难的是在尊重目标站点和保护数据的前提下,把整条流水线做扎实。建议把这篇文章的代码保存好,下次遇到真实需求时,直接拿示例站点跑通流程,再替换成你的目标页面,会省掉大量磕磕绊绊的时间。
