简介:面向Fantia创作者与订阅者,这款JavaScript编写的油猴脚本能在图片框上直接生成下载按钮,一键将整套图片自动打包为ZIP保存,解决批量收藏时逐张另存效率低下的痛点。资源压缩包仅3KB,共2个文件,其中主脚本负责注入按钮与流式打包,配套Markdown说明文档则写清了安装步骤、使用方式及浏览器兼容性提醒。已有10511人学习使用,说明其在相关人群中具备实用价值。脚本推荐在Firefox中运行,因Chrome跨域加载图片存在CORS问题,文档中对这一坑点做了明确提示;同时附有GitHub源码地址,方便开发者阅读实现逻辑、自行修改或提交问题,对想了解油猴脚本与ZIP生成技巧的前端爱好者也有参考意义。 用 Fantia 的朋友大概率都经历过这种手酸时刻:喜欢的创作者连着更新好几组作品,每张都想存,得先点开原图、右键、另存为、换个目录,再回来点下一张。要是哪天更新了 30 张图,这半小时基本就交代了,而且还不保证存到的是原图还是压缩水印版。我写这个 Fantia-Downloader 油猴脚本,就是为了把这段重复劳动彻底砍掉:装上 Tampermonkey,把脚本挂上去,打开帖子点一下,页面里的图片和附件就能批量落盘,文件名自动按规则排好,下载完直接进文件夹整理。
这个项目本身不复杂,但做下来踩了不少坑。这篇文章会把脚本的设计思路、核心实现逻辑、以及在真实页面环境下遇到的那些"文档里不会写"的问题,全部摊开讲一遍。适合两类人看:一类是正在用或想用类似脚本的读者,可以了解它到底是怎么工作的、哪些地方容易出问题;另一类是自己想写油猴脚本处理其他平台下载需求的开发者,这套思路换个选择器、改改匹配规则就能复用。
1. 为什么是油猴脚本,而不是专门做一个下载器
先回答一个很多人会问的问题:Fantia 的内容保存需求,市面上不是没有通用下载器,为什么要用油猴脚本自己造轮子?
1.1 登录态和权限才是最麻烦的坎
大多数独立下载器走的是"你喂给我一个链接,我去抓页面"的模式。可 Fantia 这类创作者平台的绝大部分内容,只有登录用户才看得到,付费订阅部分还得带着你的购买身份才能访问。下载器一旦脱离浏览器环境,就得自己去处理 Cookie、会话有效期、请求头校验,这套东西做起来费劲,还特别容易因为站点更新而失效。
油猴脚本跑在浏览器页面里,天然继承了你的登录态。脚本不需要知道你的密码,也不需要手动去复制 Cookie,它只是在你已经打开的页面上做"信息提取+文件请求",身份验证这件事完全交给浏览器本身完成。看起来轻描淡写,但这恰恰是脚本方案最稳的核心原因。
1.2 部署成本趋近于零
用户侧只需要装一个 Tampermonkey 浏览器扩展,然后一键导入脚本,刷新页面就能看到脚本注入的下载按钮。不需要装 Python 环境、不需要配 Node.js、不需要理解命令行参数。这对非技术用户非常友好——Fantia 的受众很多是插画、漫画、音声爱好者,他们的诉求就是"我要把图存下来",不该卡在环境配置这一步。
开发者侧的迭代成本也低。改完脚本,浏览器里点一下"重新加载",立刻就能看到效果。不像独立下载器,改一次请求逻辑还得重新打包、发版、等用户更新。
1.3 它的边界:只处理"你本来就能看到"的内容
需要强调一点,这类脚本的本质是"将页面上已授权给你展示的内容,以本地方便管理的格式保存下来",它不是破解工具。免费公开内容、你已经订阅过的付费内容、创作者公开的素材包,这些都属于"页面允许你访问"的范围。脚本不负责绕过任何权限校验,也做不到绕过——它拿到的数据,跟你在浏览器里亲眼看到的是同一份。把边界划清楚,后续做功能时才不会往歪路上走。
2. 脚本核心架构:从页面 DOM 到磁盘文件
整个脚本的逻辑链路可以拆成四段:注入页面、收集目标文件地址、构造下载任务、按顺序落盘。每一段都有独立的策略选择和隐藏细节。
2.1 注入时机决定了一半的稳定性
脚本通过用户脚本元数据声明匹配规则:
// ==UserScript== // @name Fantia Downloader // @namespace fantia-downloader // @version 1.0.0 // @description 抓取 Fantia 帖子页面中的图片与附件文件 // @author you // @match https://fantia.jp/* // @grant GM_download // @grant GM_xmlhttpRequest // @run-at document-end // ==/UserScript==@match用通配符覆盖整个站点域,因为脚本需要同时处理帖子详情页、粉丝主页、文章归档页几种场景。@run-at document-end表示在 DOM 解析完成后注入,这时候页面主体结构已经存在,能直接查到内容区的容器节点。
但 Fantia 的很多内容是懒加载的,这意味着"DOM 解析完成"不等于"所有图片都加载了"。实际开发时,只靠注入时机不够,脚本内部还要监听页面变化,后面会专门讲。
2.2 收集文件地址:三条路径组合着用
页面里的图片地址,用最朴素的办法就能拿到一大部分:
function collectImageUrls(root) { const results = []; const nodes = root.querySelectorAll('img'); nodes.forEach(img => { const url = img.dataset.src || img.src; if (url && url.startsWith('http') && !url.includes('avatar')) { results.push(url); } }); return [...new Set(results)]; }这里有个细节:很多平台的图片标签会先把真实地址放在>function fetchAsBlob(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', onload(res) { res.response ? resolve(res.response) : reject(new Error('empty response')); }, onerror: reject }); }); }
我的实践结论是:图片优先走GM_download,附件文件走GM_xmlhttpRequest。原因后面在"踩坑记录"那一节展开。
2.4 任务队列:别一股脑全塞给浏览器
收集到 50 个文件地址,直接对每一个地址都发起下载,浏览器会瞬间建立大量并发连接。表现是下载列表同时弹出几十个任务,磁盘和网络双双吃紧,甚至触发浏览器的并发连接限制,让后半程全部失败。
脚本里用一个简单队列控制并发数,同一时间只跑 2 到 3 个下载任务,每个任务之间加 300 到 500 毫秒的延时。这样页面不会卡死,下载成功率反而更高。
3. 文件命名的讲究:一个看着不起眼但决定体验的环节
下载脚本做了几个月后发现,用户反馈最多的问题往往不是"下载失败",而是"文件名一团乱"。Fantia 页面上图片的原始文件名通常是随机哈希串,比如202ab3c4d5e6f7.jpg,几十张图下到文件夹里,根本分不清哪张是哪张。这个问题的解法不复杂,但细节非常多。
3.1 命名规则:帖子标题加序号
推荐的文件名结构是:
[帖子编号] 帖子标题 - 序号.扩展名function buildFileName(postId, postTitle, index, ext) { const cleanTitle = postTitle .replace(/[\\/:*?"<>|]/g, '') // 去掉文件名非法字符 .replace(/\s+/g, ' ') .trim() .slice(0, 80); // 限制标题长度,避免超过文件系统全路径上限 return `[${postId}] ${cleanTitle} - ${String(index).padStart(3, '0')}.${ext}`; }去掉非法字符这步很容易漏。Windows 文件名不能用\ / : * ? " < > |,而 Fantia 的帖子标题五花八门,半角问号、全角冒号全都能出现。不处理的话,脚本会下载得很欢快,然后浏览器弹出一堆"文件名无效"的错误。
另外,标题截断到 80 个字符是实践中的平衡值。太短会丢失辨识度,太长又会让整个文件路径超过 Windows 的 260 字符限制,尤其是保存嵌套目录较深的时候。
3.2 序号补零解决排序问题
String(index).padStart(3, '0')看起来是个小动作,但很重要。如果序号是 1、2、3 …… 10、11,文件夹里的排序会变成 1、10、11、2、3,阅读顺序全部乱掉。补到三位数后就是 001、002、003,排序永远正确。图片超过 999 张的情况基本不存在,三位数足够。
3.3 避免重复下载:记录已保存文件清单
下载不是一次性的操作。今天看了帖子存了一次,过几天创作者更新了楼层内容,你可能想再跑一次。如果脚本无脑把全页面重新下一遍,会出现大量重复文件。
我的做法是:脚本在下载前,把已成功的文件名列表读出来,下次运行时做一次对比,跳过已存在的文件。这个逻辑依托于文件名里包含的帖子编号和序号——只要命名规则稳定,去重就是天然可靠的。
当然,本地文件系统里存一个 manifest 清单会更稳妥,但日常使用中,靠文件名去重已经足够省心。
3.4 原图与缩略图的识别优先级
页面里同一个图片内容往往存在多个尺寸版本。缩略图地址通常带有类似_thumb、_s、?w=300之类的特征,原图则没有或者尺寸参数更大。脚本在收集链接时需要做一轮过滤,只保留质量最高的版本。
一个可行的判断思路是,用图片地址里是否有尺寸后缀来筛:
function isLikelyOriginal(url) { return !/_(thumb|small|medium|s|m)\b/i.test(url) && !/w=\d+|h=\d+/.test(url); }这个正则不能覆盖所有情况,每个平台的命名规则不同,需要实际打开页面做一次地址对比才能确定。第一次调试脚本时,建议把收集到的地址先打印到控制台人工过一遍,确认哪些是原图哪些是缩略图,再固化过滤规则。
4. 开发实测中的坑与绕行方案
这节是整个项目最有价值的部分。诚实的讲,脚本的大体框架一个晚上就能写完,剩下八成时间全耗在"页面居然会这样"的意外上。
4.1 懒加载导致的"下载了一片蓝天白云的图"
第一次测试脚本时,下载下来二十多张图片,打开一看全是同一张占位图——灰色的背景上有个加载中的图标。原因就是懒加载机制:页面为了加速首屏渲染,真实图片地址放在>const observer = new MutationObserver(mutations => { const newUrls = collectImageUrls(document); if (newUrls.length > pendingCount) { queue.add(newUrls.slice(pendingCount)); } }); observer.observe(document.body, { childList: true, subtree: true });
4.2 原图地址藏在了一个不起眼的变量里
有段时间发现,部分帖子的胶卷式图片列表,DOM 里无论如何都找不到原图地址,>GM_xmlhttpRequest({ method: 'GET', url: imageUrl, responseType: 'blob', headers: { 'Referer': 'https://fantia.jp/' }, onload(res) { const objUrl = URL.createObjectURL(res.response); const a = document.createElement('a'); a.href = objUrl; a.download = fileName; a.click(); URL.revokeObjectURL(objUrl); } });
4.4 动态加载的楼层内容:只处理首屏的假象
Fantia 帖子页面里,创作者可以在评论区继续发图,这部分内容要点击"展开"按钮才会加载。如果脚本只在首屏收集一次地址,会漏掉评论区里的所有文件。
处理方式是:脚本注入一个"继续加载"的辅助函数,如果页面上存在展开按钮,就自动点击一次,等新内容渲染后再收集一轮地址。需要设置一个合理的等待时间,比如展开后等待 800 毫秒再执行下一轮收集。等待时间太短,新图片还没渲染完成;太长又影响用户体验。
4.5 文件名重复导致下载任务被覆盖
Fantia 上存在多楼层帖子,不同楼层里可能各有一张001.jpg。如果文件名只按序号命名,后下载的文件会直接覆盖先下载的。解决办法是命名规则里加入区块标识,比如楼层编号[01-003].jpg、[02-003].jpg,保证不同区域的同名文件不会互相冲突。
这个坑很隐蔽,如果只在单楼层帖子上做测试,永远发现不了。建议开发时用多楼层帖子做回归测试。
5. 合理使用边界与进阶扩展方向
脚本写到能稳定使用之后,我花了不少时间思考它的边界和后续可以继续做的事。这里是我的实际经验,供参考。
5.1 版权意识:下载不等于可以随意分发
Fantia 上的创作者靠订阅收入生存,付费内容更是他们主要的收入来源。脚本的定位应该是"方便自己存档、离线浏览",而不是把他人付费内容打包传播的工具。我在脚本的说明文档里也明确写了:不要将下载的内容上传到公开平台,不要二次销售,保留创作者的水印和版权标识,如果你真的喜欢某个创作者,优先去原平台支持他们。
这个边界不是客套话,而是脚本能否长期存在的寿命线——一旦被用来做侵权分发,无论是脚本还是使用者,都会面临不必要的风险。
5.2 功能扩展的方向:进度面板、筛选下载、打包压缩
当前版本的脚本已经能满足"点一下全部下载"的日常需求,但还可以扩展几个方向:
第一是下载进度面板。在页面角落注入一个小面板,实时显示"已下载 18/42""失败 2 个",失败项支持单独重试。这个功能对大量文件下载特别有用,能显著降低"下完不知道缺没缺"的焦虑感。
第二是内容筛选。有些用户只想要图片不想要音频和视频,有些用户只需要附件文件。脚本可以在收集完成后,按扩展名做一次分类展示,让用户勾选哪些类型需要下载。虽然不是必须,但用户体验提升明显。
第三是打包压缩。如果用户希望整个帖子保存为一个压缩包,可以用 JSZip 这个库在脚本里把下载的 Blob 数据打包,再一次性保存。注意这需要同时处理内存占用问题,楼层内容特别多的帖子,一次性全压会有卡顿风险,稳妥的做法是边下载边写入,控制同时驻留内存的文件数量。
5.3 给后来开发者的一句话建议
油猴脚本看起来简单,真正稳定的版本都是在真实页面上反复磨出来的。开发时请把"生产环境不同于测试环境"这句话刻在脑子里:页面结构会变、CDN 策略会变、浏览器权限策略会变,脚本想活得久,就得把地址收集、去重、命名、下载这些模块解耦开,任何一个环节出问题都能独立修复。
我自己的维护习惯是,每次 Fantia 页面上线新功能,第一时间打开脚本跑一轮测试帖子,确认四件事:登录态是否正常、收集的地址是否完整、命名规则是否仍然适用、下载是否全部成功。这套检查流程跑下来大概五分钟,却能让脚本省下大量被动修 bug 的时间。
这个项目真正让我觉得有价值的地方,不是"能把图下下来"这个结果,而是它把重复劳动一次性解决之后,省下来的时间又可以花在欣赏作品本身。如果你也有类似的内容浏览习惯,希望这篇文章里的设计和踩坑记录,能帮你少走几段弯路。
本文还有配套的精品资源,点击获取