作为课程助教需要从某个在线做题平台的班级练习排名里把某学号段的同学每人每道题得分导出来方便督促作业该如何操作本文记录从直接猜 API到最终 DOM 抓取的思路转折以及其中值得聊一聊的相关知识点。一、需求平台是一个答题系统的「排名」页分页展示每行一个学生列包括排名、学号、姓名、总分以及每道小题的得分判断、单选、程序填空……。目标只保留学号2026XXXXXXX之后的同学保留每道小题的得分导出成 Excel 能直接打开的 CSV。二、踩坑路径简述整个过程其实就是一次极简版的 Web 逆向按时间顺序尝试结果原因直接猜 RESTful 路径/api/.../exam/rankings?page1报错Unexpected token ... is not valid JSON服务器返回的是 HTML404 页 / 登录页不是 JSONF12 → Network 找到真实接口common-rankings?page0limit50自己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跨域也带 Cookiecredentials: same-origin默认同源才带credentials: omit完全不带。小知识会话固定Session Fixation、CSRF 的本质都和浏览器自动携带 Cookie有关服务端为了防 CSRRF会要求请求带一个同步令牌即后面说的x-头这个令牌通常通过meta namecsrf-token或首页接口下发不能靠猜。3. 反爬请求头x-lollipop/x-marshmallow最关键的一步用「Copy as fetch」对比成功请求和自己发的请求发现多了两个奇怪的头x-lollipop: 一段随机串 x-marshmallow: 空串或短值把它们补上就能通过一段时间又失效。这是前端反爬里非常经典的做法页面加载时JS 根据 UA、Canvas 指纹、时间戳、行为数据算一个签名后续所有 XHR 都要求带这个签名头服务端校验签名是否合法、是否过期、是否复用。为什么是自定义x-头而不是 CookieCookie 会被浏览器自动附加攻击者很难篡改自定义头需要前端 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 里是?page0limit50不是page1。第一次循环写成page1直接少抓一页。这种细节只能靠 Network 面板看真实请求不能靠想当然。(b) CSV 加 BOM 头Excel 才认中文newBlob([\ufeffcsv],{type:text/csv;charsetutf-8})\ufeff是 UTF-8 BOM。Excel 在没有 BOM 的情况下会按系统本地编码中文 Windows 是 GBK去猜 UTF-8 文件导致乱码。加这一个字符中文列名就正常了。© 表格文本解析的坑最开始我们用document.querySelector(table).innerText再按换行切分。结果解析出 800 多行假数据——因为每个单元格内部本身有换行innerText会把单元格内的br也输出成\n导致一行数据被切成多片。正确做法是直接遍历 DOMdocument.querySelectorAll(tr).forEach(tr{constcells[...tr.querySelectorAll(td)].map(tdtd.innerText.trim());});DOM 是结构化的tr就是一行td就是一列不会错。四、脱敏后的脚本asyncfunctionexportRank(){constsleepmsnewPromise(rsetTimeout(r,ms));functionparseRows(){constrows[];document.querySelectorAll(tr).forEach(tr{consttdstr.querySelectorAll(td);if(tds.length4)return;// 跳过表头rows.push([...tds].map(tdtd.innerText.replace(/\n/g, ).trim()));});returnrows;}letallparseRows();// 翻到下一页页码按钮文本为 2constnext[...document.querySelectorAll(button,li,a)].find(elel.textContent.trim()2);if(next){next.click();awaitsleep(1800);allall.concat(parseRows());}// 只保留学号 阈值学号是N位数字字符串按字典序比较即可constTHRESHOLD2026XXXXXXX;consttargetall.filter(cells{constmcells.join( ).match(/20\d{10}/);returnmm[0].localeCompare(THRESHOLD)0;});// 导出 CSV带 BOMExcel 中文不乱码constheader排名,学号,姓名,总分,...各题得分...,开始时间\n;constcsv\ufeffheadertarget.map(rr.join(,)).join(\n);constblobnewBlob([csv],{type:text/csv;charsetutf-8});constadocument.createElement(a);a.hrefURL.createObjectURL(blob);a.downloadrank_filtered.csv;a.click();}exportRank();五、合规与边界最后必须提一句这个脚本跑在你自己已经登录、且有权查看的班级页面里导出的是合规范围内的数据没有绕过登录、没有爆破接口、没有对服务端造成额外压力脚本只在本地浏览器执行不向第三方发送任何数据。清楚这条线“技术上能做到” 和 “该不该做” 是两件事。同样一段代码用来导出本班级的作业情况是效率工具用来批量爬别人平台的数据就可能踩红线。六、一句话总结遇到反爬先别急着逆向签名。问自己一句这个数据我在浏览器里已经看得见了吗看得见就直接从 DOM 拿看不见再考虑打 API。
阅读完成 · 觉得有帮助?