多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

NSFC系统改版后结题报告批量下载失效?三步修复浏览器脚本

NSFC系统改版后结题报告批量下载失效?三步修复浏览器脚本 每年结题季对攒了一堆国家自然科学基金项目的人来说都是一场折磨。学校科研处催着交存档手里五六份结题报告要一篇篇从NSFC系统里下载可ISIS系统翻遍页面就是没有批量下载功能。2023年我写了一段浏览器脚本一键把列表页的报告抓下来当时用着挺顺手。2024年NSFC系统改版后脚本再跑直接失灵下载按钮点了没反应控制台里全是红色报错。这篇文章就是我那次完整抢救过程的记录不重新开发脚本手动修复下载按钮问题三步就能让批量下载重新跑起来。整个过程不需要装任何额外软件浏览器自带的开发者工具加一段控制台代码就能搞定适合所有被结题报告归档折磨的科研人员、科研秘书和实验室管理员。1. 先弄清2024年NSFC系统改版后下载按钮到底变成了什么样旧脚本失效第一反应不是急着改代码而是先搞清楚界面到底发生了什么变化。NSFC的ISIS系统在2024年做了一次比较大的前端重构肉眼可见的是页面风格变了、布局换了、登录后的功能菜单也被重新组织过。但真正让脚本报废的是藏在表面之下的三个变化DOM结构重写、事件绑定方式变更、下载接口调整。1.1 旧脚本为什么失效选择器、事件绑定、接口三个层面逐项排查老的下载按钮在HTML里长这样一个带固定id的a标签直接挂着下载接口的href地址脚本只需要一句话document.getElementById(downloadBtn).click()就能完成下载。新版系统改成了前端框架动态渲染的模式按钮id变成了随机字符串href属性消失取而代之的是一堆data属性和事件监听。如果脚本还在用旧的id定位元素拿到的就是null后面的click自然无从执行。即便选择器侥幸能抓到元素事件绑定方式也可能变了。老站点大量使用inline的onclick属性脚本直接调用click()等同于触发浏览器的原生点击流程新版前端框架用的是addEventListener挂载事件如果脚本还想着通过$(#btn).trigger(click)这类方式触发不一定能唤起框架内部绑定的处理函数就会出现元素找到了、代码没报错、页面没反应的怪象。再往下挖下载接口本身也可能调整了。旧接口直接返回PDF文件流浏览器接收到就可以保存新接口改成了POST请求参数做了签名加密用旧接口地址去请求多半会拿到403状态码或者一段JSON错误提示。要判断具体是哪一层出了问题最有效的办法不是猜而是开浏览器开发者工具按下面三板斧去排查。1.2 从开发者工具里快速锁定新下载逻辑的三板斧第一板斧是取元素。打开结题报告列表页按F12键进入开发者工具点左上角的箭头图标再点页面上的下载按钮Elements面板会直接定位到对应节点。这时候把外层HTML完整展开认真记录按钮的id、class、data属性以及外层包裹的容器结构。新版系统的按钮可能是Element UI风格的button classel-button el-button--small加上自定义的>button classel-button el-button--small>// 旧代码 document.getElementById(downloadBtn).click(); // 新代码根据data属性定位 document.querySelector(button[data-actiondownload]).click();这里有一个重点提示模拟点击有个前提按钮事件必须挂在原生DOM上且页面渲染完成后脚本才执行。如果换成新选择器后点了依然没反应就别在按钮上继续死磕直接改成调接口这一步往往才是真正的修复点。调接口的具体做法是在Network面板里找到下载请求右键点击该请求选择Copy Copy as fetch。浏览器会生成一段完整的fetch命令包含URL、请求头和方法直接粘贴到Console面板运行如果返回正常文件流就把这段fetch代码封成函数function downloadReport(reportId) { return fetch(/api/report/download?reportId${reportId}, { method: POST, headers: { Content-Type: application/json, X-CSRF-TOKEN: getCsrfToken() }, body: JSON.stringify({ reportId }) }) .then(res res.blob()) .then(blob { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 结题报告_${reportId}.pdf; a.click(); URL.revokeObjectURL(url); }); }CSRF token的获取方式需要留意。新版系统通常会在页面头部放一个meta标签内容类似meta namecsrf-token contentabc...在函数里读取就行function getCsrfToken() { const meta document.querySelector(meta[namecsrf-token]); return meta ? meta.content : ; }如果页面里没有meta标签打开Network面板里的任意一个POST请求从请求头里找X-CSRF-TOKEN或X-XSRF-TOKEN字段手动填进去也能用只是不如meta标签稳定。2.3 第三步控制台逐条验证再放量批量下载函数封装好后先在控制台单条验证一个这是很多人容易跳过但最关键的环节downloadReport(76123).then(() console.log(单条下载成功));确认文件正常落地后再放量。批量下载前先获取当前页所有报告的id集合。常见的方式是遍历所有下载按钮const ids Array.from(document.querySelectorAll(button[data-report-id])) .map(btn btn.dataset.reportId);然后逐条调用同时加延时避免请求太密触发限流async function batchDownload(ids, delay 1500) { for (const id of ids) { try { await downloadReport(id); console.log(已完成: ${id}); } catch (e) { console.error(失败: ${id}, e); } await new Promise(r setTimeout(r, delay)); } } batchDownload(ids);NSFC系统单页展示的记录数量有限如果报告量超过一页建议在页面底部把可加载的内容全部加载出来后再抓取id列表。有人可能想直接在脚本里循环翻页但那样涉及页面状态切换容易因为异步渲染没完成而抓漏不如手动加载完再批量跑操作成本和出错概率都更低。3. 修复过程中踩过的坑常见报错与排查思路修复脚本这事最费时间的从来不是改代码而是排查那些莫名其妙的报错。下面几个典型问题是我在2024版系统上实际遇到过的整理出来给后来人省点时间。3.1 Cannot read properties of null不是代码问题是时序问题脚本跑起来第一行就报Cannot read properties of null (reading click)十有八九是document.getElementById(downloadBtn)返回了null。新系统的按钮是异步渲染的页面DOM加载完成并不代表按钮已经出现在页面上。解决思路有两个一是把脚本放到页面完全加载之后再执行二是写一个等待轮询每隔200毫秒检查一次元素是否出现。我在修复时直接用了轮询方式代码里加一个waitForElement方法const waitForElement (selector, timeout 15000) { return new Promise((resolve, reject) { const startTime Date.now(); const timer setInterval(() { const el document.querySelector(selector); if (el) { clearInterval(timer); resolve(el); } else if (Date.now() - startTime timeout) { clearInterval(timer); reject(new Error(元素 ${selector} 在 ${timeout}ms 内未出现)); } }, 200); }); };这样脚本就会等按钮渲染出来后自动继续不再依赖手动刷新页面。实测下来网络慢的时候等待时间可能长达3到5秒15秒的超时上限基本够用。3.2 下载下来是HTML文件登录态失效的表现第二次踩坑是下载成功了但文件打开后是一段HTML源代码里面写着请先登录或系统错误。这说明请求虽然发出去了但服务端没有返回真正的PDF而是直接把登录页当响应内容返回了。原因通常是脚本请求时没有携带完整的登录凭证或者请求URL与Network里抓到的版本不一致。排查思路是核对请求细节。先确认URL是否与Network里的完全一致包括路径大小写和query参数顺序再检查Request Headers里是否带上了Cookie和User-Agent。用fetch请求时如果遇到跨域或凭证丢失在fetch参数里加上credentials: include多数登录态失效问题都能解决。这里再强调一句下载操作只对你自己的账号登录态有效不要用他人账号去尝试。3.3 批量触发风控拦截节流与随机化批量下载一旦跑得太快系统可能返回429状态码或者弹出滑块验证。这个太快没有固定标准我遇到过连续请求10个就触发的情况也见过一口气下载几十个没问题的。处理方法是在循环里增加随机延时每次间隔在1200到2500毫秒之间浮动模拟真人手工点击的节奏而不是固定间隔const randomDelay (min 1200, max 2500) new Promise(r setTimeout(r, Math.floor(Math.random() * (max - min)) min));除了延时还可以把下载顺序打乱避免每次都是同一个顺序访问。这套操作能显著减少被拦的概率但并不保证一定能绕过所有风控。如果系统限制严格稳妥的做法是减小批次分几天跑完别一口气全下完。4. 实在不想碰脚本给你两条临时备用路线有些朋友对写脚本确实没兴趣只是这一次要用不想折腾。这种情况下也有偷懒的办法效果一样过程更简单。4.1 用浏览器控制台手动循环点按钮如果只是偶尔下载几篇报告不用写完整脚本直接在浏览器控制台里写一个循环强制触发页面上所有下载按钮的click事件。在Console面板里粘贴这段就能跑const btnList document.querySelectorAll(button[data-actiondownload]); let idx 0; const timer setInterval(() { if (idx btnList.length) { clearInterval(timer); console.log(所有下载按钮均已触发); return; } btnList[idx].click(); idx; }, 2000);这样每隔两秒自动点一次下载浏览器按提示依次保存文件。注意每次点击后要等浏览器弹窗处理完再继续下一轮不然容易漏掉文件。这个方案的优点是零门槛复制粘贴就能跑缺点是保存弹窗需要人工逐个确认报告数量很多的时候依然费手。如果系统弹窗拦截了多个自动下载还需要在浏览器设置里临时允许该站点的多文件下载权限。4.2 把修复经验固化成配置化脚本建议以后还要长期跟结题报告打交道的人把修复经验固化下来。修复完这次问题后我把脚本里容易变的东西全部抽成了配置包括按钮选择器、下载接口地址、请求方式、参数名、延时范围全部放在脚本顶部的配置对象里。下次系统再改版只需要重新抓一次DOM和接口更新配置即可核心函数一行都不用改。这个习惯给我省了不少事科研管理系统这两年更新频率明显比前几年高每次都从头重写脚本确实太累。配置化的大致思路是const CONFIG { buttonSelector: button[data-actiondownload], reportIdAttr: data-report-id, apiUrl: /api/report/download, method: POST, csrfMetaName: csrf-token, delayRange: [1200, 2500], timeout: 15000 }; async function downloadReport(reportId) { // 核心逻辑全部基于CONFIG动态拼接 const url CONFIG.apiUrl.replace({reportId}, reportId); // ... 后续处理 }这样即使NSFC下次又把按钮class换掉你只需要更新第一行配置里的选择器就能继续跑。最后分享一点个人体会。这类科研管理系统的下载功能本质上是高频使用的需求但官方一直没做批量下载自己动手补全体验就成了必然选择。修复过程中最重要的是耐心看Network面板请求一抓问题基本就水落石出了。如果你也遇到类似情况建议先花十分钟做排查再动手改而不是上来就重写整个脚本。经过几次调教我现在已经能做到系统改版后半天内恢复下载功能批处理几十份报告也能稳定跑完。希望这篇记录能帮你在结题季少走点弯路。
返回列表