news 2026/10/16 6:40:41

关于前端逆向、反爬校验与 DOM 抓取的取舍

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
关于前端逆向、反爬校验与 DOM 抓取的取舍

作为课程助教,需要从某个在线做题平台的班级练习排名里,把某学号段的同学每人每道题得分导出来,方便督促作业,该如何操作?
本文记录从"直接猜 API"到"最终 DOM 抓取"的思路转折,以及其中值得聊一聊的相关知识点。


一、需求

平台是一个答题系统的「排名」页,分页展示,每行一个学生,列包括:排名、学号、姓名、总分,以及每道小题的得分(判断、单选、程序填空……)。

目标:

  1. 只保留学号2026XXXXXXX之后的同学;
  2. 保留每道小题的得分;
  3. 导出成 Excel 能直接打开的 CSV。

二、踩坑路径(简述)

整个过程其实就是一次极简版的 Web 逆向,按时间顺序:

尝试结果原因
直接猜 RESTful 路径/api/.../exam/rankings?page=1报错Unexpected token '<' ... is not valid JSON服务器返回的是 HTML(404 页 / 登录页),不是 JSON
F12 → Network 找到真实接口common-rankings?page=0&limit=50自己fetch调用返回404缺了平台自定义的反爬请求头
右键「Copy as fetch」补齐x-lollipop、x-marshmallow等头偶发成功,随后又 404这类 token 是一次性/短时效的,会过期
放弃打 API,改为直接抓页面已经渲染好的表格 DOM一次跑通不再和反爬机制对抗

这是一个典型结论:当你有权限看页面本身时,DOM 抓取永远比硬刚 API 反爬更便宜。


三、关键知识点(重点)

1.Unexpected token '<'—— 一个被低估的报错信息

控制台报SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON。

看到这个报错不用慌,它在告诉你一件事:

你以为你在请求 JSON,实际上服务器给你回了一个 HTML 文档。

HTML 一定以<!DOCTYPE html>或<html>开头,JSON 不可能以<开头,所以 JSON 解析器在第一个字符就崩了。常见原因:

  • 请求路径错了,命中了前端路由,被 fallback 到index.html;
  • 未登录,被重定向到登录页;
  • 鉴权失败,返回了一个 HTML 错误页。

划重点:做接口测试时,先看Response的 Content-Type,再决定用不用.json()。一个成熟的脚本应该先console.log(resp.status, await resp.text())看一眼真实返回,再决定怎么解析。

2. Cookie / Session:浏览器已经帮你做了什么

在 Network 面板的 Cookie 一栏里能看到:

  • JSESSIONID:典型的 Java Servlet 会话 ID;
  • 平台自定义的会话凭证(形如XXXSession);
  • 若干_ga/_uid之类的统计或指纹 Cookie。

这些值意味着:你在浏览器里是登录态。当你在控制台写fetch(url, { credentials: "include" })时,浏览器会自动把这些 Cookie 带上。

fetch(url,{credentials:"include"})
  • credentials: "include":跨域也带 Cookie;
  • credentials: "same-origin"(默认):同源才带;
  • credentials: "omit":完全不带。

小知识:

  • 会话固定(Session Fixation)、CSRF 的本质都和"浏览器自动携带 Cookie"有关;
  • 服务端为了防 CSRRF,会要求请求带一个同步令牌(即后面说的x-头),这个令牌通常通过<meta name="csrf-token">或首页接口下发,不能靠猜。

3. 反爬请求头:x-lollipop/x-marshmallow

最关键的一步:用「Copy as fetch」对比成功请求和自己发的请求,发现多了两个奇怪的头:

x-lollipop: <一段随机串> x-marshmallow: <空串或短值>

把它们补上就能通,过一段时间又失效。

这是前端反爬里非常经典的做法:

  1. 页面加载时,JS 根据 UA、Canvas 指纹、时间戳、行为数据算一个签名;
  2. 后续所有 XHR 都要求带这个签名头;
  3. 服务端校验签名是否合法、是否过期、是否复用。

为什么是自定义x-头而不是 Cookie?

  • Cookie 会被浏览器自动附加,攻击者很难篡改;
  • 自定义头需要前端 JS 显式设置,没有 JS 执行环境的纯 HTTP 工具(curl、简单 requests)就拿不到正确值,这就把一部分裸脚本挡在门外。

划重点:

  • 这属于「前端行为验证 / 设备指纹」的轻量实现,比滑块、验证码便宜;
  • 绕它的正规思路是:不绕,直接复用浏览器环境(Puppeteer / Playwright),或者像我们本次一样——根本不调接口。

4. 同源策略与 CORS

控制台里看到的报错(WebSocket 连接失败等)和我们无关,但值得一提:为什么我们的fetch在控制台里能成功调用?因为我们是从目标平台自己的控制台发的请求,属于同源,不受 CORS 限制。

如果换成在https://evil.com的控制台里发fetch("https://target.com/..."),就会被 CORS 拦住,除非服务端返回Access-Control-Allow-Origin。

5. DOM 抓取 vs API 逆向:成本对比

维度API 逆向DOM 抓取
数据结构化程度高(JSON)中(要自己解析)
抗反爬能力差(签名、Token 一变就废)强(只要页面长这样就能读)
性能好(一次拿 JSON)一般(要翻页、等渲染)
工程复杂度高低
适合场景高频、大规模、无人值守一次性、小数据量、有登录态

比如一共 95 条数据,翻 2 页就完事,DOM 抓取是性价比最高的选择。

6. 两个工程小细节

(a) 分页 page 从 0 开始

URL 里是?page=0&limit=50,不是page=1。第一次循环写成page=1直接少抓一页。这种细节只能靠 Network 面板看真实请求,不能靠想当然。

(b) CSV 加 BOM 头,Excel 才认中文

newBlob(["\ufeff"+csv],{type:"text/csv;charset=utf-8"})

\ufeff是 UTF-8 BOM。Excel 在没有 BOM 的情况下会按系统本地编码(中文 Windows 是 GBK)去猜 UTF-8 文件,导致乱码。加这一个字符,中文列名就正常了。

© 表格文本解析的坑

最开始我们用document.querySelector("table").innerText,再按换行切分。结果解析出 800 多行假数据——因为每个单元格内部本身有换行,innerText会把单元格内的<br>也输出成\n,导致一行数据被切成多片。

正确做法是直接遍历 DOM:

document.querySelectorAll("tr").forEach(tr=>{constcells=[...tr.querySelectorAll("td")].map(td=>td.innerText.trim());});

DOM 是结构化的,tr就是一行,td就是一列,不会错。


四、脱敏后的脚本

asyncfunctionexportRank(){constsleep=ms=>newPromise(r=>setTimeout(r,ms));functionparseRows(){constrows=[];document.querySelectorAll("tr").forEach(tr=>{consttds=tr.querySelectorAll("td");if(tds.length<4)return;// 跳过表头rows.push([...tds].map(td=>td.innerText.replace(/\n/g,' ').trim()));});returnrows;}letall=parseRows();// 翻到下一页(页码按钮文本为 "2")constnext=[...document.querySelectorAll("button,li,a")].find(el=>el.textContent.trim()==="2");if(next){next.click();awaitsleep(1800);all=all.concat(parseRows());}// 只保留学号 >= 阈值(学号是N位数字字符串,按字典序比较即可)constTHRESHOLD="2026XXXXXXX";consttarget=all.filter(cells=>{constm=cells.join(' ').match(/20\d{10}/);returnm&&m[0].localeCompare(THRESHOLD)>=0;});// 导出 CSV(带 BOM,Excel 中文不乱码)constheader="排名,学号,姓名,总分,...各题得分...,开始时间\n";constcsv="\ufeff"+header+target.map(r=>r.join(",")).join("\n");constblob=newBlob([csv],{type:"text/csv;charset=utf-8"});consta=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="rank_filtered.csv";a.click();}exportRank();

五、合规与边界

最后必须提一句:

  • 这个脚本跑在你自己已经登录、且有权查看的班级页面里,导出的是合规范围内的数据;
  • 没有绕过登录、没有爆破接口、没有对服务端造成额外压力;
  • 脚本只在本地浏览器执行,不向第三方发送任何数据。

清楚这条线:“技术上能做到” 和 “该不该做” 是两件事。同样一段代码,用来导出本班级的作业情况是效率工具,用来批量爬别人平台的数据就可能踩红线。


六、一句话总结

遇到反爬,先别急着逆向签名。问自己一句:这个数据我在浏览器里已经看得见了吗?看得见,就直接从 DOM 拿;看不见,再考虑打 API。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:38:49

VISSIM二次开发:Python通过COM接口动态修改公交停靠时间

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:38:23

Dubbo注解开发原理:@DubboService与Spring整合机制深度拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:38:02

抖音视频优化:5 个实操方法落地抖音关键词 SEO 优化

不少运营抖音账号的朋友&#xff0c;拍了很多视频&#xff0c;播放量却一直平平&#xff0c;花了不少时间精力&#xff0c;却很难吸引到目标客户。其实做好抖音优化&#xff0c;不用盲目追热点&#xff0c;掌握几个简单可落地的办法&#xff0c;就能提升作品在搜索渠道的曝光机…

作者头像 李华
网站建设 2026/10/16 6:35:43

基于Java的全功能直播平台构建指南

引言现在, 互联网那个发展得特别快, 所以直播这个东西就成了大家每天生活里面离不开的一部分了。不管是搞教育、找乐子, 还是卖东西那个电商领域来说, 直播都显示出了很大的潜力和挺大的价值。Java这个编程语言, 它是那种已经成熟了的, 而且到处都在用的语言, 靠着它那挺强的能…

作者头像 李华