☰
Fantia付费内容批量下载:Tampermonkey用户脚本实战指南
2026/9/25 6:01:21 网站建设 项目流程

简介:一款面向 Fantia 平台用户的 Tampermonkey 用户脚本,核心功能是在 Fantia 图片框上自动添加下载按钮,点击后即可把页面内全部图片打包成 ZIP 并下载保存,有效解决图片批量下载与离线归档问题。资源压缩包体积很小,共 2 个文件,分别是 1 个 JavaScript 脚本和 1 个 Markdown 说明文档,整体仅 3KB,结构非常精简,适合熟悉用户脚本机制的读者直接安装或按需修改。需要特别留意的是,作者经测试后明确建议在 Firefox 中运行,Chrome 可能因浏览器自身跨域图片的 CORS 限制而无法正常打包,相关提示已写入说明文档。目前已有 10586 人学习下载,脚本可从 Greasyfork 安装,源代码同步托管在 GitHub,兼顾使用与二次开发需求。对经常浏览 Fantia 并希望快速批量保存图片的用户而言,这是一份轻量且实用的工具资源。

1. 这个标题在解决什么:Fantia 付费内容的批量下载到底卡在哪

看到标题里的 Fantia-Downloader-tampermonkey,你多半是来给「在 Fantia 上把图批量存下来」找个省力方案,而 Tampermonkey 正是让这个方案跑起来的浏览器环境。问题是:Fantia 的创作者主页上,内容按帖子分发,图片要真金白银开订阅才看得到,浏览器既没有「整页一键存图」,右键一张张另存又累到怀疑人生。这个标题对应的路线,就是写一个贴着页面跑的自动化脚本,帮你把订阅权限内看到的图片批量落盘。它适合两类人:一是开了订阅想把内容留档的普通用户,二是帮创作者做备份归档的人。注意,它解决的是「重复劳动」,不是绕过权限——你的账号能看见什么,脚本才能下什么。

2. 环境准备:Tampermonkey 装好后,还有 3 个权限声明要确认

2.1 为什么不用现成下载器,而要一个脚本

先摆一个对比,你就明白为什么这类下载器全是「页面脚本」而不是独立软件。

方案登录态处理订阅权限识别跨域抓图上手成本
独立下载器要手动导 Cookie,过期还要重来不识别,靠你手动拿来链接可以,但要配很多源站规则高
浏览器扩展自带 Cookie,但要写 background 通信能识别,但权限模型较重可以中高
Tampermonkey 用户脚本天然复用页面 Cookie,零配置直接读 DOM,看到即允许靠 GM 系列 API低

关键差别在登录态。Fantia 的内容是按账号订阅关系开放的,独立下载器最常见的问题就是「抓回来一堆登录页的 HTML」。用户脚本跑在页面上下文里,浏览器已经把 Cookie、订阅关系都处理好了,脚本只是从渲染后的 DOM 里拿数据,踩坑面一下少了一半。另一个现实原因是:这类需求的分享方式几乎都是「扔一个用户脚本文件给你」,Tampermonkey 是事实标准,装脚本比装扩展更接近原作者的发布形态。

局限性也要说清楚。浏览器环境的脚本有内存和速度天花板,一次抓几百张图没问题,几万张就不适合了;另外脚本依赖页面结构,Fantia 改一次版,选择器就要跟着改。所以这篇文章讲的是「中等规模、自用留档」的靠谱做法,不是批量采集生产线。

2.2 装好 Tampermonkey 并添加第一个脚本的步骤

环境搭建本身不复杂,但权限声明容易漏。先按下面步骤走一遍:

  1. 在浏览器应用商店搜索 Tampermonkey(Chrome、Edge、Firefox 都有官方版本)并安装。
  2. 点击工具栏的油猴图标,选择「添加新脚本」,进入编辑器。
  3. 清空模板内容,粘贴下面的 metadata 块,然后按 Ctrl+S 保存:
// ==UserScript== // @name Fantia 页面下载助手 // @namespace local // @version 0.1.0 // @description 在 Fantia 帖子页面上注入批量下载按钮 // @match https://fantia.jp/* // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @connect fantia.jp // @connect cdn.fantia.jp // ==/UserScript==

这段头部信息有四个参数必须说明。@match决定了脚本在哪些网址生效,https://fantia.jp/*表示主站所有页面都跑,不需要精确到某个帖子路径——因为脚本要在帖子页注入按钮,在创作者主页也可能要干活。@grant是权限清单,GM_xmlhttpRequest 用于跨域拉图,GM_download 负责把文件写进磁盘,GM_setValue 和 GM_getValue 用来记住下载进度,缺哪个功能就报哪个错。@connect是跨域请求的白名单,主站和图片 CDN 都要写进去,具体 CDN 域名以你打开浏览器开发者工具「网络」面板里实际出现的为准,我这里写的是常见值。@run-at document-idle表示等 DOM 基本渲染完再执行,避免太早注入按钮被页面脚本清掉。

保存后脚本不会立刻出现在页面上,因为里面还没有实际逻辑。你可以在 https://fantia.jp 随便打开一个公开页面,点油猴图标,确认这个脚本显示为「正在运行」。如果没生效,多半是@match写错或者浏览器没刷新页面。

2.3 确认生效范围与登录态

脚本生效不等于能下载,还有一道隐藏门槛:登录态。打开任意一个你已订阅的创作者帖子页,在页面上右键「检查」,看 Elements 面板里帖子的正文容器是否存在。如果能看到付费内容对应的图片节点,说明脚本有资格干活;如果页面跳转到登录页或提示订阅过期,那脚本做什么都是白搭。

这里有个小技巧:Tampermonkey 的管理面板里可以打开「配置模式」,它会帮你分析当前页面被哪些脚本匹配、匹配了哪条规则。遇到「脚本装了但不出现按钮」的情况,先看这里,而不是去改代码。

2.4 权限不足时的报错特征

还有一种常见的「假失效」:脚本图标亮了,但点击按钮毫无反应。打开浏览器控制台(F12),如果看到GM_download is not defined或GM_xmlhttpRequest is not defined,说明@grant声明和实际调用不一致。Tampermonkey 的规则是:脚本里用到任何 GM_ 系列 API,都必须在@grant里显式声明,否则 API 是 undefined。曾有人为了省事把@grant none写在头顶,结果所有跨域请求全部失败,这是一个非常典型的玄学坑。

3. 跑通第一条下载链路:抓原图 URL、命名与落盘的最小脚本

3.1 页面里的图是分层的:缩略图、预览图和原图

用户脚本的本质是「把页面里已有的数据重新组织」,所以第一步要搞清楚 Fantia 的图片在 DOM 里长什么样。看一段典型结构:

<img src="https://cdn.fantia.jp/thumb/0001_thumb.jpg" >const toOriginal = (url) => { let out = url.replace(/\/thumb\//, '/contents/'); out = out.replace(/_(s|m)(?=\.\w+$)/, ''); return out; };

这个函数是示例,不是通用银弹——因为每个平台的命名规则都有差异。正确姿势是用浏览器开发者工具对比缩略图和原图的实际请求地址,找出路径规律,再写正则。你要记住的判断标准只有一个:还原后的 URL 能不能在浏览器里直接打开,能打开且尺寸足够大,才算还原成功。

3.2 注入下载按钮并收集 URL 清单

最小可用的脚本,先把「收集图片地址」这件事做对。下面是一段简化的采集逻辑:

(function () { 'use strict'; const collectImageEntries = () => { const out = []; const seen = new Set(); document.querySelectorAll('img').forEach((img) => { // 优先级:data-original 原图 >>const downloadOne = (entry) => new Promise((resolve, reject) => { GM_download({ url: entry.url, name: entry.filename, saveAs: false, timeout: 30000, headers: { Referer: location.origin + location.pathname, }, onload: () => resolve(), onerror: (err) => reject(err), }); });

参数含义要拆开讲。name是保存到磁盘的文件名,可以带相对路径,比如2025-archives/posts/123_001.jpg,浏览器会自动创建目录。saveAs: false表示不弹「另存为」对话框,直接存进浏览器的默认下载目录,批量下载时这是必须的,否则每张图都弹窗你根本点不过来。timeout设 30 秒,网络慢的图超过这个时间就报错重试。headers里带 Referer 是防盗链的常规操作,很多图站会校验来源,不带 Referer 的请求会被 403 拒绝。这个 Referer 建议用location.origin,因为图片也可能在创作者主页被加载,来源始终是 Fantia 主站域名。

为什么不推荐用 fetch + blob 的方式?因为 fetch 受 CORS 限制,图片 CDN 通常不会给你开放跨域头;就算用 GM_xmlhttpRequest 拿到二进制,再转 ObjectURL 下载,批量下几十张图时浏览器内存会一路飙升,还容易触发「此网站正尝试下载多个文件」的拦截。GM_download 是把下载任务交给浏览器下载管理器,内存压力小得多。真正的坑在于 GM_download 在 Firefox 里需要显式@grant GM_download,而部分 Chromium 内核浏览器对它的存在时代理了下载行为,表现是「文件秒下完但打不开」——这种情况我在第 5 章细讲。

命名规则也要提前定好,不要直接用图里的原始文件名。因为同一张图在不同帖子里的原始文件名可能一样,直接重名会互相覆盖。推荐模板:帖子 ID + 序号。帖子的 ID 可以从 URL 里取,比如https://fantia.jp/posts/123456就是 123456。拼接成123456_001.jpg这样,既保证唯一性,又方便回查是哪一帖的图。

4. 调到顺手再放量:并发、间隔、命名模板与过滤

4.1 配置面板与 4 个必调参数

脚本跑通之后,你面临的现实问题是:一个帖子几十张图,一次全下会卡页面;批量下会被浏览器拦;创作者主页往下滚还有更多帖子。所以要把几个关键参数抽出来,做成可配置项。我一般采用「脚本顶部常量 + 页面小面板」的组合,核心参数就四个:

参数建议默认值作用
maxConcurrent2同时下载的最大任务数,超过容易触发站点限流
requestInterval800ms每个请求完成后的等待时间,给服务器喘口气
timeout30000ms单张图的超时阈值,超时计入失败并重试
filenameTemplate{postId}_{index}.{ext}落盘文件名的格式,必须保证不同文件不重名

并发数默认只给 2,很多人上来就填 8,结果是请求发太快,服务器开始返回 429 或直接拒绝响应,整个队列连环失败。这个参数不是越高越好,你总觉得「慢了」,但实测中并发 2~3 配合 800ms 间隔,下载 200 张图的时间只比并发 8 慢 20% 左右,稳定性和失败率却好得多。间隔参数同理,它主要防的是「同一 IP 短时间高频请求被限流」,一旦限流,后面所有图都会失败,重试反而更慢。

4.2 过滤规则怎么写:日期、帖子、格式

有了参数,还要有过滤逻辑,不然脚本会把整个主页的公开图全拉下来。最常用的三条过滤规则:

  • 按日期:只下载最近 7 天或某个日期之后发布的帖子里的图。实现思路是页面里帖子列表会显示日期,把日期文本解析成时间戳后和当前时间做差,超过阈值就跳过。
  • 按帖子:只在当前打开的帖子页生效,创作者主页只列出图片清单但不自动下载。这个需求常见于「我只想备份某一位创作者的某一帖」。
  • 按格式:只下载图片,跳过视频和压缩包。因为有些帖子里挂着视频,GM_download 虽然能下,但视频文件动不动几百 MB,不适合用浏览器脚本批量拉。

过滤条件的判定要在收集阶段就做,而不是下载阶段。因为过滤掉的资源连清单都不要进,这样能保证下载队列干净。我给这组规则配置了优先级:日期 > 格式 > 帖子。也就是说,先按日期砍掉过期老帖,再对剩下的做格式筛选,最后如果启用了「仅当前帖」模式就直接锁死容器范围。

4.3 并发队列与断点记忆

批量下载的骨架是并发队列,配合断点记录才能保证中途挂断不从头再来。一段常用的实现:

async function runQueue(tasks, limit) { const results = []; const pool = []; let i = 0; const worker = async () => { while (i < tasks.length) { const index = i++; const task = tasks[index]; const done = GM_getValue('doneMap', {}); if (done[task.url]) { results[index] = { status: 'skipped' }; continue; } try { await downloadOne(task); // 内部带重试逻辑 const map = GM_getValue('doneMap', {}); map[task.url] = 1; GM_setValue('doneMap', map); results[index] = { status: 'ok' }; } catch (e) { results[index] = { status: 'fail', err: e }; } } }; for (let k = 0; k < limit; k++) pool.push(worker()); await Promise.all(pool); return results; }

这里有两个设计点值得解释。第一,断点续传的本质是「记住已成功下载的 URL」,而不是「检查本地文件是否存在」。浏览器脚本没有能力直接读磁盘目录,用 GM_setValue 把完成的 URL 记在本地存储里是最可靠的。下次跑同一批清单时,命中的 URL 直接跳过,实现幂等。第二,limit个 worker 共享同一个i指针,谁空闲谁拿下一个任务,避免任务分配不均导致部分 worker 空闲、部分顶着满负荷。

断点记录有一个边界坑:如果你改了命名模板,同样的 URL 下次会生成新文件名,但doneMap里还记着旧 URL,导致这次什么都不下。解决方法是把模板版本号也存进去,改模板时清空一次记录。GM_setValue 的存储量有限制,几万条记录没问题,几十万条就要考虑换数组缓冲了,自用场景基本到不了这个量级。

5. 常见问题与避坑:缩略图混入、拦截、漏图和权限失效

这个方案最气人的不是「下不下来」,而是「下下来了才发现不对」。第四张开始全是缩略图、文件全是 0 字节、滚动加载后直接漏一批——这些我全踩过。下面四条是按翻车频率排的,每条都按「现象 → 原因 → 解决」写清楚。

5.1 下回来的图全是几百 KB 的缩略图

现象:下载目录里文件都在,但凡是正文图片统统是 400 × 300 左右的小图,根本没法用。

原因:抓取优先级里img.src排到了最前面,而 Fantia 页面的缩略图铺得满屏都是。尤其是有 srcset 的图片,浏览器会根据视口宽度自动选一张合适尺寸的渲染,脚本看到的是渲染后的缩略图 URL,而不是原图链接。另一种情况是懒加载:>const observer = new MutationObserver(() => { const fresh = collectImageEntries(); if (fresh.length > 0) { queue.push(...fresh.filter(e => !seenSet.has(e.url))); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true });

注意attributes: true也要开着,因为有些懒加载是直接把>const dryRun = true; // 改 false 才会真正下载 if (dryRun) { const entries = collectImageEntries().map(e => ({ filename: buildFilename(e), sizeKB: '未请求', url: e.url, })); console.table(entries); } else { await runQueue(collectImageEntries(), maxConcurrent); }

6.2 选择器失效的自查两步

Fantia 一旦改版,最常见的症状是「按钮在,清单空」。此时不要急着改代码,先做两步自查。第一步,在浏览器控制台手动执行document.querySelectorAll('img[data-original]').length,看返回的数字是否大于 0。如果返回 0,说明style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询