拦截网络请求:一种更优雅的数据获取方式

拦截网络请求:一种更优雅的数据获取方式

前言

上一篇我们讲了 DOM 解析------直接从页面上"抠"数据。它简单直观,但有一个明显的缺点:太依赖页面结构了。蝉妈妈改一次前端代码,你的选择器可能就全废了。

今天介绍另一种思路:拦截网络请求

不从页面上抠数据,而是直接截获浏览器和服务器之间的通信数据。拿到的是干干净净的 JSON,不用再费劲去解析 DOM。


一、先搞懂一个基本事实

你在蝉妈妈上看到的达人列表,数据不是写死在 HTML 里的

真实的过程是这样的:

复制代码
┌──────────┐    ① 请求数据     ┌──────────┐
│          │ ──────────────►  │          │
│  浏览器   │                  │  服务器   │
│          │ ◄──────────────  │          │
└──────────┘    ② 返回 JSON    └──────────┘
      │
      ▼ ③ 渲染到页面
┌──────────┐
│  你看到的  │
│  达人列表  │
└──────────┘
  1. 浏览器向服务器发送一个请求(通常是 AJAX/Fetch 请求)
  2. 服务器返回一段 JSON 数据
  3. 前端 JavaScript 拿到 JSON 后,渲染成你看到的页面

DOM 解析是在第 ③ 步之后取数据,而拦截网络请求是在第 ② 步直接截获数据。

显然,第 ② 步拿到的数据更干净、更完整、更稳定。


二、先用开发者工具看看真实的请求

在写代码之前,我们先手动观察一下蝉妈妈到底发了什么请求。

步骤

  1. 打开蝉妈妈达人排行页面

  2. F12 打开开发者工具

  3. 切换到 Network(网络) 面板

  4. 点击 Fetch/XHR 过滤器(只看 AJAX 请求)

  5. 刷新页面或翻页,观察出现了哪些请求

    ┌─────────────────────────────────────────────────────────┐
    │ Network 面板 │
    │ │
    │ Filter: [Fetch/XHR] │
    │ │
    │ Name Status Size Time │
    │ ───────────────────────────────────────────── │
    │ rank/promoter/list 200 12.3KB 320ms ← 就是它!│
    │ user/info 200 1.2KB 150ms │
    │ config 200 0.5KB 80ms │
    │ │
    └─────────────────────────────────────────────────────────┘

点击那个关键请求(比如 rank/promoter/list),你能看到:

Request(请求信息):

复制代码
URL: https://api.chanmama.com/v1/rank/promoter/list
Method: GET
Query Parameters:
  page: 1
  size: 20
  category: 美妆
  sort: fans_count

Response(返回数据):

json 复制代码
{
  "code": 0,
  "data": {
    "total": 5000,
    "list": [
      {
        "user_id": "123456",
        "nickname": "张三",
        "fans_count": 1000000,
        "category": "美妆",
        "avg_sales": 500000,
        "contact": "zhangsan@example.com"
      },
      {
        "user_id": "789012",
        "nickname": "李四",
        "fans_count": 500000,
        "category": "美妆",
        "avg_sales": 300000,
        "contact": "lisi@example.com"
      }
    ]
  }
}

看到了吗?服务器直接返回了结构化的 JSON 数据,字段名称清清楚楚,不需要你去猜 DOM 结构。


三、拦截请求的三种方式

知道了数据在哪里,接下来就是怎么在脚本里截获它。

方式一:拦截 XMLHttpRequest

大部分网站的 AJAX 请求底层用的是 XMLHttpRequest(简称 XHR)。我们可以重写它的原型方法来拦截。

javascript 复制代码
function interceptXHR(urlKeyword, callback) {
  // 保存原始的 open 和 send 方法
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url, ...args) {
    // 记录这次请求的 URL
    this._url = url;
    this._method = method;
    return originalOpen.apply(this, [method, url, ...args]);
  };

  XMLHttpRequest.prototype.send = function (body) {
    this.addEventListener('load', function () {
      // 检查 URL 是否包含我们关心的关键词
      if (this._url && this._url.includes(urlKeyword)) {
        try {
          const response = JSON.parse(this.responseText);
          callback(response, this._url);
        } catch (e) {
          console.error('解析响应失败:', e);
        }
      }
    });

    return originalSend.apply(this, [body]);
  };
}

使用方式:

javascript 复制代码
interceptXHR('rank/promoter/list', (data, url) => {
  console.log('拦截到达人列表数据:', data);
  console.log('请求地址:', url);

  // data.data.list 就是达人数组
  const promoters = data.data.list;
  promoters.forEach(p => {
    console.log(`${p.nickname} - ${p.fans_count}粉丝`);
  });
});

当蝉妈妈的页面发起请求加载达人列表时,我们的回调函数就会自动被触发,拿到完整的 JSON 数据。

方式二:拦截 Fetch

现代网站也可能使用 fetch API 来发请求,原理一样,重写 window.fetch

javascript 复制代码
function interceptFetch(urlKeyword, callback) {
  const originalFetch = window.fetch;

  window.fetch = function (url, options) {
    return originalFetch.apply(this, arguments).then(response => {
      // clone 一份,因为 response body 只能读一次
      const clonedResponse = response.clone();

      if (url.includes(urlKeyword)) {
        clonedResponse.json().then(data => {
          callback(data, url);
        }).catch(e => {
          console.error('解析 fetch 响应失败:', e);
        });
      }

      // 返回原始 response,不影响页面正常逻辑
      return response;
    });
  };
}

使用方式:

javascript 复制代码
interceptFetch('rank/promoter/list', (data, url) => {
  console.log('Fetch 拦截到数据:', data);
});

为什么要 clone? response.body 是一个流(Stream),只能被读取一次。如果我们读了,页面的代码就读不到了,页面会出错。clone() 复制一份,我们读复制品,原始的留给页面。

方式三:同时拦截两者

不确定网站用的是 XHR 还是 Fetch?两个都拦截就行了

javascript 复制代码
function interceptAllRequests(urlKeyword, callback) {
  // 拦截 XHR
  interceptXHR(urlKeyword, callback);
  // 拦截 Fetch
  interceptFetch(urlKeyword, callback);

  console.log(`已设置请求拦截,关键词: ${urlKeyword}`);
}

// 一行搞定
interceptAllRequests('rank/promoter/list', (data, url) => {
  console.log('拦截到数据:', data);
});

四、完整实战:用拦截方式重写蝉妈妈爬取脚本

把拦截方式整合进我们的蝉妈妈脚本:

javascript 复制代码
// ==UserScript==
// @name         蝉妈妈达人爬取(请求拦截版)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @match        https://www.chanmama.com/promoter/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // 存储所有拦截到的数据
  let allData = [];

  // 在页面代码执行之前就设置拦截(注意 @run-at document-start)
  interceptAllRequests('rank/promoter/list', (response, url) => {
    if (response.code === 0 && response.data && response.data.list) {
      const list = response.data.list;

      console.log(`拦截到 ${list.length} 条达人数据`);

      // 提取我们需要的字段
      const cleaned = list.map(item => ({
        nickname: item.nickname,
        userId: item.user_id,
        fansCount: item.fans_count,
        category: item.category,
        avgSales: item.avg_sales,
        contact: item.contact || '无',
      }));

      allData.push(...cleaned);

      // 实时保存
      GM_setValue('crawl_data', JSON.stringify(allData));

      console.log(`累计获取 ${allData.length} 条数据`);
    }
  });

  // 页面加载完成后添加控制面板
  window.addEventListener('load', () => {
    addExportButton();
  });

  function addExportButton() {
    const btn = document.createElement('button');
    btn.innerText = `📥 导出数据 (${allData.length} 条)`;
    btn.style.cssText = `
      position: fixed; top: 80px; right: 20px; z-index: 99999;
      padding: 10px 20px; background: #52c41a; color: white;
      border: none; border-radius: 6px; cursor: pointer; font-size: 14px;
    `;

    btn.addEventListener('click', () => {
      const data = JSON.parse(GM_getValue('crawl_data', '[]'));
      if (data.length === 0) {
        alert('暂无数据,请先翻几页');
        return;
      }
      downloadJSON(data);
    });

    document.body.appendChild(btn);

    // 定时更新按钮上的数量
    setInterval(() => {
      btn.innerText = `📥 导出数据 (${allData.length} 条)`;
    }, 2000);
  }

  function downloadJSON(data) {
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `蝉妈妈达人数据_${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }
})();

使用流程

这次的使用方式更简单了:

  1. 安装脚本
  2. 打开蝉妈妈达人排行页面
  3. 正常浏览、正常翻页就行
  4. 脚本在后台默默拦截每一次请求,自动收集数据
  5. 想导出了,点右上角的导出按钮

你甚至不需要"启动"脚本------只要你在翻页,数据就在自动收集。


五、一个关键细节:@run-at document-start

注意脚本头部有一行:

javascript 复制代码
// @run-at       document-start

这非常重要。它决定了脚本在什么时机执行。

执行时机 能否拦截请求
document-start HTML 刚开始加载,页面代码还没执行 ✅ 能
document-end DOM 加载完成 ❌ 可能漏掉早期请求
document-idle 页面完全加载完(默认值) ❌ 大概率漏掉

如果你在 document-idle 时才去重写 XMLHttpRequest,页面的请求可能已经发完了,你什么都拦截不到。

所以拦截请求类的脚本,一定要用 document-start


六、DOM 解析 vs 请求拦截 对比

对比项 DOM 解析 请求拦截
数据来源 页面上渲染好的元素 服务器返回的原始 JSON
数据质量 可能有格式问题(如"100万"需要自己转换) 干净的原始数据(如 1000000)
稳定性 前端改版就可能失效 只要 API 不变就能用
实现难度 简单,querySelector 就行 需要理解请求拦截原理
执行时机 页面渲染完成后 必须在页面代码执行前(document-start)
翻页方式 需要模拟点击翻页按钮 用户手动翻页,后台自动收集
数据完整性 只能拿到页面上展示的字段 能拿到 API 返回的所有字段(可能更多)

简单来说:DOM 解析更容易上手,请求拦截更稳定更强大。


七、进阶:不仅拦截,还能修改

拦截请求不仅能"看"数据,还能"改"数据。虽然在爬取场景中不常用,但了解一下能开阔思路。

7.1 修改请求参数

比如蝉妈妈每页只返回 20 条,你想改成 100 条:

javascript 复制代码
const originalOpen = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function (method, url, ...args) {
  // 把 size=20 改成 size=100
  if (url.includes('rank/promoter/list')) {
    url = url.replace('size=20', 'size=100');
    console.log('已修改请求参数,每页获取100条');
  }
  return originalOpen.apply(this, [method, url, ...args]);
};

⚠️ 这不一定能成功------服务器可能会校验 size 参数,超过上限会拒绝或者忽略。

7.2 修改返回数据

javascript 复制代码
const originalSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.send = function (body) {
  this.addEventListener('load', function () {
    if (this._url.includes('rank/promoter/list')) {
      // 修改 responseText(只读属性,需要用 defineProperty 重写)
      const data = JSON.parse(this.responseText);
      data.data.list.forEach(item => {
        item.nickname = '【已爬取】' + item.nickname;
      });
      Object.defineProperty(this, 'responseText', {
        value: JSON.stringify(data)
      });
    }
  });

  return originalSend.apply(this, [body]);
};

八、踩坑记录

坑 1:拦截代码要在最早时机注入

前面说了,一定要用 @run-at document-start。但有时候即使设置了,还是会漏掉请求。

原因 :有些网站会在 <head> 的内联 <script> 中立刻发请求,比油猴脚本执行还早。

解决:目前没有完美解决方案。可以在注入拦截代码后,手动刷新页面触发一次重新请求。

坑 2:response.clone() 忘记调用

拦截 Fetch 时,如果你直接读取 response 的 body 而不 clone,页面的代码就读不到数据了,页面会白屏或报错。

javascript 复制代码
// ❌ 错误写法
window.fetch = function(url, options) {
  return originalFetch.apply(this, arguments).then(response => {
    response.json().then(data => { /* ... */ });  // 读了 body
    return response;  // 页面再读就报错了
  });
};

// ✅ 正确写法
window.fetch = function(url, options) {
  return originalFetch.apply(this, arguments).then(response => {
    const cloned = response.clone();  // 先 clone
    cloned.json().then(data => { /* ... */ });  // 读 clone 的
    return response;  // 原始的给页面
  });
};

坑 3:JSON.parse 报错

不是所有请求的响应都是 JSON,有些可能是 HTML、图片、或者空内容。直接 JSON.parse 会报错。

javascript 复制代码
// 加一个安全解析
function safeParseJSON(text) {
  try {
    return JSON.parse(text);
  } catch (e) {
    return null;
  }
}

// 使用
const data = safeParseJSON(this.responseText);
if (data) {
  callback(data, this._url);
}

坑 4:原型污染导致页面异常

重写 XMLHttpRequest.prototype 是一种"侵入式"操作,如果写得不小心,可能影响页面本身的请求逻辑。

原则

  • 只"观察",不要修改原始行为
  • 一定要调用原始方法(originalOpen.applyoriginalSend.apply
  • 回调中的错误一定要 try-catch,不能让它影响原始请求

九、总结

复制代码
                    两种数据获取方式

          DOM 解析                 请求拦截
            │                       │
     从渲染后的页面取数据        从网络传输中截获数据
            │                       │
     querySelector               重写 XHR / Fetch
            │                       │
     拿到的是文本                 拿到的是 JSON
     "100万粉丝"                 { fans: 1000000 }
            │                       │
     页面结构变了就废了           API 不变就能一直用

如果说 DOM 解析是"从货架上拿商品",那请求拦截就是"在仓库门口直接截货"------更早、更全、更干净。

两种方式各有适用场景,实际项目中也可以结合使用:请求拦截为主,DOM 解析为辅(有些数据可能只在页面上有,API 中没有返回)。


希望这篇文章让你对网络请求拦截有了清晰的认识。有问题欢迎交流!

后记

2026年4月15日于上海,在opus 4.6辅助下完成。

相关推荐
TechWayfarer3 小时前
IP归属地API 技术解析与应用实践
网络·网络协议·tcp/ip
zhgjx-dengkewen3 小时前
eNSP实验:配置NAT Server
服务器·网络·华为·智能路由器
添砖java‘’4 小时前
NAT代理、内网打洞和内网穿透
linux·服务器·网络
Once_day4 小时前
网络以太网之(3)LLDP协议
网络·以太网·lldp
m0_738120725 小时前
渗透测试基础ctfshow——Web应用安全与防护(五)
前端·网络·数据库·windows·python·sql·安全
其实防守也摸鱼5 小时前
XSS漏洞全景解析:从原理、实战利用到纵深防御
前端·网络·安全·xss·xss漏洞
路由侠内网穿透.6 小时前
本地部署开源客服系统 FreeScout 并实现外部访问( Windows 版本)
运维·服务器·网络·windows·网络协议
你觉得脆皮鸡好吃吗6 小时前
Check Anti-CSRF Token (AI)
前端·网络·网络协议·安全·csrf·网络安全学习
向宇it7 小时前
关闭SSH密码登录,SSH 如何使用公钥密钥登录服务器(解决服务器经常被攻击问题)
服务器·网络·ssh