2. HTTP 协议与请求:请求方法、状态码、请求头与响应体解析
做数据采集,说白了就是跟 HTTP 打交道。你写的爬虫、调用的 API、甚至浏览器打开网页,底层都在走同一套协议。我刚开始接触数据采集时,总觉得 HTTP 是网络工程师的事,直到有一次抓取一个电商网站,对方返回了 403,我愣是排查了半天才发现是请求头少了一个 Referer。嗯,从那以后我再也不敢轻视 HTTP 了。
2.1 HTTP 协议的本质:你问我答
HTTP 是一种请求-响应协议。客户端发一个请求,服务端回一个响应。就这么简单。但简单背后藏着不少细节,咱们一个一个拆开看。
核心要点:HTTP 是无状态的。每次请求都是独立的,服务端不会记住你上次干了什么。这也是为什么后来有了 Cookie、Session 这些东西来弥补。
2.2 请求方法:你想对资源做什么?
HTTP 定义了多种请求方法,每种方法代表一种操作意图。数据采集中最常用的就几个,我列个表给你看。
| 方法 | 含义 | 数据采集中的用途 | 是否携带请求体 |
|---|---|---|---|
| GET | 获取资源 | 抓取网页、获取列表数据 | 否 |
| POST | 提交数据 | 登录、搜索、提交表单 | 是 |
| PUT | 更新资源 | 修改数据(较少用) | 是 |
| DELETE | 删除资源 | 删除操作(极少用) | 通常否 |
| HEAD | 获取响应头 | 检查链接有效性、获取文件大小 | 否 |
我个人习惯,写爬虫时 90% 的情况只用 GET 和 POST。GET 用来拿页面,POST 用来模拟登录或提交搜索条件。你想想看,如果对方网站是 RESTful 风格的 API,可能还会用到 PUT 和 DELETE,但普通网页抓取基本碰不到。
小技巧:用 HEAD 方法可以快速检查一个 URL 是否有效,而不需要下载整个页面内容。我在做大规模 URL 验证时,经常先用 HEAD 探路,能省不少带宽和时间。
2.3 状态码:服务器在告诉你什么?
每次请求,服务器都会返回一个三位数的状态码。这玩意儿就像交通信号灯,告诉你请求是顺利通过了,还是出了什么幺蛾子。
我把它分成五类,你记牢了,排查问题会快很多。
| 分类 | 范围 | 含义 | 常见状态码 |
|---|---|---|---|
| 1xx | 100-199 | 信息性响应 | 101(切换协议) |
| 2xx | 200-299 | 成功 | 200(OK)、201(Created) |
| 3xx | 300-399 | 重定向 | 301(永久)、302(临时)、304(未修改) |
| 4xx | 400-499 | 客户端错误 | 403(禁止)、404(未找到)、429(请求过多) |
| 5xx | 500-599 | 服务端错误 | 500(内部错误)、502(网关错误)、503(服务不可用) |
数据采集时,你最常碰到的几个状态码:
- 200:一切正常,放心解析。
- 301/302:页面搬家了。你的爬虫需要跟着跳转,否则拿不到真实内容。
- 304:内容没变,可以用缓存。我一般会利用这个做增量抓取。
- 403:被拒绝了。大概率是请求头不对,或者 IP 被封了。
- 404:页面不存在。可能是链接失效了。
- 429:请求太频繁,被限流了。这时候需要加延时。
- 500/502/503:服务器自己出问题了。等会儿再试。
注意:我曾经遇到过某个网站,明明页面正常,但返回的状态码是 302。原来它把所有请求都先重定向到一个中间页做统计,然后再跳回目标页。如果你的爬虫不处理重定向,就会一直拿到空数据。所以,写代码时一定要设置 allow_redirects=True(Requests 库默认就是 True)。
2.4 请求头:你的身份和意图
请求头是客户端发给服务器的元信息。服务器通过请求头来判断你是谁、想干什么、能接受什么格式的数据。
数据采集时,请求头是反爬虫的第一道防线。很多网站一看请求头不对,直接给你 403。我整理了一份常用请求头清单,你照着配基本不会出错。
| 请求头 | 作用 | 数据采集中的建议值 |
|---|---|---|
| User-Agent | 标识客户端类型 | 模拟真实浏览器,如 Chrome、Safari |
| Referer | 来源页面 | 模拟从上一页跳转过来 |
| Cookie | 携带会话信息 | 登录后获取,保持会话 |
| Accept | 可接受的响应格式 | text/html, application/json, */* |
| Accept-Language | 语言偏好 | zh-CN,zh;q=0.9 |
| Authorization | 身份认证 | Bearer token 或 Basic 认证 |
我个人习惯,写爬虫前先用浏览器开发者工具(F12)看一遍正常请求的请求头,然后照搬过来。尤其是 User-Agent 和 Referer,这两个最容易出问题。
避坑指南:我曾经因为 User-Agent 太老被网站识别为爬虫。后来我写了个 User-Agent 池,每次请求随机换一个,效果好了很多。另外,有些网站会检查 Accept-Encoding,如果你设置了 gzip,记得在代码里处理解压,否则拿到的是乱码。
2.5 响应体:你真正想要的数据
响应体就是服务器返回给你的实际内容。数据采集的目标,就是从响应体里提取出你需要的信息。
响应体的格式通常有这几种:
- HTML:网页结构,用解析库(如 BeautifulSoup、lxml)提取。
- JSON:API 接口返回,直接解析成字典或列表。
- XML:老式接口或 RSS 订阅,用 XML 解析器处理。
- 二进制:图片、视频、文件,直接保存成文件。
怎么判断响应体是什么格式?看响应头里的 Content-Type 字段。比如:
text/html→ HTMLapplication/json→ JSONimage/png→ 图片
下面是一个完整的 HTTP 请求-响应流程的示意图,帮你把整个流程串起来。
2.6 实战:用 Python 发起一个完整的 HTTP 请求
理论说完了,咱们来点实际的。下面是一个用 Python 的 requests 库发起 GET 请求的完整示例。
import requests
# 构造请求头
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/json,*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": "https://www.example.com/"
}
# 发送 GET 请求
url = "https://api.example.com/data"
response = requests.get(url, headers=headers, timeout=10)
# 检查状态码
if response.status_code == 200:
print("请求成功!")
# 判断响应格式
content_type = response.headers.get("Content-Type", "")
if "application/json" in content_type:
data = response.json() # 解析 JSON
print(data)
elif "text/html" in content_type:
html = response.text # 获取 HTML 文本
print(html[:200]) # 打印前 200 个字符
else:
# 二进制内容,保存文件
with open("output.bin", "wb") as f:
f.write(response.content)
elif response.status_code == 403:
print("被拒绝了,检查请求头或 IP 是否被封")
elif response.status_code == 429:
print("请求太频繁,需要加延时")
else:
print(f"请求失败,状态码:{response.status_code}")
个人经验:我写爬虫时,习惯把请求头和状态码检查封装成一个函数。这样每次请求都自动带上标准头,还能统一处理异常。另外,timeout 参数一定要设,否则遇到慢响应,你的爬虫会卡死在那里。
2.7 常见问题与排查思路
数据采集时,HTTP 层面的问题占了很大比例。我总结了几种常见场景和排查方法。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 返回 403 | 请求头不全或 IP 被封 | 检查 User-Agent、Referer;换代理 IP |
| 返回 302 但没拿到数据 | 未处理重定向 | 设置 allow_redirects=True |
| 返回 200 但内容是空 | 数据通过 JavaScript 加载 | 检查页面是否用了 AJAX;改用 Selenium |
| 返回 429 | 请求频率过高 | 增加延时,使用随机间隔 |
| 返回 500 | 服务器内部错误 | 稍后重试,或联系对方 |