简介面向Fantia图片收藏、创作内容存档与素材整理用户这份Tampermonkey用户脚本能在Fantia的图片框内新增“下载”按钮点击后自动将整组图片打包为ZIP文件免去逐张右键保存的繁琐操作大幅提升批量收集效率。压缩包体积仅3KB共包含2个文件FantiaDownloader.js为可直接加载的脚本主体负责在图片框中注入按钮、抓取图片并生成ZIPREADME.md则补充安装指引、浏览器兼容性提示及GitHub源码入口。脚本支持从GreasyFork一键安装也可在GitHub上查看维护中的源代码便于审计行为或自行调整。需特别留意由于Chrome在跨域加载图片时存在CORS限制作者实测后建议优先在Firefox浏览器中运行以保证打包下载流程正常走通。目前已有10586人学习/下载适合Fantia深度用户、图片收集爱好者以及希望参考油猴脚本封装与ZIP生成思路的开发者资源虽小但即装即用也具备二次开发价值。1. Fantia-Downloader一个油猴脚本把网页看图变成批量下载器如果你在 Fantia 上追过创作者大概率经历过这种尴尬一张一张点开大图右键另存为遇到几十张的帖子手都酸了想图省事直接抓网页源码找图片链接结果拿到的全是压缩过的缩略图原图 URL 藏在某个请求里手工翻半天才找到。让「网页浏览器本身变成下载器」的关键不是装一个独立软件而是在 Tampermonkey 里跑一段 JavaScript 脚本也就是标题里那个 Fantia-Downloader。它属于用户脚本跟着浏览器走不用额外开程序不改变网站结构只是在页面加载完成后帮你识别媒体资源、按规则命名、触发浏览器下载。适合经常买订阅又不想在本地维护一堆爬虫代码的人也适合想把创作者历史内容整批备份的存档党。核心也就一件事把「点开看图」变成「勾选下载」而且能拿到原图级别的高质量文件不是页面展示的缩略图。2. 为什么用油猴脚本而不是独立爬虫从 CORS 到页面渲染的取舍2.1 Fantia 页面结构数据藏在哪脚本在哪个阶段介入要理解这个脚本的设计逻辑得先看 Fantia 的网页构成。Fantia 是一个创作者内容订阅平台帖子列表、图片、音频都靠 AJAX 异步加载首页 HTML 里只有骨架真正的下载地址是页面脚本通过请求 JSON 接口拿到的。这意味着普通爬虫必须模拟登录态、解析 token、处理多次重定向任何一个环节过期就翻车。油猴脚本的好处是直接跑在浏览器环境里登录态、Cookie、localStorage 都是现成的脚本只要在 DOM 渲染完成后挂上钩子去读取页面框架留下的数据即可。常见做法是拦截网络请求或者监听页面上的自定义事件。比如 Fantia 的页面框架在构建图片预览时会以 background-image 或 img 标签的 src 形式暴露媒体地址脚本抓住这些节点就能拿到一份「可见资源列表」。还有一个细节Fantia 的帖子详情页打开时滚动到页面底部会继续加载更多内容实际是触发了翻页接口。这时候脚本不能只处理当前 DOM 中已有节点还要监听滚动事件、MutationObserver 监听新增节点。我一般建议优先用 MutationObserver 而不是定时轮询因为轮询间隔设短了费性能设长了又漏资源而 DOM 变化回调是精准的。2.2 油猴脚本的权限配置文件GM_* API 与前端 fetch 的边界油猴脚本和普通页面脚本最大的区别是一组以 GM_ 开头的 API。Fantia-Downloader 这类工具脚本核心依赖是 GM_xmlhttpRequest、GM_download、GM_getValue 和 GM_setValue。第一个用于跨域请求浏览器原生 fetch 发到其他域名会被 CORS 拦但 GM_xmlhttpRequest 由扩展层面发起不受页面跨域限制只要在脚本头部的 metadata 块里声明对应权限就能用。第二个是直接触发浏览器下载任务支持指定文件名和保存位置。后面两个用来做本地缓存比如记录哪些文件已经下过避免下次重复拉取。这里要特别说明一件事很多人写下载脚本时习惯用 a 标签加 download 属性触发下载这在同源场景下没问题但 Fantia 的图片资源在另一套 CDN 域名下a 标签加上 download 属性往往不生效浏览器会直接打开图片而不是下载。所以这个项目选择走 GM_download 或者 GM_xmlhttpRequest 拿 Blob 再转 Object URL本质是绕开浏览器对 download 属性的跨域限制。实际使用中我倾向于让脚本优先走 GM_download省内存、落盘快只有需要自定义请求头时再退回 GM_xmlhttpRequest。提示不要在一份脚本里同时用原生 fetch 和 GM_xmlhttpRequest 混着下载同一个资源失败重试会出现重复命名或半截文件后续做断点会很啰嗦。选定一条路走到底。2.3 资源识别策略DOM 扫描、接口拦截与自定义事件哪个更稳资源识别是整个脚本的命门。最粗暴的是扫描 img 标签但 img 拿到的可能是网页压缩图不是原图稍好一点的是观察接口返回的 JSON 数据但需要把请求体里所有字段都摸清一旦网站改版就要重新适配还有一种思路是拦截 Blob 对象的 URL.createObjectURL 调用这样能抓住所有「即将被页面展示」的资源思路不错但代码实现容易误伤非媒体内容。Fantia-Downloader 这个项目走的路线是结合 MutationObserver 监听节点插入与自定义事件捕获。具体来说Fantia 的前端框架在渲染帖子内容时会触发内部事件脚本不管它内部实现只守住结果当包含图片地址的节点出现在 DOM 里脚本就收集它然后做二次清洗——去掉重复项、过滤成分比如排除头像、图标、按帖子 ID 分组。这种方案的稳定性比直接抓接口好得多因为接口字段是内部实现随时可变但 DOM 最终渲染结果一定是用户能看到的东西这是相对稳定的边界。有朋友问过我能不能直接分析请求 URL 的哈希值来判断图片分档比如文件名里带 resize。这个思路在 Fantia 上也成立平台确实会为不同尺寸生成不同路径但我不推荐写死这个规则因为一旦平台调整路径命名脚本就静默失效。更稳妥的做法是始终取当前页面渲染出来的最大分辨率版本并以这个为基准做去重。3. 把脚本装进浏览器并跑通第一次下载配置参数与权限清单3.1 新建用户脚本metadata 块里的权限声明不能省安装油猴脚本不像装普通软件不是双击就完事。你要在浏览器扩展栏里打开 Tampermonkey 面板点击「添加新脚本」编辑器里会出现一段默认模板其中 / / UserScript / / 和 / / /UserScript 之间的内容称为 metadata 块这个块里声明的权限决定了脚本能用哪些 API。Fantia-Downloader 这类脚本至少需要声明 match匹配的网站、grant授权的 GM API、connect允许跨域请求的域名这几项。如果漏掉 connect 指定域名GM_xmlhttpRequest 请求 CDN 会被扩展拒掉现象是控制台报错但页面看起来一切正常这个坑相当隐蔽。我常用的最小配置块长这样// UserScript // name Fantia-Downloader // namespace http://tampermonkey.net/ // version 1.0 // description 从 Fantia 帖子页抓取原图与音频批量命名并下载 // author you // match https://fantia.jp/* // match https://*.fantia.jp/* // grant GM_xmlhttpRequest // grant GM_download // grant GM_getValue // grant GM_setValue // grant GM_notification // connect fantia.jp // connect *.fantia.jp // connect * // require https://code.jquery.com/jquery-3.6.0.min.js // /UserScript逻辑说明match 限定脚本只在 Fantia 域名下运行避免在其他网页上意外触发grant 声明的那几项缺一不可GM_xmlhttpRequest 负责跨域拿文件GM_download 负责实际写盘GM_getValue 和 GM_setValue 用来保存下载进度GM_notification 用于结束时的系统通知。connect 写 * 是图省事生产环境建议按实际抓到的 CDN 域名来填比如某些图片域名是 img.fantia.jp 这种那就填 img.fantia.jp权限最小化更安全。参数说明require 里引入 jQuery 只是个人习惯如果你不想引入外部依赖完全可以用原生的 querySelectorAll 替代脚本体积更小。版本号建议每次改动递增 0.0.1否则 Tampermonkey 会认为脚本没更新不拉取新代码。3.2 核心下载流程从页面采集到触发下载的完整链路脚本的主流程可以拆成四步采集媒体节点、清洗 URL 列表、拼接文件命名、逐条触发下载。采集部分用 MutationObserver 监听帖子内容区域每次新增节点就做一次过滤把图片和音频地址收进一个 Set 里确保去重。命名部分建议用「创作者名_ID_序号」这类结构比如 fanmi_12345_01.jpg这样后续排序和归档都很直观。以下是我拆过的实际实现中一段可复用的采集与去重逻辑const mediaSet new Set(); const observer new MutationObserver(() { document.querySelectorAll(img[src*fantia], img[src*cdn], audio source).forEach(el { const url el.currentSrc || el.src; // 过滤掉小图标、头像等非内容资源 if (url.includes(icon) || url.includes(avatar)) return; if (url url.startsWith(http)) { mediaSet.add(url); } }); }); observer.observe(document.body, { childList: true, subtree: true });逻辑说明currentSrc 优先于 src因为 video 和 audio 元素在浏览器加载完媒体信息后currentSrc 才是实际生效的地址尤其当页面里有多个 source 标签做自适应切换时拿 src 可能取到空值或占位符。过滤条件里的 icon 和 avatar 是 Fantia 页面里常见的路径片段实际使用时你要根据自己看到的 URL 结构去调整过滤规则不同平台的静态资源路径差异很大。参数说明observer.observe 配置里的 childList 和 subtree 必须同时为 true前者监听直接子节点增减后者监听所有后代节点只开 childList 会漏掉深层嵌套的图片。这里没有用属性监听 attributes: true是因为图片 src 变化会触发节点属性变更但如果图片已经存在、只是换了地址属性监听会重复触发配合判断逻辑更复杂让它在 catch 阶段重新扫描更省事。下载触发的标准做法是逐个调用 GM_download每次传 url 和文件名。但浏览器对并发下载数有限制一次性丢 50 个任务会被浏览器拦下来需要做一个并发控制比如每批 5 个、间隔 800 毫秒。我通常会再加一个队列机制失败的任务自动重试一次重试仍失败就记录到日志数组里最后一次性输出。这样可以避免网络闪断导致整批中断比连续 fetch 的方式抗摔很多。3.3 第一次运行路径打开帖子页、控制面板、验证文件落地脚本装好后打开任意一个 Fantia 帖子详情页Tampermonkey 图标上会显示脚本在该页面生效。这时按下 F12 打开开发者工具在 Console 面板里应该能看到脚本打印的「检测到 N 个媒体资源」之类日志。如果没看到优先检查油猴脚本是否启用以及 match 规则是否真的匹配当前页面地址。通过之后脚本默认会在页面右下角渲染一个半透明的小面板上面列出当前帖子的图片数量、总大小估算、一个「全部下载」按钮和「仅下载原图」的开关。点一次「全部下载」脚本会逐个执行 GM_download底部面板里能看到实时进度。下载完成后浏览器默认会弹窗询问保存位置如果你不设置可能下载几十张图片后弹窗几十次这是个很影响体验的点。解决方式是脚本在初始化时引导用户设置一次「自动保存到指定文件夹」——Chrome 的设置项里可以开启这个选项但油猴脚本没法直接改浏览器设置只能你在第一次下载时手动勾选「对类似文件自动执行操作」并选择目录后续再下载就不会反复弹窗。这一步不是脚本代码能覆盖的但确实决定了这个工具好不好用把它当成安装流程的一部分来看待。走完这一遍流程你手头就有了一个能批量下载 Fantia 媒体资源的工具接下来真正耗时间的是遇到问题怎么排查。4. 避坑与排查从脚本没反应到文件名全乱码的修复记录4.1 现象点击下载按钮后没有任何反应控制台也不报错原因最常见的是油猴脚本的 GM_download 和页面自身的 window.URL 冲突尤其是页面里也定义了 URL.createObjectURL 的环境下脚本调用的 GM_download 被页面重写覆盖指向了原生下载流程。另一个高频原因是浏览器的「自动下载」权限被拦截Chrome 会提示「此网站尝试下载多个文件」需要用户在地址栏右侧手动允许不点这个允许的话后续所有 GM_download 调用全部静默失败。解决先查地址栏右侧的下载图标是否有拦截提示点「允许」后再试。如果允许后仍然没反应打开控制台输入 GM_info检查脚本是否真的执行到下载分支。还有一种场景是 match 规则里写了 https://fantia.jp/* 但漏掉了 www 前缀导致页面实际地址是 https://www.fantia.jp 时脚本根本不注入这种情况连日志都看不到属于典型的「脚本没生效」而不是「脚本报错」。4.2 现象下载下来的图片打不开文件大小只有几百字节原因这是典型的「拿到了错误响应」而不是「没拿到数据」。常见误用是用原生 fetch 去请求跨域图片服务器返回的是登录跳转页或 CORS 错误页的 HTML 内容你把它当图片保存了。表面看文件后缀是 .jpg实际内容是文本。解决换用 GM_xmlhttpRequest 并设置 responseType: blob再通过 Blob 转 Object URL 触发下载。这段代码里要额外加一个校验逻辑——拿到 blob 后检查它的 type 字段是否以 image/ 开头不是就丢弃并记录失败。不要盲目信任 URL 的后缀名Fantia 有些动态生成的地址末尾根本没扩展名判断文件类型必须靠响应头或 blob.type。GM_xmlhttpRequest({ method: GET, url: imageUrl, responseType: blob, headers: { Referer: https://fantia.jp/ }, onload: function(res) { const blob res.response; if (!blob || !blob.type.startsWith(image/)) { console.warn(非图片响应可能被拦截, imageUrl, blob blob.type); return; } GM_download({ url: URL.createObjectURL(blob), name: fileName, saveAs: false, onerror: function(err) { console.error(下载失败, err); } }); }, onerror: function(err) { console.error(请求媒体失败, err); } });逻辑说明Referer 请求头是反盗链的关键。Fantia 的 CDN 会校验请求来源直接从油猴脚本发请求不带 Referer 的话部分图片会返回 403。这里手动把 Referer 设成 https://fantia.jp/ 模拟页面环境。另外把 blob 转成 Object URL 后传给 GM_download而不是直接把 imageUrl 传给 GM_download是为了避免 GM_download 内部再走一次网络的步骤少一层网络就少一层失败概率。参数说明saveAs: false 表示不弹「另存为」对话框直接按 name 字段保存到浏览器默认下载目录这是保证批量下载不被打断的关键。onerror 回调里至少要把 err 打出来看一眼很多问题是重试 N 次都失败其实错误码早就提示了本质问题——比如 403 是防盗链、404 是地址过期、500 是服务器临时故障三者处理策略完全不同。4.3 现象重复点击多次下载每次还能拿到不同数量的文件原因MutatonObserver 注册后没有手动断开第一次进入页面时绑定一个观察器退出帖子页再回来又绑定一个旧观察器仍然存活并继续采集新增的节点会被多个观察器同时捕捉采集结果里自然多了大量重复 URL。另一个原因是脚本没有做「采集完成后再下载」的状态切换用户可能不小心在采集还在进行时就点了下载导致只拿到当前已发现的资源滚动页面后发现还有一部分没下到。解决在每次进入详情页时先调用 observer.disconnect() 断开旧观察器再新建确保始终只有一组监听。同时给下载按钮加一个状态变量isCollecting 为 true 时按钮置灰并显示「采集中…」数据量稳定后再允许点击。另外建议采集阶段做两个去重维度URL 去重和文件名去重。URL 一样但文件名不同的情况也要拦截比如两张图片的请求地址相同但 CDN 加了随机参数导致每次 URL 都不同这时候按 URL 去重就没意义了要以 DOM 节点的唯一标识做基准。4.4 现象脚本运行正常但下载下来的文件名全是乱码原因Fantia 的帖子标题和创作者名包含日文假名、全角符号、emoji浏览器保存文件时如果文件名超出系统文件系统支持的范围会自动替换成乱码字符。另外 Windows 的路径长度限制是 260 个字符如果文件名里拼接了很长的帖子标题系统会自动把后半部分截断然后文件名和扩展名都被打乱。解决在拼接文件名时集中做一次净化把非 ASCII 字符替换成拼音或保留 ID或者直接不做标题拼接只用日期加序号命名。我一般这么处理——日期用帖子发布时间序号用三位数补零比如 20250112_001.jpg这样不仅避开乱码排序也顺。对创作者名如果本地确实需要保留可读性建议做一次 Unicode 规范化后再拼接。还可以搭配 GM_getValue 记录当前帖子已下载的序号下次再下同一帖子时从最大序号继续递增避免覆盖。4.5 现象下到一半显示全部成功却发现少了最后几张原因滚动加载的页面场景下下载启动时只采集到当前可视区域内的资源更后面的内容还没触发加载脚本没有等待采集完成就开始下载。此外 Fantia 的帖子可以包含「付费解锁」和「公开预览」两部分未解锁部分的图片地址对访客返回的不是真实图片而是一张提示付费的占位图脚本照单全收自然等于白下。解决下载流程前置一个「等待悬浮加载完成」的步骤。常见做法是在帖子页顶部自动触发滚动到底部再回到顶部触发懒加载直到连续多次滚动后资源数量不再增加才认为采集完成。付费内容的处理要看具体情况——如果脚本只适配已购买登录账号通过 GM_xmlhttpRequest 带上 Cookie 访问接口能拿到高清图如果访问权限本身不够建议在日志里标识「跳过受限资源」而不是把它当成失败任务反复重试。注意受限资源和加载失败是完全不同的两个状态前者重试一万次也不会成功。把「受限」「失败」「跳过」三种状态分开打日志排查效率会高非常多。5. 进阶玩法从「能下载」到「会下载」的配置细节5.1 命名模板与自动归档不靠人肉整理所谓进阶用法核心是让脚本产生的文件不落进浏览器默认的「下载」目录里堆成山。本地文件管理这件事油猴脚本做不了全局文件移动但可以通过文件名前缀把同创作者的内容集中放到一个目录——如果你用支持按路径存取的下载工具文件名里的路径分隔符可以触发自动建目录比如Fantia/CreatorName/20250112_001.jpg这种命名格式配合浏览器「自动按来源分文件夹」的功能基本能做到零整理。注意 Windows 文件名里不能包含反斜杠作为路径分隔符的实际问题得用正斜杠浏览器下载时会做兼容处理。5.2 增量下载与断点续传记住上次下到哪另一个值得投入的方向是增量下载。作者账号和帖子 ID 是天然的主键你可以用 GM_setValue 以帖子 ID 为键、以已下载图片列表为值存成一个 JSON 数组。下次打开同一帖子时采集完媒体地址后先跟缓存比对已存在的直接从待下载列表里移除。这个能力在追更场景下特别管用作者每天发帖但你不用每次都全量拉一遍只需要把新增的几篇下下来就行。断点续传还要考虑半截文件的处理。GM_download 失败时不会留下任何记录但某些情况下文件已经在磁盘上写了一半。判断方法是每次下载前检查本地缓存里的记录是否存在「完成」标记如果只有「开始」没有「完成」那多半是中断了应该重新排队。这个逻辑可以用一个简单的状态机实现每个任务有 pending / started / done / failed 四个状态。function getTaskState(postId) { const key task_ postId; const state GM_getValue(key, {}); return state; // { started: [], done: [], failed: [] } } function markDone(postId, filename) { const state getTaskState(postId); state.done.push(filename); GM_setValue(task_ postId, state); }逻辑说明这里把每个帖子的下载进度单独隔离成一个小存储块状态数组里存的不是 URL 而是文件名因为 URL 可能变动文件名才是最终落地的实体。每次标记 done 时把文件名记进去下次扫描时如果发现同样的文件名已经存在于 done 数组就跳过对应的 URL。如果某张图下载失败failed 数组里会留下它你可以在管理面板里单独列出一个「失败重试」按钮只拉取这些失败的资源不影响其他内容。参数说明GM_setValue 存储容量有上限虽然通常够用但如果订阅的创作者发布了上千个帖子缓存数据可能膨胀到几 MB。这时候要考虑裁掉历史记录只保留最近 200 条帖子的状态旧记录用我们前面说的 GM_listValues 批量清理掉防止脚本变卡。5.3 把日志输出成可读报告发现哪些下载被跳过你可能觉得日志是给开发者看的校正阶段没那么重要。但 Fantia-Downloader 这类资源类工具下载过程被跳过的情况远比想象多有反爬限流、有付费内容无权访问、有 CDN 404。如果脚本不把跳过原因记录清楚你很难判断是作者删了图还是脚本出了问题。我的习惯是在每次下载任务结束时把结果拼接成一行文本例如「2025-01-12 14:03 | SUCCESS | post_12345 | 001.jpg」写入一个叫 fantia_download_log 的 GM 变量里脚本面板里提供复制按钮方便粘贴到表格里归档。日志里还可以加一个「资源完整度」的统计字段把页面声明数量和实际下载成功数量做对比差距超过阈值就在通知里提醒。这么做之后碰到图片缺失你就能快速判断是平台端失效还是下载端跳过不用重新抓一遍页面数次数。这也是 Fantia-Downloader 这类脚本给从业者最大的启发浏览器的环境本身就是最好的调试平台把采集、下载、校验、日志闭环地跑在一个脚本里能省掉一整套后端爬虫基础设施。从那以后我每次写新的下载脚本都强制走一遍「权限声明、资源采集、去重校验、状态持久化」四步宁可先把日志结构写明白也不急着去碰下载 API。这个习惯帮我规避了至少三四次因为接口改版导致的整批失效问题希望帮到你。本文还有配套的精品资源点击获取