Python 爬虫(一):先别急着写代码,搞懂 HTTP 再动手
Posted on Tue 13 October 2026 in 技术 • Sucre Liu
第一课不该是 requests.get
几乎所有爬虫教程都从这两行开始:
import requests
from bs4 import BeautifulSoup
html = requests.get(url).text
soup = BeautifulSoup(html, "html.parser")
然后初学者很快卡在"为什么拿到的内容和浏览器不一样"、"为什么一会儿就 403"。九成问题不在解析,而在 HTTP 层。 解析只是把已经拿到的 HTML 拆开;拿不到正确的 HTML,解析写得再漂亮也没用。
所以这个系列的第一篇不写解析器,先把 HTTP 讲清楚。本文基于 requests 2.33 与 httpx 0.28,Python 3.12。
HTTP 里你必须先看懂的几件事
状态码:它才是第一个信号
把响应当"文本"是最大的误区。先看状态码:
| 码 | 含义 | 爬虫里的常见原因 |
|---|---|---|
| 200 | 成功 | —— |
| 301 / 302 | 重定向 | 少了结尾斜杠、跳登录、跳地区 |
| 304 | 未修改 | 你带了 If-Modified-Since 等条件头 |
| 403 | 禁止 | UA/Referer 缺失、被 WAF 拦、需要 Cookie |
| 404 | 不存在 | URL 拼错或资源已删 |
| 429 | 请求过多 | 被限速了,看 Retry-After 头 |
| 5xx | 服务端错误 | 目标站故障或你触发了防护 |
一个负责任的习惯:先 raise_for_status(),再谈解析。 把 403 当成"HTML 不对"去调解析器,是在错误的地方花时间。
请求头不是装饰
服务器判断"你是不是正常用户",很大程度靠请求头。最少要带:
headers = {
"User-Agent": "Mozilla/5.0 (compatible; MySpider/1.0; +https://example.com/bot)",
"Accept-Language": "zh-CN,zh;q=0.9",
}
两点经验:
- 别伪装成某个具体浏览器又到处乱爬。更诚实的做法是在 UA 里留下你的项目名和联系方式——很多站点对"可联系"的 bot 更宽容。
- 需要登录态的接口,
Referer、Cookie、有时还有X-Requested-With缺一不可。用浏览器 DevTools 的 "Copy as cURL" 把真实请求抄下来,比猜头高效得多。
编码:text 不一定对
requests.get(...).text 会按响应头里的 charset 解码,响应头没写 charset 时它可能猜错,中文尤其容易变乱码。稳妥做法:
r = requests.get(url, timeout=10)
r.encoding = r.apparent_encoding # 或显式指定
html = r.text
# 或者干脆拿字节自己解:
html = r.content.decode("utf-8", errors="replace")
另外,gzip/deflate 默认会自动解压;br(Brotli)需要额外装 brotli,否则可能拿到二进制垃圾。
超时:不设就是等死
requests 默认没有超时——目标站不响应时,你的脚本会一直挂着。任何生产代码都必须显式设:
r = requests.get(url, timeout=(3.05, 10)) # (连接超时, 读取超时)
httpx 默认有 5 秒超时,但显式写出来更好。
同步还是异步:requests 与 httpx
先给结论:单机几百个页面以内,同步 + 连接复用就够;要并发几千个请求,再上异步。
requests 和 httpx 的基本用法几乎一样:
# requests
with requests.Session() as s:
r = s.get(url, headers=headers, timeout=10)
# httpx(同步接口同构)
import httpx
with httpx.Client(headers=headers, timeout=10, http2=True) as c:
r = c.get(url)
差异值得知道的:
Session/Client复用 TCP 连接,比每次requests.get新建连接快得多,也少触发风控。httpx原生支持异步(httpx.AsyncClient)和 HTTP/2(http2=True,需装h2)。HTTP/2 对多路复用和某些 CDN 更友好。requests的生态和第三方兼容更好;httpx的 API 更现代、更统一。两者都值得会。
并发不是越多越好。对目标站来说,你就是负载。 后文会专门讲并发与退避。
合规与边界:先想清楚再动手
这段不写就是耍流氓。技术能做,不等于可以随便做。
robots.txt:它是站点的"访问礼仪",不是法律条文。用标准库读一下,尊重 Disallow 是基本素养:
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://books.toscrape.com/robots.txt")
rp.read()
print(rp.can_fetch("MySpider", "https://books.toscrape.com/catalogue/page-1.html"))
比 robots 更重要的是这几条判断:
- 服务条款(ToS):很多站点在条款里明确禁止自动化抓取,违反可能构成违约。
- 版权与数据库权利:抓到的是别人的内容,转载/再发布是另一回事。
- 个人数据:涉及个人信息时,受《个人信息保护法》《GDPR》等约束——不是"公开可见"就等于"可以批量收集"。
- 频率:即使没写 robots,也不要对单站做高频请求。加
time.sleep(),别把对方打挂。 - 登录态与验证码:绕过验证码、破解登录,可能触及法律红线。这类场景,走官方 API 或直接放弃。
一句话:能拿到 ≠ 能合法使用。 系列里所有示例都使用公开的练习站点(books.toscrape.com、quotes.toscrape.com、httpbin.org),它们就是给人练爬虫的。
一个"负责任"的最小抓取器
把上面的点合起来,一个能直接用的骨架:
import time
import requests
from urllib.robotparser import RobotFileParser
BASE = "https://books.toscrape.com/"
UA = "MySpider/1.0 (+https://example.com/bot)"
def allowed(url: str) -> bool:
rp = RobotFileParser()
rp.set_url(BASE + "robots.txt")
try:
rp.read()
except Exception:
return True
return rp.can_fetch(UA, url)
def fetch(session: requests.Session, url: str) -> str | None:
try:
r = session.get(url, timeout=(3.05, 10))
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except requests.RequestException as e:
print(f"[warn] {url} -> {e}")
return None
def main():
with requests.Session() as s:
s.headers.update({"User-Agent": UA})
for page in range(1, 4):
url = f"{BASE}catalogue/page-{page}.html"
if not allowed(url):
print(f"[skip] robots 不允许: {url}")
continue
html = fetch(s, url)
if html:
print(url, "->", len(html), "chars")
time.sleep(1) # 限速:对目标站最基本的尊重
if __name__ == "__main__":
main()
它做了四件事:尊重 robots、显式超时、状态码检查、限速。这四点做到,你后面所有代码都建立在正确的 HTTP 认知上。
小结
- 拿不到数据,先看状态码和响应头,别急着调解析器。
- 请求头(UA/Referer/Cookie)、编码(
apparent_encoding)、超时(必须显式设)是最容易踩的三个坑。 Session/Client复用连接;httpx额外给你异步与 HTTP/2。- 合规先于技术:robots、ToS、版权、个人信息、频率,缺一不可;绕过验证码/登录的事不做。
- 用练习站起步,用"最小负责任抓取器"打底。
下一篇讲解析:CSS 选择器与 XPath 该怎么选,BeautifulSoup / lxml / parsel 各自适合什么场景,以及为什么"能解析"和"解析得稳"是两回事。