第一次接触爬虫的时候,我最大的困惑不是 Python 语法,而是完全看不懂浏览器地址栏里那一长串字符是什么东西,更搞不清楚我在地址栏按下回车之后,服务器到底是怎么把页面给我送回来的。后来才明白,想要写好爬虫、看懂抓包数据、排查报错原因,必须先弄清楚网页最基本的运行机制:URL 是什么,请求怎么发出去的,响应又是怎么回来的,以及状态码到底在说什么。这篇文章就从这几个最核心的概念入手,把这套流程彻底讲透,适合刚学 Python、刚开始碰爬虫的零基础读者。这一节不写代码,只讲底层的“网页工作原理”,但因为后面所有爬虫代码都建立在这套机制上,所以我会尽量把每个概念都掰开揉碎了讲,配合生活化的类比和实际抓包演示,确保你看完之后,再打开浏览器 F12 面板不至于一头雾水。
1. 网页交互的整体设计思路
1.1 浏览器和服务器之间到底在“聊”什么
每次你在浏览器里输入一个网址、按下回车,表面上看只是打开了一个页面,实际上背后发生了一次完整的“对话”。这个对话的双方,一边是你的浏览器(客户端),另一边是存放网页文件的那台计算机(服务器)。它们之间交流用的“语言”,就是 HTTP 协议,即超文本传输协议。
我用一个日常场景来类比:你去餐厅吃饭。你手里拿着一张写着详细说明的纸条,上面写着“某某饭店,202房间,鱼香肉丝一份,不要放香菜”,这就是 URL 加请求体。你把纸条递给柜台的服务员,服务员把它交给后厨,后厨做完菜之后,服务员端着菜走到你面前,菜盘上还附了一张小票写明了“菜做好了、用了12分钟、价格38元”,这就是响应。整个过程遵循固定的流程:先由你发起需求,然后服务端接收、处理、返回结果。爬虫做的事情,本质上就是把“你”这个角色换成了一段 Python 程序,由程序来代替人向服务器发起这些“点菜”请求。
这里有一个很多人容易忽略的点:HTTP 协议本身是无状态的。什么叫无状态?就是服务器默认不记得你上一次来过。你第一次请求页面,它给你返回内容;你再请求一次,它完全不记得“这个人刚刚来过”,所以每次请求都得带上完整的上下文信息。这就是为什么后面我们爬虫需要处理 Cookie、处理会话(Session)——就是为了让服务器把多次请求“认成”同一个人发起的。这一节先记住这个结论,后面需要保持登录状态爬数据时会频繁用到。
1.2 为什么爬虫入门必须先学这套机制
很多初学者会问:我会用 requests 库调几个接口不就能爬虫了,为什么要先学 URL、请求、响应这些概念?我的经验是,爬虫不同于普通的 Web 开发,它是反过来利用 Web 机制来获取数据。如果你不理解请求和响应的对应关系,后面遇到的几个典型问题根本没法定位:
第一,不知道数据藏在哪。一个页面里,有的是服务端渲染直接返回 HTML 内容,有的是前端异步加载接口返回 JSON 数据。不懂请求和响应的结构,你会在 HTML 里翻半天也找不到自己要的数据,其实数据早就通过另一个接口响应返回了,只是你根本不知道去抓那个网络请求。
第二,报错时无从下手。爬虫最常见的报错无非是访问被拒(403)、页面不存在(404)、服务器错误(500)、请求太频繁(429)。这些本质上都是状态码告诉你的信息。懂状态码,一眼就能判断问题是出在“链接写错”“需要登录”还是“IP 被限制”。不懂状态码,就只能百度报错信息,找了半天也找不到重点。
第三,不会伪装请求。反爬机制的第一步通常就是看请求头(Headers)是否符合浏览器的特征。比如 User-Agent、Referer、Cookie 这些字段,都是需要你在代码里主动带上的。如果不知道请求头里有哪些字段、服务器拿这些字段干什么用,你的爬虫请求和真实浏览器之间有啥差别,你根本无从判断。
所以这一章的内容虽然看起来不像“写代码”,但它决定了你后面写的代码能不能稳定跑通。我见过太多跳过了网页基础直接写爬虫代码的人,最后回来补课的时候才恍然大悟:原来当初报错的原因早就写在状态码里了,只是自己看不懂而已。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. URL 解析:网页的“门牌号”是怎么设计的
2.1 URL 的五个基本组成部分
URL(Uniform Resource Locator,统一资源定位符)是互联网资源的地址。你可以在浏览器的地址栏里输入一个完整的 URL 来定位一个唯一的资源。一个完整的 URL 通常由协议、域名、路径、查询参数这几个部分组成,我拆开一个一个说。
拿一个典型的地址举例:
text复制https://www.example.com:443/products/list?category=phone&page=2#top
- 协议(Scheme):比如
https://,它决定了浏览器用什么方式去沟通。最常见的两种是 HTTP 和 HTTPS,HTTPS 是加密版的 HTTP,两者底层机制是一样的,只是数据在传输过程中做了加密。 - 域名(Host/Domain):比如
www.example.com,它是一台服务器的“人类可读”名字。因为计算机之间通信靠的是 IP 地址(一串数字,比如 192.168.1.1),但让人去记数字太困难了,所以有了域名这种方便记忆的别名。域名通过 DNS(域名系统)解析成 IP 地址。 - 端口(Port):比如
:443,它是一台服务器上的“门牌号”,用来区分这台机器上不同的服务。HTTP 默认端口是 80,HTTPS 默认端口是 443。如果你的地址里没写端口,浏览器会自动用默认端口。 - 路径(Path):比如
/products/list,它表示这台服务器上的具体资源位置,相当于“哪个功能区,哪个文件”。 - 查询参数(Query String):比如
?category=phone&page=2,它通常以?开头,后面跟一组key=value键值对,多个参数之间用&连接。这部分用来告诉服务器“我想要什么样的数据”。注意它本质上是字符串,没有层级含义,所以?page=1和?page=2是同一个接口获取不同页的数据。
还有一个容易被忽略但很常见的部分是锚点(Anchor),也就是 URL 末尾 # 后面的内容,比如 #top。锚点通常用来定位到页面内部的某个位置,它不会发送给服务器,只是浏览器内部用的定位标记。做爬虫的时候,通常不需要关心锚点。
关于 URL,爬虫初学者最容易犯的一个错误是:查询参数部分漏掉了、顺序不对、或者复制的时候被截断了。很多网站会根据查询参数的不同返回不同的数据,哪怕只是少了 page=2 这个参数,你也拿不到第二页的内容。所以在写爬虫的时候,我建议先完完整整把浏览器地址栏里的 URL 复制下来,再对着它的结构去分析哪些参数是固定的、哪些参数是需要动态生成的(比如时间戳、签名)。
2.2 URL 编码:为什么地址栏里全是百分号
还有一个很常见的情况:你在浏览器地址栏或者抓包工具里看到一些 URL 里有 % 符号和一堆数字字母组合,比如 %E4%B8%AD%E6%96%87。这就是 URL 编码,也叫百分号编码。
因为 URL 本身只能安全地传输一部分 ASCII 字符,像中文、空格、特殊符号(比如 &、= 已经有了特殊作用)不能直接放在 URL 里,所以需要把它们转换成安全格式。转换规则就是把字符的字节值用十六进制表示,前面加一个 %。比如“中文”这两个字的 UTF-8 编码就是 %E4%B8%AD%E6%96%87。
这跟我上一小节说的查询参数有什么关系?关系很大。你请求一个搜索接口,搜索关键词是“手机”,如果直接在 URL 里写中文,有的服务器可能不认,所以得先编码成百分号形式。Python 的 requests 库会自动帮你处理大部分编码工作,但当你自己拼 URL 的时候,得用 urllib.parse.quote 或者 urlencode 来处理用户输入部分,避免因为编码问题导致请求失败。后面到实战部分我会单独演示,这里先建立一个概念。
3. HTTP 请求:客户端到底在“要”什么
3.1 请求方法 GET 和 POST 的本质区别
一个 HTTP 请求,首先要明确“你想干什么”,这个动作就叫请求方法(HTTP Method)。HTTP 协议里定义了一组方法,普通爬虫用得最多的是两个:GET 和 POST。
GET 的语义是“获取资源”。你想从服务器拿数据,服务器不关心你拿数据这件事本身有没有副作用,只负责返回对应内容。GET 请求的参数通常直接放在 URL 的查询字符串里,所以它有几个特点:参数会暴露在地址栏上、有长度限制(浏览器和服务器通常有限制,虽然 HTTP 协议本身没强制)、相对更适合拿不敏感的数据。
POST 的语义是“提交数据给服务器处理”。它可以做登录认证、提交表单、上传文件等操作。POST 请求的参数不是放在 URL 里,而是放在请求体(Body)里,所以从表面上看,地址栏不会显示出你提交了什么数据。但请注意,POST 并不等于“安全”,因为请求体照样可以通过抓包工具直接看到,只是它在传输过程中不是明文显示在 URL 里而已。
我做一个简单对比:
| 维度 | GET | POST |
|---|---|---|
| 语义 | 获取数据 | 提交数据处理 |
| 参数位置 | URL 查询参数 | 请求体(Body) |
| 参数可见性 | 地址栏可见 | 地址栏不可见 |
| 使用场景 | 翻页、搜索、拉取列表 | 登录、提交表单、上传文件 |
| 是否改变服务器数据 | 通常不会 | 通常会改变 |
实际操作中的经验是:同一套接口,GET 能改成 POST 吗?不一定,得看服务器怎么写的。有些服务器接口写死了只接受 GET,你用 POST 去请求会直接报 405 Method Not Allowed;反过来也一样。所以调接口之前,先打开浏览器开发者工具,看这个接口实际上是用什么方法发的,照抄就行。后面我会详细演示怎么在浏览器里查看。
除了 GET 和 POST,还有 PUT(更新资源)、DELETE(删除资源)、HEAD(只获取响应头,不获取响应体)等。做爬虫时,PUT 和 DELETE 很少用到,主要是某些需要操作数据的接口才会涉及。HEAD 请求偶尔会用,比如批量检测 URL 是否有效时,只取响应头能省不少流量,速度快很多。
3.2 请求头的关键字段与爬虫伪装
HTTP 请求除了方法、URL 之外,还有一个非常关键的部分叫请求头(Headers)。请求头是一堆“元数据”,用来描述请求本身的信息,比如客户端是什么软件、支持什么格式、当前是什么环境等等。爬虫能不能成功,很大程度上取决于请求头伪装得像不像真实浏览器。
下面几个请求头字段是我每次写爬虫都会优先检查的:
- User-Agent(UA):就是“我是谁”的自我介绍。浏览器会带上自己的 UA,比如 Chrome 的 UA 里包含
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36。而 Python requests 库默认的 UA 是python-requests/2.31.0,服务器一眼就能识别出这不是浏览器。所以写爬虫的第一件事就是改 UA,改成浏览器的样子,不然很容易被拒。 - Referer:表示“你从哪个页面跳过来的”。有的服务器会校验这个字段,如果你直接访问一个资源而没带正确的 Referer,服务器会拒绝返回。常见场景是防盗链,比如某些图片、视频资源,必须从指定页面跳转才能访问。
- Cookie:表示“你是谁,你之前做过什么”。Cookie 是服务器下发给客户端的一段小数据,客户端保存后在后续请求中自动携带,用来维持会话状态。爬虫想保持登录状态,就是在请求头里带上登录后拿到的 Cookie。
- Accept / Accept-Language:告诉服务器你能接收什么格式的数据、偏好的语言是什么。大多数情况下,这些字段可以用浏览器默认值,减少了被识别为脚本的嫌疑。
经验之谈:请求头不是越多越好,关键是跟真实浏览器保持一致。有些新手会复制一大串请求头粘贴到代码里,但这反而可能因为某些字段跟请求内容不一致而被拦截。正确做法是用浏览器开发者工具复制该请求的实际请求头,然后只挑必要的字段设置到代码里:UA、Referer、Cookie 这三个优先级最高。
3.3 请求体:POST 数据放哪里
当你用 POST 方法提交数据时,数据放在请求体里。请求体有几种常见的格式,这决定了你解析和构造数据的方式:
- application/x-www-form-urlencoded:最常见的表单格式,数据形如
key1=value1&key2=value2,跟 URL 查询参数的格式一样。 - application/json:JSON 格式,数据形如
{"key1":"value1","key2":"value2"},现在很多 API 接口喜欢用这种格式。 - multipart/form-data:主要用于文件上传,数据里除了字段之外还包含文件二进制内容,会用一段随机分隔线来分隔不同字段。
构造 POST 请求时,如果格式对不上,服务器解析不到正确字段,就会返回参数缺失或请求失败。比如接口要求 JSON 格式,你偏给它表单格式,服务器可能压根读不出来。在 Python 的 requests 库中,用 data 参数传的是表单,用 json 参数传的是 JSON,用 files 参数传文件,这个选择要符合接口的实际情况。
4. HTTP 响应:服务器到底“回”了什么
4.1 响应体与响应头的结构
服务器处理完请求之后,会返回一个 HTTP 响应。响应也分两部分:响应头和响应体。
响应头里有一堆描述性信息,其中最常见的几个字段有:Content-Type(内容类型,告诉客户端怎么解析响应体)、Content-Length(内容长度)、Set-Cookie(服务器下发 Cookie 给客户端)、Location(发生重定向时,告诉客户端去新的地址)。做爬虫时,Content-Type 尤其重要。如果响应头里的 Content-Type 是 text/html,说明返回的是 HTML 页面;如果是 application/json,说明返回的是 JSON 数据。很多新手拿回来的数据明明长着一副 JSON 的样子,却因为用错了解析方式而报错,这时候看一眼响应头就能定位。
响应体就是服务器真正返回的数据内容,可能是一个完整的 HTML 页面,也可能是一段 JSON 字符串,也可能是图片、视频等二进制流。爬虫的主要工作,就是拿到响应体之后,根据内容格式去提取里面的目标数据。
4.2 状态码速查:每个爬虫都必须掌握的“暗号”
状态码是服务器在响应中返回的三位数字,用来概要地告诉你这次请求的结果。它的设计有点像餐厅服务员喊号:2 开头就是“好的,马上好”,3 开头是“去旁边那个窗口”,4 开头是“你点的菜有问题”,5 开头是“后厨出事儿了”。我整理了一个爬虫高频状态码速查表:
| 状态码 | 含义 | 爬虫常见触发场景 |
|---|---|---|
| 200 | 请求成功,正常返回内容 | 正常访问 |
| 301 | 永久重定向 | 网站换了新域名,旧地址自动跳转 |
| 302 | 临时重定向 | 未登录访问某些页面,跳到登录页 |
| 304 | 未修改,使用缓存 | 资源没变化,直接读本地缓存 |
| 400 | 请求语法错误 | 参数格式不对、编码错误 |
| 401 | 未认证 | 需要先登录,Token 缺失或过期 |
| 403 | 禁止访问 | IP 被封、UA 被检测、无权限 |
| 404 | 资源不存在 | 路径写错、资源被删除 |
| 405 | 方法不被允许 | 该用 POST 你用了 GET,或反之 |
| 429 | 请求过多,被限流 | 请求频率太高,触发风控 |
| 500 | 服务器内部错误 | 服务器自己出问题 |
| 502 | 网关错误 | 服务器作为代理时上游出错 |
| 503 | 服务不可用 | 服务器过载或正在维护 |
我个人的经验是:爬虫遇到 200 才是开始,遇到 403/429 才是真正的技术活。403 通常意味着你的请求被识别为机器人,需要检查 UA 和 Cookie;429 则明确告诉你“访问太快了”,解决办法是降低请求频率、加延时、用代理池。很多人一看到 403 就慌了,实际上只要肯看状态码、肯分析原因,大多数反爬拦截都是可以逐步绕过的。
另外要注意:状态码是 HTTP 层面的,业务层面的“成功”不一定等于 HTTP 200。有些接口即使请求处理失败,也会返回 HTTP 200,但在响应体的 JSON 里放一个业务状态码,比如 "code": 5001 或 "success": false。这种接口在业务上叫“伪 200”,需要你把响应体完整打印出来,结合业务字段来判断真正的成功与否。这是爬虫领域一个非常经典的坑。
5. 完整请求一瞥:从浏览器地址栏到爬虫代码
5.1 用浏览器开发者工具看一次真实请求
讲了这么多理论,下面我带你实操一次,看一下真实的请求长什么样。这个方法以后会天天用到,比任何教程都实在。
打开 Chrome 浏览器,按 F12 打开开发者工具,切到“Network”(网络)标签页,然后在地址栏输入一个网址并回车。你会看到 Network 面板里刷出了一堆请求记录,每条记录对应一个 HTTP 请求。点击任意一条,右侧或下方会展开详情,里面有完整的 Headers(请求头)、Payload(请求参数)、Response(响应体)信息。
真正抓请求的时候,我建议用“Preserve log”(保留日志)和过滤功能。因为一个页面会发起几十个请求,有 HTML、CSS、JS、图片、接口,你只关心数据接口的话,可以在过滤框里输入 Fetch/XHR 类型,这类请求通常是异步加载数据的接口,里面往往藏着页面显示的核心数据。比如一个商品列表页,页面上的商品数据可能就是通过某个 JSON 接口返回的,你找到它,分析它的请求参数,就能直接用 requests 模拟它。
5.2 用 curl 和 Python 复现一次请求
找到目标请求之后,在 Network 面板里右键点击该请求,选择“Copy”再选“Copy as cURL”,就能得到一条完整的 curl 命令。curl 是一个命令行工具,可以用这条命令直接复现刚才的请求。然后你可以再打开 Python 的 requests 库,把 curl 命令转换成 Python 代码。
这里我给一个最简的 Python 请求示例,注意这一节讲的是原理,代码只是用来验证你刚刚学到的概念:
python复制import requests
url = "https://httpbin.org/get" # 一个测试接口,会原样返回请求信息
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"
}
response = requests.get(url, headers=headers)
print("状态码:", response.status_code)
print("响应头 Content-Type:", response.headers.get("Content-Type"))
print("响应体:", response.text[:500])
运行这段代码,你就能看到一次真实的 HTTP 请求和响应:状态码是 200,响应头里带着内容类型,响应体是一段 JSON。把这段代码里拿到的信息跟 F12 里看到的信息做对比,你对请求、响应、状态码的感觉一下子就落地了。
5.3 状态码在爬虫中的应用
上面代码里打印出来的 status_code 就是状态码。在真正的爬虫代码里,状态码通常用来做逻辑判断:如果返回 200,继续解析数据;如果返回 403,可能触发 IP 封禁,需要换代理;如果返回 429,需要睡一会儿再重试;如果返回 404,说明资源不存在,直接跳过。这种判断逻辑叫“响应处理策略”,怎么写合适取决于目标网站的反爬强度和数据重要性,但核心思路就是:不要盲目继续往下解析,先确认请求确实成功了。
我习惯在自己写爬虫的时候,把状态码和响应体的前半段都打进日志里。这样就算程序跑到一半崩了,我翻日志也能快速定位是哪一个请求出了问题、响应长什么样。这个习惯帮我省了大量排错时间。
6. 常见问题与排查技巧实录
6.1 新手最容易踩的四个坑
结合我带爬虫新手的经验,下面这几个坑几乎人人都会踩一遍,提前打个预防针:
第一个坑,状态码 200 但拿不到数据。 这种情况十有八九是数据根本不是这个请求返回的,而是另一个异步接口。排查方法:在 F12 的 Network 面板里把所有 XHR 请求都过一遍,找到真正返回目标数据的那个接口。同理,如果接口响应体是一大段 HTML 里藏着几行数据,可能是服务端渲染,可以直接解析 HTML;如果数据是 JSON 结构,就要直接请求那个 JSON 接口。
第二个坑,忘记带请求头被服务器识别出来。 最典型的就是 UA 没改,Python requests 默认的 UA 太显眼。解决方法是把浏览器的完整 UA 复制过来,设到 headers 里。另外注意有些接口还校验 Referer 和 Origin,如果只改了 UA 还是被拒,再把这两个字段补上。
第三个坑,编码问题导致乱码。 响应体里如果有中文,直接 .text 打印出来可能是乱码。原因是服务器返回的编码和 requests 猜测的编码不一致。解决办法是检查响应头里的 charset 字段,或者直接用 response.encoding = "utf-8" 手动指定编码。如果还不行,用 response.content 拿到字节流,再按正确编码 decode。
第四个坑,大模型和爬虫新手都容易忽略的 Cookie 过期问题。 如果网站需要登录,你复制了登录后的 Cookie 写死在代码里,一开始能跑,过了一段时间突然返回 401 或 302(跳转登录页),大概率就是 Cookie 过期了。解决思路是维护一个 Cookie 池,或者在代码里实现自动登录。这部分内容后面章节会展开。
6.2 状态码异常排查速查表
这里整理了一张更实用的排查表,专门针对爬虫运行时的状态码异常,你可以截图保存:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 返回 403 | 被识别为爬虫/无权限 | 检查 UA、Referer,尝试带 Cookie |
| 返回 429 | 请求太频繁被限流 | 增加延时,降低并发,换代理 IP |
| 返回 302 到登录页 | 需要登录才能访问 | 模拟登录,带上 Session/Cookie |
| 返回 404 | URL 不对或资源删除 | 核对地址、路径、查询参数 |
| 返回 502/503 | 目标服务器异常或反爬网关拦截 | 稍后重试,检查代理是否正常 |
| 响应乱码 | 编码判断错误 | 设置 response.encoding 或用 stream 处理 |
| 请求超时 | 目标服务器响应慢/网络问题 | 增加 timeout,设置重试机制 |
再说一个很多人不知道的细节:状态码 200 不代表内容里没有“空”。有些反爬系统会在返回 200 的同时,给你一个验证页面或者一个空列表,这是“软封禁”。所以爬虫代码里最好不仅检查状态码,还要检查响应体内容里有没有出现诸如“验证码”“访问异常”这样的关键词。一旦出现,立刻停止请求,检查是不是 IP 已被标记或需要更换策略。
6.3 用请求响应机制解决实际问题的思路
学完这套请求响应机制之后,你会发现自己排查问题的思路完全不一样了。以前看到一个爬虫报错,第一反应是去百度复制报错信息;现在你会自然地按照这个顺序做判断:先看状态码是哪一类,再点开响应体看看里面有什么,最后回头检查自己发出的请求头、请求参数、请求方法是不是和目标接口一致。这个排查链路的底层逻辑,就是你在这一节学到的请求和响应在 Web 交互中的分工。
我自己刚学爬虫的时候,曾经花了一整晚查一个“明明浏览器能打开,但 Python 请求却 403”的问题。当时我把报错信息翻来覆去看了好多遍,完全没头绪。后来静下来用 F12 对比了一下浏览器请求和 Python 请求的差异,发现只是少了一个 User-Agent 头。加上之后,请求秒通。这个经历让我深刻地意识到:爬虫的本质不是“写代码”,而是“模拟真实用户的请求”,你对 HTTP 请求响应的理解越深,写爬虫的速度和稳定性就越高。
这一节的内容偏基础,但它是后续所有爬虫实战的地基。下一小节我会顺着这个思路,展开讲 HTML 和 DOM 结构,教你怎么从响应体里精准提取想要的数据。我个人的体会是,与其急着写很多看似炫酷的爬虫代码,不如先把 URL、请求、响应、状态码这几个概念吃透,后面你再看任何爬虫教程,都会觉得顺很多。
