1. HTTP协议基础:从0.9到3.0,我们到底在聊什么?
做数据采集这些年,我经常被问到同一个问题:「HTTP不就是发个请求收个响应吗?有什么好学的?」
嗯,这话对了一半。HTTP确实就干这两件事。但你要是真把它当黑盒用,踩坑是迟早的事。我见过太多采集脚本跑着跑着突然挂了,查半天发现是协议版本不兼容,或者没处理好连接复用。所以这一章,咱们先把地基打牢。
1.1 HTTP发展史:从一行文本到多路复用
HTTP从诞生到现在,经历了四个大版本。每个版本的出现,都是为了解决当时最头疼的问题。
| 版本 | 诞生年份 | 核心特点 | 我眼中的意义 |
|---|---|---|---|
| HTTP/0.9 | 1991 | 只有GET,无首部,无状态码 | 原型验证,简陋但够用 |
| HTTP/1.0 | 1996 | 引入首部、状态码、多种方法 | 真正可用的协议 |
| HTTP/1.1 | 1997 | 持久连接、管道化、分块传输 | 统治互联网20年 |
| HTTP/2 | 2015 | 二进制分帧、多路复用、头部压缩 | 性能飞跃 |
| HTTP/3 | 2022 | 基于QUIC、0-RTT、无队头阻塞 | 面向移动和弱网 |
HTTP/0.9 时代,请求就一行:GET /index.html。服务器直接返回HTML内容,没有首部,没有状态码。你想想看,连「404」都没有,出错了就是空响应。我刚开始学HTTP时翻到这段历史,觉得这哪是协议,就是个对讲机。
HTTP/1.0 终于像样了。它引入了 Content-Type、Status-Code,还支持了POST和HEAD。但有个致命问题——每个请求都要新建TCP连接。一个网页如果有10张图片,就得建立10次TCP连接。这在1996年还能忍,放到今天,光握手时间就够用户骂街了。
HTTP/1.1 是我用得最多的版本。它最大的贡献是持久连接(Connection: keep-alive)。同一个TCP连接可以发多个请求,不用反复握手。还加了管道化(Pipelining),理论上可以连续发请求不用等响应。但说实话,管道化在实际项目中几乎没人用——因为队头阻塞问题太严重了。我曾经在一个采集项目里试过管道化,结果一个慢请求堵住了后面所有请求,还不如老老实实排队。
HTTP/2 彻底改了玩法。它把文本协议变成了二进制协议,引入了多路复用。什么意思?就是多个请求可以在同一个TCP连接上同时传输,互不干扰。还做了头部压缩(HPACK),减少了重复数据。我在采集高并发API时,从1.1切到2,吞吐量直接翻了一倍。
HTTP/3 是最近的事。它把底层的TCP换成了QUIC(基于UDP)。为什么要换?因为TCP的队头阻塞是根子里的问题——丢一个包,后面所有数据都得等。QUIC解决了这个问题,还支持0-RTT连接,说白了就是「秒连」。对于移动端采集和弱网环境,这是大杀器。
核心结论: 做数据采集,HTTP/1.1 和 HTTP/2 是当前主力。HTTP/3 正在普及,但很多中间件和代理还不支持。我建议你至少掌握1.1和2的区别,因为很多反爬机制会利用协议版本做检测。
1.2 HTTP协议特点:无状态、无连接
这两个特点,是理解HTTP一切行为的基础。
无状态:服务器不记得你是谁。每次请求都是独立的,服务器不会自动关联你上一次干了什么。为什么会这样?因为设计初衷就是简单——服务器只负责「收到请求→返回响应」,不保存客户端上下文。这样服务器可以轻松横向扩展,扛住海量请求。
但无状态也带来了麻烦。比如你登录了淘宝,刷新一下页面,服务器又不认识你了。怎么办?用Cookie和Session。Cookie把状态信息存在客户端,每次请求自动带上。Session把状态存在服务器,通过Session ID关联。我在采集需要登录的网站时,最常干的事就是模拟Cookie,把登录后的凭证塞进请求里。
无连接:HTTP/1.0时代,每次请求结束后TCP连接就断了。好处是服务器不用维护大量空闲连接,节省资源。坏处是每次请求都要重新建立连接,效率低。HTTP/1.1用持久连接解决了这个问题,但「无连接」的思想依然影响着协议设计——服务器随时可以关闭连接,客户端要做好重连的准备。
避坑指南: 我曾经写过一个采集脚本,没处理连接被服务器主动关闭的情况。跑了半小时,突然所有请求都超时了。查了半天,发现是服务器在空闲60秒后自动断开了连接。解决方案很简单:加上重试机制,并在请求头里设置 Connection: keep-alive。
1.3 URL结构详解:拆开看,其实很简单
URL(统一资源定位符)是HTTP世界的地址。你每天在浏览器地址栏里敲的东西,拆开来看就几个部分。
https://user:pass@api.example.com:8080/path/to/resource?key1=val1&key2=val2#fragment
|__| |______| |_________________| |__| |_______________| |_________________| |______|
协议 用户信息 主机 端口 路径 查询参数 片段
| 组成部分 | 示例 | 说明 |
|---|---|---|
| 协议 | https | http 或 https,决定了数据传输方式 |
| 用户信息 | user:pass | 很少用了,现在基本用Authorization头 |
| 主机 | api.example.com | 域名或IP地址 |
| 端口 | 8080 | HTTP默认80,HTTPS默认443 |
| 路径 | /path/to/resource | 服务器上的资源位置 |
| 查询参数 | key1=val1&key2=val2 | 以?开头,&分隔键值对 |
| 片段 | #fragment | 不会发送到服务器,只在浏览器端使用 |
做数据采集时,我最关注的是查询参数。很多网站会把分页、排序、搜索条件放在这里。比如:?page=2&size=20&sort=time。我习惯用Python的 urllib.parse 来解析和构造URL,比手拼字符串靠谱多了。
注意: URL中的中文和特殊字符必须进行百分号编码(Percent-encoding)。比如中文「数据」会变成 %E6%95%B0%E6%8D%AE。我见过有人直接拼接中文URL,结果服务器返回400错误。记住:URL只支持ASCII字符集。
1.4 HTTP报文结构:请求行、状态行、首部、实体
HTTP报文就两种:请求报文和响应报文。结构非常对称,你记住一个,另一个就懂了。
请求报文
GET /api/data?page=1 HTTP/1.1 ← 请求行
Host: api.example.com ↓
User-Agent: Mozilla/5.0 ← 首部
Accept: application/json ↓
← 空行(必须)
← 实体(GET通常无实体)
请求行由三部分组成:方法、URI、协议版本。方法有GET、POST、PUT、DELETE等。URI是请求的资源路径(不含域名)。协议版本告诉服务器你用哪个版本的HTTP。
首部是键值对,每行一个。常见的如 Host、User-Agent、Cookie、Content-Type。首部可以分成几类:通用首部、请求首部、响应首部、实体首部。做采集时,User-Agent 和 Cookie 是最常需要伪造的。
空行是必须的。它用来分隔首部和实体。很多新手写HTTP请求时忘了加空行,结果服务器一直等实体,导致超时。我踩过这个坑,后来写代码时都会检查报文末尾有没有 \r\n\r\n。
实体是请求携带的数据。GET请求通常没有实体,POST请求把表单数据或JSON放在这里。
响应报文
HTTP/1.1 200 OK ← 状态行
Content-Type: application/json ↓
Content-Length: 123 ← 首部
Set-Cookie: session_id=abc123 ↓
← 空行(必须)
{"status": "ok", "data": [...]} ← 实体
状态行包含协议版本、状态码、状态描述。状态码是三位数字,1xx表示信息,2xx表示成功,3xx表示重定向,4xx表示客户端错误,5xx表示服务器错误。做采集时,我最怕遇到429(Too Many Requests),说明被限流了。
首部和请求报文类似,但多了 Set-Cookie、Cache-Control 等响应特有的字段。
实体就是服务器返回的实际数据。可能是HTML、JSON、图片二进制流等。采集时根据 Content-Type 决定怎么解析。
我的习惯: 每次写采集脚本前,先用浏览器的开发者工具(F12 → Network)看一遍目标接口的请求和响应报文。确认请求行、首部、实体都长什么样,再动手写代码。这能省掉80%的调试时间。
1.5 本章知识体系总览
下面这张图,把本章的核心知识点串在了一起。你可以把它当路线图用。
好了,HTTP协议的基础就讲到这里。记住:发展史让你知道「为什么这么设计」,特点让你理解「协议的天花板在哪」,URL和报文结构是你每天写代码都要面对的东西。把这些吃透了,后面讲请求方法、状态码、Cookie、代理的时候,你会觉得顺理成章。