Skip to content

浏览器会发出哪些请求,抓包和爬虫时应关注什么

1. 浏览器请求的分类方式

1.1 不要只按 DevTools 标签理解请求

面试里常见误区,是把 xhrfetchdocumentwebsocket 都当成同一层级的“请求类型”。 实际上它们混合了 发起方式资源目标协议形态 这三类维度,所以抓包时容易看乱。

更稳妥的理解方式,是把浏览器请求拆成三层:

  • 导航类请求:例如地址栏输入 URL、点击链接、表单跳转,目标通常是 document
  • 子资源请求:例如 scriptstyleimagefontmedia,它们服务于页面渲染。
  • 编程式请求:例如 XMLHttpRequestfetch()EventSourceWebSocketsendBeacon(),它们通常由 JavaScript 主动发起。

Fetch 标准里的 Request.destination 提供了一个很好的统一视角。 它把请求目标描述为 documentscriptstyleimagefontworkermanifest 等;而 fetch()XMLHttpRequestEventSourceWebSocketnavigator.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、报告上报等 通常低

结论是:抓包时不要先盯“数量最多的请求”,而要先盯“真正承载业务数据的请求”。 对大多数业务站点来说,优先级一般是 documentFetch/XHRWSEventSource,而不是图片和字体。

2. 浏览器常见请求类型详解

2.1 导航请求 document

导航请求是浏览器最基础的一类请求。 它由地址栏输入、点击 <a>window.location 跳转、iframe 加载、表单提交跳转等动作触发,服务端通常返回 HTML 文档。

这类请求对爬虫非常关键,原因有三点:

  • 首屏数据可能直接写在 HTML 中,根本没有额外的 XHRfetch
  • 页面里可能内嵌了初始化状态,例如 __INITIAL_STATE__window.__DATA__script[type="application/json"]
  • 首次响应往往会下发登录态、Set-Cookie、防重放令牌、csrf token 等后续请求依赖的信息。

如果一个站点是传统的服务端渲染,也就是 SSR,那么 真正的数据可能就在 document 响应体里。 这时只盯 Fetch/XHR 会得出“页面没有接口”的错误结论。

2.2 静态子资源请求:scriptstyleimagefontmedia

浏览器解析 HTML 和 CSS 之后,会继续请求脚本、样式、图片、字体、音视频等子资源。 这些请求的主要目标是“把页面渲染出来”,而不是“把业务数据返回给你”。

从爬虫角度看,它们通常分成两类:

  • 大多可忽略styleimagefont 通常不承载结构化业务数据。
  • 有时必须分析script 里可能藏着接口地址、签名算法、分页参数拼接逻辑;media 可能对应分片流,例如 m3u8tsmp4 分段。

很多视频站或音频站还会发出 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 WorkerStreamsCORS 等机制结合得更自然。

fetch() 的几个高频考点:

  • fetch() 返回的 Promise,在 收到响应头 时就会兑现,不必等整个响应体读完。
  • HTTP 404500 这类业务失败,不会让 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);

在现代 SPAReactVueNext.jsNuxt 等应用里,业务数据接口大多优先使用 fetch()。 所以抓包时看到 Fetch/XHR,里面非常大一部分实际就是 fetch() 请求。

2.5 表单提交请求

表单提交是很多人抓包时容易忽略的一类请求。 登录、注册、搜索、筛选、上传文件、导出报表,仍然大量依赖 <form> 提交。

常见表单编码方式有三种:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

这类请求的几个重点是:

  • 文件上传几乎一定要看 multipart/form-data 的边界和字段名。
  • 登录表单通常包含隐藏字段,例如 csrf tokennonce、一次性验证码票据。
  • JavaScript 调用 form.submit() 和用户真正点击提交按钮,并不完全等价;前者不会触发 submit 事件和约束校验。

2.6 OPTIONS 预检请求

当页面发起跨域请求,并且该请求不属于“简单请求”时,浏览器会先自动发一个 OPTIONS 请求做 CORS preflight。 这个请求不是业务方手写的,而是浏览器为了确认“目标站是否允许这样跨域访问”自动插入的。

典型预检请求会带上这些头:

  • Origin
  • Access-Control-Request-Method
  • Access-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 EventSourceSSE

EventSource 对应的是 Server-Sent Events,简称 SSE。 它基于普通 HTTP 连接,但响应体是持续不断推送的 text/event-stream,属于“服务端单向推送”。

它和 WebSocket 的区别很明确:

  • SSE单向 的,只能服务端推给客户端。
  • WebSocket双向 的,双方都可以主动发消息。

如果页面看起来在自动更新,但没有发现明显的轮询 XHRfetch(),就要检查是否存在 EventSource。 很多监控面板、实时状态页、日志流页面会用这种方式。

2.9 navigator.sendBeacon()<a ping>

这两类请求通常是埋点和统计噪声。 它们的价值不在业务数据,而在“告诉服务端用户发生了什么动作”。

navigator.sendBeacon() 的特点:

  • 只能异步发送一个小型 POST 请求。
  • 常用于页面卸载、隐藏、跳转时上报埋点。
  • 调用方通常不关心响应内容。

<a ping> 的特点:

  • 用户点击链接时,浏览器会额外向 ping 指定的地址发送 POST 请求。
  • 典型用途是点击统计、广告归因、跳转链路埋点。

对爬虫来说,这类请求大多可以忽略。 但它们会污染抓包视图,所以识别出来很重要,避免误把埋点接口当成业务接口。

2.10 预加载与投机请求:preloadmodulepreloadprefetchpreconnectdns-prefetch

这类请求不是为了立即展示业务数据,而是为了优化性能。 它们的目标是“提前做准备”,所以抓包时很容易和真实业务请求混淆。

几个概念需要分清:

  • preload:提前下载当前页面很快就要用到的资源,例如脚本、样式、字体、图片。
  • modulepreload:提前获取、解析、编译模块脚本及其依赖。
  • prefetch:低优先级预取未来可能用到的资源,常用于“下一页可能会访问”的内容。
  • preconnect:提前做 DNSTCPTLS 握手,重点是“连上”,不一定立刻下载资源。
  • dns-prefetch:只提前做域名解析,不会直接发 HTTP 资源请求。

因此:

  • preloadmodulepreloadprefetch 可能在 Network 面板里看到真实资源请求。
  • preconnect 更像“连接预热”。
  • dns-prefetch 甚至可能根本不表现为普通 HTTP 请求。

如果你在抓包时看见页面一打开就先去请求几个脚本、字体、下一页资源,不要急着判断那就是核心业务接口。 先确认它到底是“渲染资源”,还是“真实数据接口”。

2.11 其它容易忽略的请求

还有几类请求也会在真实项目里出现:

  • manifest:PWA 清单,请求 manifest.json 之类的元数据。
  • worker / sharedworker / serviceworker:加载不同类型的 Worker 脚本。
  • report:浏览器安全或性能上报,例如 CSPNELReporting API
  • favicon.ico:标签页图标请求。

其中最需要额外注意的是 Service Worker。 它可以拦截页面和子资源请求,并从本地缓存返回响应。这样一来,你在浏览器里看到的数据,不一定每次都真的来自网络。

所以分析站点时,最好同时关注:

  • 该请求是否被 Service Worker 接管
  • 响应是否来自内存缓存、磁盘缓存或 Service Worker
  • 重新加载时数据是否仍然命中缓存

3. XHRfetch 的区别

3.1 相同点

XMLHttpRequestfetch() 都能发 HTTP 请求,也都经常承载业务数据。 所以浏览器工具常把它们放进同一个 Fetch/XHR 视图里,抓包时也常一起看。

3.2 关键差异

维度 XMLHttpRequest fetch()
编程模型 事件驱动,常见 readystatechange Promise 模型
错误处理 通过事件和状态判断 HTTP 错误不会自动 reject
上传进度 支持 xhr.upload.onprogress 原生支持相对弱,常需额外方案
下载流 能做,但模型不如 fetch 自然 更适合与 Streams 协作
同步请求 历史上支持,但不推荐 不提供同步模式
中止请求 xhr.abort() AbortController
与现代 Web 特性协同 较旧 更适合 Service Worker、现代框架

3.3 对爬虫的实际意义

从“抓包复现”的角度看,XHRfetch() 的差别没有“接口参数、鉴权头、签名逻辑”重要。 但从“还原前端行为”的角度看,二者差异会影响你如何分析源码。

例如:

  • 老站里你更容易在 xhr.open()xhr.send() 附近找到参数拼装逻辑。
  • 新站里你更容易在 fetch() 包装器、请求拦截器、中间层 SDK 里找到公共头和签名逻辑。

4. 从爬虫视角看,哪些请求最值得关注

4.1 第一优先级:真正承载业务数据的请求

通常最值得优先排查的是:

  1. document:确认首屏 HTML 是否已经包含数据。
  2. Fetch/XHR:确认列表、翻页、搜索、筛选、详情接口。
  3. WebSocket / EventSource:确认实时数据是否通过长连接推送。
  4. 表单提交:确认登录、搜索、上传、导出是否通过传统表单完成。

这四类请求决定了你到底要用哪种抓取策略:

  • 只拉 HTML 就够。
  • 直接复用 JSON 接口即可。
  • 必须执行 JavaScript。
  • 必须维持长连接或订阅通道。

4.2 第二优先级:帮助你还原接口逻辑的请求

这类请求本身不一定是最终数据,但常常能帮助你把接口复现出来:

  • script:定位接口地址、公共请求头、签名算法、加密逻辑。
  • OPTIONS 预检:判断真实请求是否跨域、是否带自定义头。
  • manifest、初始化配置 JSON:可能暴露环境变量、资源域名、接口网关。

4.3 通常可以先忽略的请求

以下请求多数时候不值得一开始就深挖:

  • 图片、字体、样式
  • 埋点 beacon
  • <a ping> 跳转统计
  • prefetchpreloadpreconnect
  • favicon.ico

但“可先忽略”不等于“永远无用”。 例如图片站的核心数据可能就在图片 URL,本地化字体接口也可能带防盗链参数,视频站的核心目标可能就是媒体分片地址。

4.4 判断“真正业务接口”的几个强信号

如果一个请求同时满足下面几条,它大概率就是你真正要复现的接口:

  • 响应是 JSONprotobufgraphql 结果,而不是 HTML 或静态资源。
  • 参数里出现 pageoffsetcursorkeywordsortfilter 之类字段。
  • 这个请求会随着滚动、翻页、点击筛选条件而重复出现。
  • 响应里直接出现商品、帖子、评论、用户、订单、文章等业务实体。
  • 请求头里带 AuthorizationCookiex-csrf-tokenx-requested-with 等鉴权信息。

5. 抓包时的实战流程

5.1 建议的排查顺序

抓包时建议严格按顺序做,而不是一上来就翻几百条请求:

  1. 打开 Network,勾选 Preserve log,必要时关闭缓存。
  2. 清空面板,只执行一个业务动作,例如“点下一页”或“输入关键词搜索”。
  3. 先看 documentFetch/XHR,再看 WSOther
  4. 对可疑请求重点看 HeadersPayloadResponseInitiator
  5. 确认这个请求是否真的承载了业务数据,而不是埋点或性能预取。

这种做法的本质,是建立“动作 -> 请求 -> 响应 -> 页面变化”的因果链。 没有这个因果链,抓包只是看热闹。

5.2 必须记录的关键信息

真正准备复现某个接口时,至少要记下这些字段:

字段 为什么重要 例子
URL 确定接口地址和路由模式 /api/search
Method 区分 GETPOSTPUT POST
Query / Body 业务参数就在这里 page=2cursor=abc
Content-Type 决定请求体编码 application/json
Cookie / Authorization 决定是否有权限访问 Bearer ...
Origin / Referer 有些站点会校验来源 页面来源 URL
x-csrf-token 等自定义头 很多请求没有它就会失败 防重放令牌
响应格式 判断如何解析 JSON、HTML、二进制
分页标记 决定如何翻页 offsetcursor
错误码和限流信息 决定重试策略 429Retry-After

5.3 三个高频误判

5.3.1 只盯 Fetch/XHR,错过了 HTML 首屏数据

很多 SSR 页面在初始 document 里已经给出了完整列表。 这时额外的 XHR 只是局部刷新,不是主数据源。

5.3.2 把 OPTIONSbeaconping 当成核心接口

这类请求经常很多,也很显眼。 但它们通常只是跨域预检或埋点,不是页面业务数据的真正来源。

5.3.3 看到了 WebSocket 握手,却没继续看消息帧

WS 的价值不在 101 握手本身,而在握手之后收发的内容。 如果不继续看帧数据,就等于只看到了门牌号,没进门。

6. 复现浏览器请求时的关键细节

6.1 不要机械复制所有头

复现接口时,不是所有请求头都同样重要。 你应该优先判断“哪些头决定业务是否成功”,而不是把浏览器里看到的每个头全量照抄。

通常优先级更高的头包括:

  • Cookie
  • Authorization
  • Content-Type
  • Origin
  • Referer
  • X-CSRF-Token 或类似自定义头
  • If-None-MatchIf-Modified-Since 这类缓存相关头

而像 sec-ch-uapriority、部分 Sec-Fetch-* 头,更多是浏览器上下文信息。 有些站点会校验它们,但很多普通接口并不依赖它们决定业务正确性。

6.2 注意缓存、304 与条件请求

浏览器为了减轻流量,会自动发条件请求。 如果你看到 ETagIf-None-MatchLast-ModifiedIf-Modified-Since,说明浏览器可能并没有每次都重新下载完整资源。

对爬虫来说,这意味着:

  • 看到 304 Not Modified 并不代表“没拿到数据”,而是“浏览器复用了本地缓存”。
  • 如果你脱离浏览器单独复现请求,可能会得到 200 全量响应,而不是 304

6.3 注意 Service Worker 和前端缓存

有些站点的请求虽然显示在 Network 里,但响应可能来自:

  • 内存缓存
  • 磁盘缓存
  • Service Worker

这会带来一个常见误区: 你以为“页面没有真实请求”,其实只是响应已经被前端缓存层接住了。

6.4 注意接口之外的“前端计算”

很多站点真正的难点,不是接口地址,而是接口参数并不是静态写死的。 例如:

  • 签名参数由脚本运行后计算出来
  • 请求体被加密
  • 分页游标来自上一页响应
  • csrf token 先藏在 HTML 或脚本变量里

所以分析请求时,要同时看:

  • 这个请求是谁发起的
  • 发起前做了哪些参数加工
  • 这些参数是写死的,还是运行时生成的

7. 面试回答精简版

浏览器发出的请求,不能只按 xhrfetch 这种 API 名称理解,更应该按目标和语义来分。最核心的几类是:页面导航的 document 请求、渲染页面的静态资源请求、JavaScript 发起的 Fetch/XHR 请求,以及 WebSocketSSE 这类长连接或流式请求。像 beaconpingprefetchpreload 更多是埋点和性能优化请求,不一定承载业务数据。

如果从爬虫角度抓包,我会优先看 documentFetch/XHRWebSocket、表单提交这几类请求,先判断数据到底是在首屏 HTML、普通 JSON 接口,还是在长连接消息里。然后重点记录 URL、方法、请求体、鉴权头、Cookiecsrf token、分页参数和响应格式。预检 OPTIONS、埋点 beacon、图片字体这类请求通常不是第一优先级,但它们能帮助我排除噪声、识别跨域和反爬策略。