ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

油猴脚本批量下载Fantia资源:注入原理、请求构造与打包落地

油猴脚本批量下载Fantia资源:注入原理、请求构造与打包落地 简介Fantia-Downloader-tampermonkey 是一款面向 Fantia 平台内容收藏者的浏览器用户脚本用于解决逐张保存图片繁琐、无法批量归档的问题。脚本会在 Fantia 的图片框上注入下载按钮点击后自动将整组图像打包为 ZIP 并保存到本地适合需要长期整理画师作品、建立个人素材库的用户使用。资源包共 2 个文件包含 1 个 js 脚本与 1 个 md 说明文档压缩包体积约 3KB轻量易部署配合 Tampermonkey 即可运行脚本可从 greasyfork 获取源码在 GitHub 上维护。需要特别注意的是作者明确建议使用 Firefox 安装运行Chrome 在跨域加载图片时存在 CORS 问题虽经多次尝试仍难以规避因此 Firefox 是更稳妥的选择。目前已有 10586 人学习下载说明该方案在同类需求中具备一定实用价值读者可直接获得可用的下载脚本与使用说明省去自行研究接口与打包逻辑的成本。1. 从 Fantia 批量下载的痛点说起为什么一个油猴脚本能省下整晚时间如果你订阅了 Fantia 上的创作者大概经历过这种场景打开投稿列表一张张点开图片、一个个右键保存遇到分页或者动态加载还得反复滚动一晚上下来手指酸了文件夹里还是乱七八糟的命名。Fantia-Downloader-tampermonkey 就是冲着这个场景来的——它是一个跑在浏览器里的油猴Tampermonkey用户脚本装好之后直接在 Fantia 页面上注入下载按钮把原本需要手动重复的操作变成批量抓取。这篇文章不讲空泛概念而是把这类脚本的注入原理、请求构造、批量打包、断点续传和常见翻车点拆开讲清楚让你能自己判断它值不值得用、怎么改、坑在哪。适合有基础前端或脚本经验、想省下重复劳动的人。2. 油猴脚本注入 Fantia 页面的原理与最小可跑框架2.1 为什么选 Tampermonkey 而不是写独立爬虫独立爬虫要处理登录态、Cookie、CSRF Token、动态渲染而 Fantia 的投稿页大量依赖前端渲染和登录会话。用 Tampermonkey 的最大好处是脚本运行在已经登录的浏览器上下文里天然带着有效 Cookie 和会话省掉了模拟登录这一大块最容易翻车的环节。你不需要去逆向登录接口也不用担心验证码脚本直接复用当前页面的document和fetch请求头和来源都和真实用户一致。另一个理由是维护成本。Fantia 的页面结构会变独立爬虫一旦选择器失效就得重新部署油猴脚本改完保存刷新即可生效调试链路短。常见做法是用match精确匹配投稿页和详情页只在需要的页面注入逻辑避免污染其他页面。2.2 脚本头部元数据与注入时机一个能跑的最小框架核心是脚本头部的match、grant、run-at三个字段。下面这段是可以直接抄的骨架// UserScript // name Fantia Downloader Helper // namespace local.fantia.helper // version 0.1.0 // description 在 Fantia 投稿页注入批量下载入口 // match https://fantia.jp/posts/* // match https://fantia.jp/fanclubs/* // grant GM_download // grant GM_xmlhttpRequest // run-at document-idle // connect fantia.jp // /UserScript (function () { use strict; // 等待投稿列表渲染完成再注入避免拿到空节点 const READY_SELECTOR .post-content, .fanclub-post; function waitFor(selector, timeout 15000) { return new Promise((resolve, reject) { const start Date.now(); const timer setInterval(() { const el document.querySelector(selector); if (el) { clearInterval(timer); resolve(el); } else if (Date.now() - start timeout) { clearInterval(timer); reject(new Error(timeout)); } }, 300); }); } waitFor(READY_SELECTOR).then(() { injectToolbar(); // 注入下载工具栏 }).catch(() console.warn([Fantia] 页面结构未匹配跳过注入)); })();逻辑说明match限定只在投稿和粉丝俱乐部页面运行run-at document-idle保证 DOM 基本就绪后再执行避免脚本跑在页面渲染之前。waitFor是一个轮询等待函数因为 Fantia 的列表可能是异步加载的直接querySelector很可能拿到 null。参数上timeout默认 15 秒网络慢可以调到 30000轮询间隔 300ms 是性能和响应速度的折中太短浪费 CPU太长注入延迟明显。提示grant里声明了GM_download和GM_xmlhttpRequest这两个是跨域下载的关键普通fetch在部分资源上会被同源策略拦住。2.3 定位投稿资源节点的选择器策略注入工具栏只是第一步真正要下载的是图片和附件。Fantia 的投稿内容通常包在若干容器里图片多为img或背景图附件是a链接。稳妥的做法是分层提取先找投稿容器再在容器内收集img[src]、a[href]过滤掉头像、图标这类噪声。function collectAssets(root) { const assets new Set(); // 正文图片优先取>function downloadBlob(url) { return new Promise((resolve, reject) { GM_xmlhttpRequest({ method: GET, url: url, responseType: blob, // 以二进制接收避免图片被当文本损坏 headers: { Referer: location.href, // 带上来源降低被拦概率 }, onload: res { if (res.status 200) resolve(res.response); else reject(new Error(HTTP res.status)); }, onerror: reject, ontimeout: () reject(new Error(timeout)), timeout: 30000 // 单文件 30 秒超时 }); }); }逻辑说明responseType: blob是必须的否则二进制图片会被当成字符串处理导致文件损坏这是新手最常见的翻车点之一。Referer设为当前页面地址模拟正常点击。timeout给 30 秒大附件可以调到 60000。失败时通过 reject 把错误抛给上层方便做重试。3.2 并发控制与失败重试别把浏览器打爆一次性发起几十上百个请求浏览器会排队甚至卡死服务端也可能限流。合理的做法是限制并发数比如同时最多 3 到 5 个失败后指数退避重试。async function batchDownload(urls, concurrency 3, maxRetry 2) { const results []; let index 0; async function worker() { while (index urls.length) { const i index; const url urls[i]; let attempt 0; while (attempt maxRetry) { try { const blob await downloadBlob(url); results[i] { url, blob, ok: true }; break; } catch (e) { attempt; if (attempt maxRetry) results[i] { url, ok: false, error: e.message }; else await new Promise(r setTimeout(r, 1000 * attempt)); // 退避 } } } } // 启动 concurrency 个 worker 并行消费队列 await Promise.all(Array.from({ length: concurrency }, worker)); return results; }逻辑说明这是典型的 worker pool 模式index是共享游标每个 worker 取一个任务处理完再取下一个天然实现并发上限。maxRetry控制重试次数退避时间1000 * attempt让第二次等 1 秒、第三次等 2 秒避免瞬间重试又被限流。参数上并发 3 适合普通网络带宽好可以到 5但不建议超过 8否则容易触发服务端风控。3.3 打包成 zip 并触发本地保存下载完一堆 blob 后逐个保存会弹很多次对话框。更好的做法是用 JSZip 打包成一个 zip再一次性保存。油猴环境里可以引入 JSZip 的 CDN 版本或者用require声明。// require https://cdn.jsdelivr.net/npm/jszip3.10.1/dist/jszip.min.js async function packAndSave(items, zipName fantia-batch.zip) { const zip new JSZip(); items.filter(it it.ok).forEach((it, i) { // 从 URL 提取文件名取不到就用序号兜底 const name decodeURIComponent(it.url.split(/).pop().split(?)[0]) || file_${i}; zip.file(name, it.blob); }); const content await zip.generateAsync({ type: blob }); const a document.createElement(a); a.href URL.createObjectURL(content); a.download zipName; a.click(); URL.revokeObjectURL(a.href); // 释放内存避免大包泄漏 }逻辑说明zip.file逐个加入文件名从 URL 末段解析并decodeURIComponent还原中文和特殊字符。generateAsync是异步的大包不会阻塞主线程太久。最后用临时a触发下载revokeObjectURL及时释放否则打包几百 MB 时内存会持续上涨。参数上type: blob适合浏览器直接保存如果要做流式大文件可以换type: uint8array分片处理。4. 避坑与排查那些让脚本白跑的细节4.1 下载下来的图片打不开或只有几 KB现象文件能下但打开是损坏的或者大小只有几百字节。原因请求时没设responseType: blob二进制被当文本处理或者拿到的是懒加载占位图。解决确认GM_xmlhttpRequest里responseType为blob并在收集阶段优先读>const CONFIG { concurrency: 3, retry: 2, timeout: 30000, selectors: { post: .post-content, .fanclub-post, image: img, attachment: a[href] }, fileExt: /\.(jpg|jpeg|png|gif|webp|zip|rar|7z|pdf|psd)(\?|$)/i };这样页面改版时只改selectors限流时只改concurrency不用动核心逻辑。另一个技巧是加一个「仅下载选中项」的勾选框避免每次都全量抓取——全量在投稿多的创作者页面上很容易触发风控。验证脚本是否可靠我的习惯是先拿一个只有几张图的投稿页跑通全流程确认文件能打开、命名正确、zip 完整再放到大页面上批量跑。别一上来就对着几百个文件测试出了问题你连是哪一步错的都定位不到。最后说个血泪经验任何依赖页面结构的脚本都有寿命Fantia 改版是迟早的事。与其每次改版后手忙脚乱不如把选择器和请求逻辑解耦再留一个控制台日志开关出问题时能快速看出是选择器失效还是请求被拦。这套思路不只适用于 Fantia换成任何需要登录态的前端批量抓取场景都能套。希望帮到你。本文还有配套的精品资源点击获取
返回列表