浏览器会发出哪些请求,抓包和爬虫时应关注什么
1. 浏览器请求的分类方式
1.1 不要只按 DevTools 标签理解请求
面试里常见误区,是把 xhr、fetch、document、websocket 都当成同一层级的“请求类型”。
实际上它们混合了 发起方式、资源目标、协议形态 这三类维度,所以抓包时容易看乱。
更稳妥的理解方式,是把浏览器请求拆成三层:
- 导航类请求:例如地址栏输入 URL、点击链接、表单跳转,目标通常是
document。 - 子资源请求:例如
script、style、image、font、media,它们服务于页面渲染。 - 编程式请求:例如
XMLHttpRequest、fetch()、EventSource、WebSocket、sendBeacon(),它们通常由 JavaScript 主动发起。
Fetch 标准里的 Request.destination 提供了一个很好的统一视角。
它把请求目标描述为 document、script、style、image、font、worker、manifest 等;而 fetch()、XMLHttpRequest、EventSource、WebSocket、navigator.sendBeacon()、<a ping> 这类请求,destination 通常是空字符串 ""。
1.2 DevTools 常见标签与底层语义映射
不同浏览器的开发者工具命名略有差异,但底层语义大致一致。抓包时可以先做如下映射:
| 面板常见标签 | 底层语义 | 常见触发方式 | 对爬虫价值 |
|---|---|---|---|
Doc / document |
页面导航请求 | 输入 URL、点击链接、iframe 加载 |
很高 |
Fetch/XHR |
JavaScript 发起的数据请求 | fetch()、XMLHttpRequest |
很高 |
JS / script |
脚本资源 | <script>、动态导入 |
中 |
CSS / style |
样式资源 | <link rel="stylesheet"> |
低 |
Img / image |
图片资源 | <img>、CSS 背景图 |
低 |
Font |
字体资源 | @font-face |
低 |
Media |
音视频资源 | <video>、分片流媒体 |
视场景而定 |
WS |
WebSocket 握手与帧 |
new WebSocket() |
高 |
Manifest |
PWA 清单 | <link rel="manifest"> |
低 |
Other |
其它杂项 | sendBeacon()、ping、报告上报等 |
通常低 |
结论是:抓包时不要先盯“数量最多的请求”,而要先盯“真正承载业务数据的请求”。
对大多数业务站点来说,优先级一般是 document、Fetch/XHR、WS、EventSource,而不是图片和字体。
2. 浏览器常见请求类型详解
2.1 导航请求 document
导航请求是浏览器最基础的一类请求。
它由地址栏输入、点击 <a>、window.location 跳转、iframe 加载、表单提交跳转等动作触发,服务端通常返回 HTML 文档。
这类请求对爬虫非常关键,原因有三点:
- 首屏数据可能直接写在 HTML 中,根本没有额外的
XHR或fetch。 - 页面里可能内嵌了初始化状态,例如
__INITIAL_STATE__、window.__DATA__、script[type="application/json"]。 - 首次响应往往会下发登录态、
Set-Cookie、防重放令牌、csrf token等后续请求依赖的信息。
如果一个站点是传统的服务端渲染,也就是 SSR,那么 真正的数据可能就在 document 响应体里。
这时只盯 Fetch/XHR 会得出“页面没有接口”的错误结论。
2.2 静态子资源请求:script、style、image、font、media
浏览器解析 HTML 和 CSS 之后,会继续请求脚本、样式、图片、字体、音视频等子资源。 这些请求的主要目标是“把页面渲染出来”,而不是“把业务数据返回给你”。
从爬虫角度看,它们通常分成两类:
- 大多可忽略:
style、image、font通常不承载结构化业务数据。 - 有时必须分析:
script里可能藏着接口地址、签名算法、分页参数拼接逻辑;media可能对应分片流,例如m3u8、ts、mp4分段。
很多视频站或音频站还会发出 Range 请求,只取资源的一部分。
如果看到请求头里有 Range: bytes=...,说明它不是普通文本接口,而更像是大文件或流媒体的分片读取。
2.3 XMLHttpRequest 请求
XMLHttpRequest,也就是常说的 XHR,是早期 Ajax 的核心 API。
它允许页面在不整页刷新的情况下,向服务端发起 HTTP 请求并读取响应,因此经常被用来加载列表、搜索结果、评论、详情数据。
XHR 的几个关键点:
- 它是 事件驱动模型,而不是
Promise模型。 - 它支持下载和上传进度事件,所以一些老项目上传文件时仍然常见。
- 它曾支持同步请求,但同步
XHR会阻塞主线程,现代前端通常应避免。
最小示例:
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/list?page=1", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
抓包时,XHR 往往是最值得看的请求之一。
尤其是老站、后台管理系统、jQuery 项目、历史包袱较重的业务,仍然会大量使用它。
2.4 fetch() 请求
fetch() 是现代浏览器更推荐的请求 API。
它和 XHR 一样可以发 HTTP 请求,但模型更贴近现代 JavaScript:基于 Promise,并且与 Service Worker、Streams、CORS 等机制结合得更自然。
fetch() 的几个高频考点:
fetch()返回的Promise,在 收到响应头 时就会兑现,不必等整个响应体读完。- HTTP
404、500这类业务失败,不会让Promise自动reject;它只在网络错误、请求构造失败等情况下reject。 - 如果要中止请求,通常配合
AbortController使用。
最小示例:
const controller = new AbortController();
const resp = await fetch("/api/list?page=1", {
method: "GET",
credentials: "include",
signal: controller.signal,
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
const data = await resp.json();
console.log(data);
在现代 SPA、React、Vue、Next.js、Nuxt 等应用里,业务数据接口大多优先使用 fetch()。
所以抓包时看到 Fetch/XHR,里面非常大一部分实际就是 fetch() 请求。
2.5 表单提交请求
表单提交是很多人抓包时容易忽略的一类请求。
登录、注册、搜索、筛选、上传文件、导出报表,仍然大量依赖 <form> 提交。
常见表单编码方式有三种:
application/x-www-form-urlencodedmultipart/form-datatext/plain
这类请求的几个重点是:
- 文件上传几乎一定要看
multipart/form-data的边界和字段名。 - 登录表单通常包含隐藏字段,例如
csrf token、nonce、一次性验证码票据。 - JavaScript 调用
form.submit()和用户真正点击提交按钮,并不完全等价;前者不会触发submit事件和约束校验。
2.6 OPTIONS 预检请求
当页面发起跨域请求,并且该请求不属于“简单请求”时,浏览器会先自动发一个 OPTIONS 请求做 CORS preflight。
这个请求不是业务方手写的,而是浏览器为了确认“目标站是否允许这样跨域访问”自动插入的。
典型预检请求会带上这些头:
OriginAccess-Control-Request-MethodAccess-Control-Request-Headers
抓包和复现时,预检请求的意义是:
- 它告诉你:真正的业务请求是跨域的,而且用了特殊方法或特殊头。
- 但它本身通常不是你要复现的数据接口。
- 很多后端脚本、服务端爬虫直接请求接口时,不会自动遭遇浏览器同源策略,因此也未必需要自己先发
OPTIONS。
所以不要把“只复现了 OPTIONS”误认为“接口已经调通了”。
真正有业务价值的,仍然是后面的实际请求。
2.7 WebSocket
WebSocket 适合做长连接、双向通信。
它一开始通过 HTTP 发起握手,随后协议升级为 WebSocket,不再是传统的一问一答式 HTTP 请求。
典型握手特征是:
- 请求里带
Upgrade: websocket - 服务端返回
101 Switching Protocols - 后续真正的数据在帧里收发,而不是普通 HTTP 响应体里
最小示例:
const ws = new WebSocket("wss://example.com/socket");
ws.addEventListener("open", () => {
ws.send(JSON.stringify({ op: "subscribe", roomId: 123 }));
});
ws.addEventListener("message", (event) => {
console.log(event.data);
});
如果页面数据是实时刷新的,例如聊天室、行情、推送通知、直播间弹幕,那么 真正的数据流很可能不在 Fetch/XHR,而在 WebSocket 帧里。
这时只看握手 URL 还不够,必须继续看发送帧和接收帧。
2.8 EventSource 与 SSE
EventSource 对应的是 Server-Sent Events,简称 SSE。
它基于普通 HTTP 连接,但响应体是持续不断推送的 text/event-stream,属于“服务端单向推送”。
它和 WebSocket 的区别很明确:
SSE是 单向 的,只能服务端推给客户端。WebSocket是 双向 的,双方都可以主动发消息。
如果页面看起来在自动更新,但没有发现明显的轮询 XHR 或 fetch(),就要检查是否存在 EventSource。
很多监控面板、实时状态页、日志流页面会用这种方式。
2.9 navigator.sendBeacon() 与 <a ping>
这两类请求通常是埋点和统计噪声。 它们的价值不在业务数据,而在“告诉服务端用户发生了什么动作”。
navigator.sendBeacon() 的特点:
- 只能异步发送一个小型
POST请求。 - 常用于页面卸载、隐藏、跳转时上报埋点。
- 调用方通常不关心响应内容。
<a ping> 的特点:
- 用户点击链接时,浏览器会额外向
ping指定的地址发送POST请求。 - 典型用途是点击统计、广告归因、跳转链路埋点。
对爬虫来说,这类请求大多可以忽略。 但它们会污染抓包视图,所以识别出来很重要,避免误把埋点接口当成业务接口。
2.10 预加载与投机请求:preload、modulepreload、prefetch、preconnect、dns-prefetch
这类请求不是为了立即展示业务数据,而是为了优化性能。 它们的目标是“提前做准备”,所以抓包时很容易和真实业务请求混淆。
几个概念需要分清:
preload:提前下载当前页面很快就要用到的资源,例如脚本、样式、字体、图片。modulepreload:提前获取、解析、编译模块脚本及其依赖。prefetch:低优先级预取未来可能用到的资源,常用于“下一页可能会访问”的内容。preconnect:提前做DNS、TCP、TLS握手,重点是“连上”,不一定立刻下载资源。dns-prefetch:只提前做域名解析,不会直接发 HTTP 资源请求。
因此:
preload、modulepreload、prefetch可能在Network面板里看到真实资源请求。preconnect更像“连接预热”。dns-prefetch甚至可能根本不表现为普通 HTTP 请求。
如果你在抓包时看见页面一打开就先去请求几个脚本、字体、下一页资源,不要急着判断那就是核心业务接口。 先确认它到底是“渲染资源”,还是“真实数据接口”。
2.11 其它容易忽略的请求
还有几类请求也会在真实项目里出现:
manifest:PWA 清单,请求manifest.json之类的元数据。worker/sharedworker/serviceworker:加载不同类型的Worker脚本。report:浏览器安全或性能上报,例如CSP、NEL、Reporting API。favicon.ico:标签页图标请求。
其中最需要额外注意的是 Service Worker。
它可以拦截页面和子资源请求,并从本地缓存返回响应。这样一来,你在浏览器里看到的数据,不一定每次都真的来自网络。
所以分析站点时,最好同时关注:
- 该请求是否被
Service Worker接管 - 响应是否来自内存缓存、磁盘缓存或
Service Worker - 重新加载时数据是否仍然命中缓存
3. XHR 和 fetch 的区别
3.1 相同点
XMLHttpRequest 和 fetch() 都能发 HTTP 请求,也都经常承载业务数据。
所以浏览器工具常把它们放进同一个 Fetch/XHR 视图里,抓包时也常一起看。
3.2 关键差异
| 维度 | XMLHttpRequest |
fetch() |
|---|---|---|
| 编程模型 | 事件驱动,常见 readystatechange |
Promise 模型 |
| 错误处理 | 通过事件和状态判断 | HTTP 错误不会自动 reject |
| 上传进度 | 支持 xhr.upload.onprogress |
原生支持相对弱,常需额外方案 |
| 下载流 | 能做,但模型不如 fetch 自然 |
更适合与 Streams 协作 |
| 同步请求 | 历史上支持,但不推荐 | 不提供同步模式 |
| 中止请求 | xhr.abort() |
AbortController |
| 与现代 Web 特性协同 | 较旧 | 更适合 Service Worker、现代框架 |
3.3 对爬虫的实际意义
从“抓包复现”的角度看,XHR 和 fetch() 的差别没有“接口参数、鉴权头、签名逻辑”重要。
但从“还原前端行为”的角度看,二者差异会影响你如何分析源码。
例如:
- 老站里你更容易在
xhr.open()、xhr.send()附近找到参数拼装逻辑。 - 新站里你更容易在
fetch()包装器、请求拦截器、中间层 SDK 里找到公共头和签名逻辑。
4. 从爬虫视角看,哪些请求最值得关注
4.1 第一优先级:真正承载业务数据的请求
通常最值得优先排查的是:
document:确认首屏 HTML 是否已经包含数据。Fetch/XHR:确认列表、翻页、搜索、筛选、详情接口。WebSocket/EventSource:确认实时数据是否通过长连接推送。- 表单提交:确认登录、搜索、上传、导出是否通过传统表单完成。
这四类请求决定了你到底要用哪种抓取策略:
- 只拉 HTML 就够。
- 直接复用 JSON 接口即可。
- 必须执行 JavaScript。
- 必须维持长连接或订阅通道。
4.2 第二优先级:帮助你还原接口逻辑的请求
这类请求本身不一定是最终数据,但常常能帮助你把接口复现出来:
script:定位接口地址、公共请求头、签名算法、加密逻辑。OPTIONS预检:判断真实请求是否跨域、是否带自定义头。manifest、初始化配置 JSON:可能暴露环境变量、资源域名、接口网关。
4.3 通常可以先忽略的请求
以下请求多数时候不值得一开始就深挖:
- 图片、字体、样式
- 埋点
beacon <a ping>跳转统计prefetch、preload、preconnectfavicon.ico
但“可先忽略”不等于“永远无用”。 例如图片站的核心数据可能就在图片 URL,本地化字体接口也可能带防盗链参数,视频站的核心目标可能就是媒体分片地址。
4.4 判断“真正业务接口”的几个强信号
如果一个请求同时满足下面几条,它大概率就是你真正要复现的接口:
- 响应是
JSON、protobuf、graphql结果,而不是 HTML 或静态资源。 - 参数里出现
page、offset、cursor、keyword、sort、filter之类字段。 - 这个请求会随着滚动、翻页、点击筛选条件而重复出现。
- 响应里直接出现商品、帖子、评论、用户、订单、文章等业务实体。
- 请求头里带
Authorization、Cookie、x-csrf-token、x-requested-with等鉴权信息。
5. 抓包时的实战流程
5.1 建议的排查顺序
抓包时建议严格按顺序做,而不是一上来就翻几百条请求:
- 打开
Network,勾选Preserve log,必要时关闭缓存。 - 清空面板,只执行一个业务动作,例如“点下一页”或“输入关键词搜索”。
- 先看
document和Fetch/XHR,再看WS、Other。 - 对可疑请求重点看
Headers、Payload、Response、Initiator。 - 确认这个请求是否真的承载了业务数据,而不是埋点或性能预取。
这种做法的本质,是建立“动作 -> 请求 -> 响应 -> 页面变化”的因果链。 没有这个因果链,抓包只是看热闹。
5.2 必须记录的关键信息
真正准备复现某个接口时,至少要记下这些字段:
| 字段 | 为什么重要 | 例子 |
|---|---|---|
| URL | 确定接口地址和路由模式 | /api/search |
| Method | 区分 GET、POST、PUT 等 |
POST |
| Query / Body | 业务参数就在这里 | page=2、cursor=abc |
Content-Type |
决定请求体编码 | application/json |
Cookie / Authorization |
决定是否有权限访问 | Bearer ... |
Origin / Referer |
有些站点会校验来源 | 页面来源 URL |
x-csrf-token 等自定义头 |
很多请求没有它就会失败 | 防重放令牌 |
| 响应格式 | 判断如何解析 | JSON、HTML、二进制 |
| 分页标记 | 决定如何翻页 | offset、cursor |
| 错误码和限流信息 | 决定重试策略 | 429、Retry-After |
5.3 三个高频误判
5.3.1 只盯 Fetch/XHR,错过了 HTML 首屏数据
很多 SSR 页面在初始 document 里已经给出了完整列表。
这时额外的 XHR 只是局部刷新,不是主数据源。
5.3.2 把 OPTIONS、beacon、ping 当成核心接口
这类请求经常很多,也很显眼。 但它们通常只是跨域预检或埋点,不是页面业务数据的真正来源。
5.3.3 看到了 WebSocket 握手,却没继续看消息帧
WS 的价值不在 101 握手本身,而在握手之后收发的内容。
如果不继续看帧数据,就等于只看到了门牌号,没进门。
6. 复现浏览器请求时的关键细节
6.1 不要机械复制所有头
复现接口时,不是所有请求头都同样重要。 你应该优先判断“哪些头决定业务是否成功”,而不是把浏览器里看到的每个头全量照抄。
通常优先级更高的头包括:
CookieAuthorizationContent-TypeOriginRefererX-CSRF-Token或类似自定义头If-None-Match、If-Modified-Since这类缓存相关头
而像 sec-ch-ua、priority、部分 Sec-Fetch-* 头,更多是浏览器上下文信息。
有些站点会校验它们,但很多普通接口并不依赖它们决定业务正确性。
6.2 注意缓存、304 与条件请求
浏览器为了减轻流量,会自动发条件请求。
如果你看到 ETag、If-None-Match、Last-Modified、If-Modified-Since,说明浏览器可能并没有每次都重新下载完整资源。
对爬虫来说,这意味着:
- 看到
304 Not Modified并不代表“没拿到数据”,而是“浏览器复用了本地缓存”。 - 如果你脱离浏览器单独复现请求,可能会得到
200全量响应,而不是304。
6.3 注意 Service Worker 和前端缓存
有些站点的请求虽然显示在 Network 里,但响应可能来自:
- 内存缓存
- 磁盘缓存
Service Worker
这会带来一个常见误区: 你以为“页面没有真实请求”,其实只是响应已经被前端缓存层接住了。
6.4 注意接口之外的“前端计算”
很多站点真正的难点,不是接口地址,而是接口参数并不是静态写死的。 例如:
- 签名参数由脚本运行后计算出来
- 请求体被加密
- 分页游标来自上一页响应
csrf token先藏在 HTML 或脚本变量里
所以分析请求时,要同时看:
- 这个请求是谁发起的
- 发起前做了哪些参数加工
- 这些参数是写死的,还是运行时生成的
7. 面试回答精简版
浏览器发出的请求,不能只按 xhr、fetch 这种 API 名称理解,更应该按目标和语义来分。最核心的几类是:页面导航的 document 请求、渲染页面的静态资源请求、JavaScript 发起的 Fetch/XHR 请求,以及 WebSocket、SSE 这类长连接或流式请求。像 beacon、ping、prefetch、preload 更多是埋点和性能优化请求,不一定承载业务数据。
如果从爬虫角度抓包,我会优先看 document、Fetch/XHR、WebSocket、表单提交这几类请求,先判断数据到底是在首屏 HTML、普通 JSON 接口,还是在长连接消息里。然后重点记录 URL、方法、请求体、鉴权头、Cookie、csrf token、分页参数和响应格式。预检 OPTIONS、埋点 beacon、图片字体这类请求通常不是第一优先级,但它们能帮助我排除噪声、识别跨域和反爬策略。