Python 爬虫合规与反爬实战:从 requests 到 Playwright
"写爬虫不难,难的是写得既抓得到数据、又睡得着觉。"
这两年我带过不少大学生做数据采集的小项目,几乎每个人第一版代码都是直接 requests.get 然后疯狂循环。结果不是被封 IP,就是收到律师函警告。这篇就把我从"野蛮抓取"到"合规可控"的完整经验拆给你,覆盖合规边界、技术选型、基础抓取、JS 渲染处理、登录态、反爬应对和落坑清单。
一、合规边界:先别急着写代码
我实测过,90% 的爬虫新手翻车不是因为技术不行,而是因为没搞清"能不能爬"。合规不是选修课,是写爬虫的第一行代码。
先看 robots.txt。 大多数网站根目录下都有这个文件,比如 https://example.com/robots.txt,它用 User-agent 和 Disallow 告诉爬虫哪些路径禁止访问:
python
import urllib.request
# 读取目标站点的 robots 协议,先看清楚人家允不允许
robot_url = "https://example.com/robots.txt"
with urllib.request.urlopen(robot_url, timeout=5) as resp:
rules = resp.read().decode("utf-8")
print(rules) # 重点看 Disallow 行,命中的路径别碰
这几类数据,碰都别碰:
- 个人信息:手机号、身份证、住址、账号,未经授权抓取涉嫌侵犯公民个人信息;
- 受版权保护的内容:付费文章、付费课程、图库原图,批量搬运属于侵权;
- 绕过登录权限才能拿到的内容:人家设了墙,你非要去翻,性质就变了。
礼貌抓取是底线。 单 IP 每秒几十次请求,对方服务器会直接把你当攻击处理,我一般把间隔设在 1~3 秒并错峰限流。国内相关法律红线(如《数据安全法》《个人信息保护法》)不是摆设,量级一大、涉及个人信息、或对对方业务造成实质影响,风险就上来了。动手前先问自己三个问题:对方允许吗?数据敏感吗?我的抓取速率礼貌吗?
二、技术选型:四把工具怎么挑
不同场景用不同武器,别一上来就堆 Playwright,重得要命。
| 方案 | 适用场景 | 代价 |
|---|---|---|
| requests + BeautifulSoup | 静态页面、接口返回 HTML | 遇到 JS 渲染就抓瞎 |
| httpx | 需要异步、HTTP/2、更现代 API | 生态比 requests 略小 |
| Scrapy | 大型、需要调度/去重/管道的工程 | 学习曲线陡,太重 |
| Playwright | JS 渲染、需要点击/登录/截图 | 启动慢、资源占用高 |
我的经验:能用 requests 解决的绝不开浏览器。只有页面数据靠 JS 动态塞进去、或者必须模拟点击登录时,才上 Playwright。
三、requests 基础抓取实战
先打牢基础。下面这段是我最常用的"稳一点"写法:
python
import requests
# 用 Session 复用 TCP 连接,比每次 get 快很多
session = requests.Session()
# 伪装成正常浏览器,别用默认的 python-requests UA
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Encoding": "gzip, deflate", # 让服务器发压缩包,省流量
"Accept-Language": "zh-CN,zh;q=0.9",
}
try:
# 超时一定要设,不然一个卡死的请求能挂你一整晚
resp = session.get("https://example.com/list", headers=headers, timeout=10)
resp.raise_for_status() # 4xx/5xx 直接抛异常,别静默吞掉
print(resp.text[:200])
except requests.exceptions.RequestException as e:
print("请求失败,重试或跳过:", e)
超时与重试 建议配合 urllib3 的 Retry 或者自己写个简单退避循环,遇到 5xx 等几秒再试,别死磕。
编码与 gzip 坑 :requests 会自动解 gzip,但有时 resp.encoding 猜错导致乱码,手动指定更稳:resp.encoding = resp.apparent_encoding 或直接 resp.content.decode("utf-8")。
四、碰到 JS 渲染页面怎么办:上 Playwright
页面源码里根本找不到数据,全靠前端 JS 拉接口填 DOM,requests 只能干瞪眼。这时候我用 Playwright 同步 API:
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # 无头模式跑在服务器上
page = browser.new_page()
page.goto("https://example.com/spa", wait_until="networkidle")
# 关键:等某个选择器出现,数据才真正渲染好
page.wait_for_selector("div.goods-item", timeout=15000)
items = page.query_selector_all("div.goods-item")
for it in items:
print(it.inner_text())
# 截图存证,出了问题有据可查
page.screenshot(path="evidence.png")
browser.close()
我踩过这个坑:没写 wait_for_selector,截图是白屏,抓到的全是空。渲染类页面,等选择器比等固定时间靠谱得多。
五、登录态的两种获取方式
很多数据要登录才能看。两种方式我都试过:
- 方式 A:账号密码表单提交。 用
session.post带上表单字段。问题是现在很多站有验证码、加密参数、滑块,模拟登录极容易被风控。 - 方式 B(我推荐):手动登录后复制 cookie 粘贴。 自己在浏览器登录,打开开发者工具把 Cookie 字符串拷出来,塞进请求头。稳、省事、不被风控针对。
python
# 方式 B:把浏览器里复制的 cookie 直接用,最稳
headers = {
"User-Agent": "Mozilla/5.0 ...",
"Cookie": "sessionid=abc123; csrftoken=xyz; ...", # 从浏览器 Network 面板复制
}
resp = requests.get("https://example.com/ucenter", headers=headers, timeout=10)
六、反爬手段与应对清单
我整理了一份实战应对表:
| 反爬手段 | 表现 | 应对 |
|---|---|---|
| 频控 | 403/429 | 降速、加随机延时、退避重试 |
| IP 封禁 | 全部请求超时/拦截 | 代理池,但免费代理极不稳 |
| 字体反爬 | 数字/文字显示错乱 | 下载字体文件映射编码 |
| CSS 偏移 | 文字顺序被错位 | 还原 DOM 真实顺序再取 |
| 验证码 | 弹滑块/图文 | 打码平台或人工,别硬刚 |
| 签名参数 | XHR 带 token/sign | 浏览器抓包分析算法 |
关于代理:免费代理池我实测过,十个里有八个连不上或速度个位数,还经常混进蜜罐 IP。真要规模抓取,上收费代理并按量付费,比免费池靠谱一万倍。
七、数据解析与存储
解析优先用 lxml(比 BeautifulSoup 快),存数据用 pandas 直接落 CSV,量大再入库:
python
from lxml import html
import pandas as pd
tree = html.fromstring(resp.text)
rows = []
for li in tree.xpath('//ul[@class="list"]/li'):
rows.append({
"title": li.xpath("string(.//a)").strip(),
"link": li.xpath(".//a/@href")[0],
})
# 落地 CSV,小项目够用了
pd.DataFrame(rows).to_csv("data.csv", index=False, encoding="utf-8-sig")
入库(如 MySQL/SQLite)建议分批 executemany,别一条条插。
八、我踩过的高频坑
坑 1:中文乱码。 没设 encoding,CSV 用 Excel 打开全是乱码。解决:encoding="utf-8-sig",或 resp.encoding = "utf-8"。
坑 2:相对 URL 不处理。 抓到的 /a/b 直接当链接用,拼接后 404。解决:from urllib.parse import urljoin; urljoin(base, href)。
坑 3:页面结构和本地不一样。 本地跑得好好的,服务器上全空------大概率是 CDN 缓存了旧页面,或动了地域/登录态。先截图对比。
坑 4:并发爬太快被封。 上了多线程瞬间 429。并发数调小、加全局限速令牌桶。
坑 5:内存爆了。 一次性把所有数据塞列表再写。解决:边抓边写、分批落盘,或用生成器。
九、结尾:礼貌爬虫守则
爬虫这件事,技术只是工具,分寸才是门槛。我把日常执行的守则浓缩成一条:单 IP 请求间隔 ≥1 秒、只抓公开且不敏感的数据、尊重 robots 与对方服务器负载、出问题能立刻停。 今晚就把你正在跑的脚本加上限速和超时,再去翻一眼目标站的 robots.txt------这十分钟,比你要熬的无数个封 IP 的夜都值钱。