news 2026/10/13 7:53:17

浏览器插件脚本实现AI图片批量下载与自动去水印

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浏览器插件脚本实现AI图片批量下载与自动去水印

干过这件事的朋友应该都懂:用豆包攒了一堆生成图,单张手动保存还能忍,可作品一多,一张张右键另存为,再挨个处理右下角的小水印,手都能点抽筋。更气人的是,平台导出的图经常自带标识,发出去总感觉不够干净。这个需求我相信不是少数人的痛点,网上相关提问也不少,但大部分方案要么停留在“自己慢慢裁”,要么推荐各种不知底细的在线工具,实在谈不上顺手。

我花了一下午折腾了一套比较实用的思路,核心就用一个浏览器插件脚本,直接做到批量下载,顺带把固定在角落的水印区域自动处理掉,全程不用反复人工干预。这篇就把完整思路、脚本原理、实操步骤以及我踩过的几个坑一次性说清楚,照着做基本能省下大半重复劳动。

1. 需求拆解与方案选型:为什么首选浏览器插件脚本

1.1 先想清楚痛点在哪:水印和批量下载其实是两件事

很多人一上来就奔着“去水印”去,但真正用的时候你会发现,困扰你的其实是两个叠加的问题:

  • 图片数量多,下载动作重复,手动操作耗时严重。
  • 下载下来的图带平台角标,影响二次使用,需要额外处理。

这两个问题如果能一次解决,体验会好非常多。我当时把市面上常见做法都过了一遍:

方案路径优点缺点
手动一张张保存,再用画图软件裁剪零学习成本极慢,图一多就想砸键盘
在线图片批量下载网站操作简单稳定性看人品,有隐私风险,部分在线工具会压缩画质
浏览器F12手动找原图链接可控性强操作繁琐,非技术人员容易懵
浏览器插件脚本自动化一次配置长期使用,流程可复制需要了解最基本的脚本逻辑

对比下来,插件脚本这条路是投入产出比最高的。不用开发独立软件,只要借助现成的扩展运行环境,把“找链接、批量下载、批量去水印”这三件套串成一条自动化链路就行。日常维护成本也低,平台页面结构变化以后,改几行规则就能继续用。

1.2 一个插件轻松拿捏的核心逻辑

先别急着上手,我们得把技术原理掰开揉碎。

任何AI绘图平台在前端页面的运作方式都类似:生成过的图片会以缩略图列表的形式展示,点击之后进入大图预览。缩略图通常是压缩过的版本,原图则藏在预览接口或者下载按钮的背后。水印基本有两种处理逻辑:

  • 第一种,平台把水印直接合成到原图文件里,这种最麻烦,只能靠裁剪或局部覆盖解决。
  • 第二种,水印是前端在预览图上叠加的视觉元素,比如用CSS固定在图片角落,这种情况下只要拿到原始图片地址,本身就带无水印版本。

豆包实际更多像是前一种逻辑,生成的图片文件中会固定压一个角落标识。但这并不意味着我们要去做那种黑科技级别的“破解”——完全没必要。既然它位置固定、尺寸固定,我们用最朴素的办法处理就行:下载完成之后,把右下角或左上角的水印区域裁掉。因为AI生成的图片通常分辨率比较大,边缘裁掉一小条,对主体内容几乎没有影响。这也是很多设计师处理素材时常用的做法。

批量下载的自动化思路更直接。脚本在页面里自动扫描所有图片元素,把符合规则的原图地址提取出来,逐个触发下载。浏览器会询问“是否允许下载多个文件”,允许之后,所有图片就自动进到下载目录里。如果需要顺便处理水印,那就让脚本下载完以后,再利用本地图像库统一做一次边角裁剪。

1.3 为什么建议用油猴脚本(Tampermonkey)作为载体

提到浏览器插件,有人会想到自己开发完整扩展,这当然是种方案,但门槛高一些。我更推荐用油猴脚本,原因有三个:

  • 开发成本低到可以忽略,脚本本质就是一段JavaScript代码,直接写在编辑器里,保存即生效。
  • 不需要打包、发布、过审,自己本地用完全够。
  • 脚本只作用于指定的页面域名,不影响其他网站,权限可控。

当然,这段脚本我们不用写得特别复杂,核心就几件事:匹配页面、扫描图片、提取链接、批量下载、可选裁剪。稍微有代码基础的人马上能上手,完全没写过代码的朋友也不用怕,后面我会给出可以直接抄的代码和详细的安装步骤。

注意:整个方案的核心目的,是帮你更高效地管理自己账号内已生成的图片。使用时请确保只下载自己创作的内容,尊重平台规则和内容版权。

2. 插件脚本的设计与实现:从零写出一套顺手工具

2.1 脚本的基础骨架

先看一下整体结构,后面会逐步解释每一部分。打开编辑器,新建一个文件,保存成任意名字,后缀.js即可。下面是骨架:

// ==UserScript== // @name 豆包图片批量下载助手 // @namespace local.helper // @version 1.0.0 // @description 批量提取豆包生成的图片,带角落水印裁剪功能 // @match https://*.doubao.com/* // @match https://doubao.com/* // @grant GM_download // @grant GM_registerMenuCommand // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // 核心配置区 const CONFIG = { enableCrop: true, // 是否启用自动裁剪去水印 cropRatio: 0.02, // 裁剪比例,默认裁掉边缘2% allowMultipleDownloads: 10 // 允许同时下载的图片数量 }; // 后面所有功能代码都挂载在这里 })();

先解释几个关键字段:

  • @match:限制脚本在哪些网址上运行。我用的是通配符,匹配豆包主域名下的所有页面,包括子路径。
  • @grant GM_download:申请调用油猴提供的下载接口,这样可以绕过部分浏览器对自动下载的限制。
  • @grant GM_registerMenuCommand:在油猴菜单里注册一个自定义入口,方便执行批量下载动作。
  • @run-at document-idle:等待页面基本加载完成后再运行,避免脚本运行太早找不到节点。

CONFIG里的参数可以先保持默认,后面实操环节我会逐一告诉你每个参数在什么情况下改。

2.2 如何精准提取页面中的图片地址

这是整个脚本的地基。图片地址找得准,后面的下载和处理才有意义。豆包页面的图片元素通常有两种存在形式:

  • 直接包裹在<img>标签里,它的src属性就是图片地址。
  • 以CSS背景图的方式存在,藏在background-image样式中。

为了兼容这两种情况,我在脚本里写一个统一的提取方法。思路是:遍历页面上所有可视范围内的图片节点,先从src取地址,拿不到再从样式里解析url("...")。顺带过滤掉头像、图标等无关小图:

function collectImageUrls() { const urls = new Set(); // 方式一:遍历img标签 const imgElements = document.querySelectorAll('img'); imgElements.forEach(img => { const src = img.currentSrc || img.src || ''; if (!src) return; if (isCandidateUrl(src)) { urls.add(normalizeUrl(src)); } }); // 方式二:遍历带背景图的元素 const bgElements = document.querySelectorAll('[style*="background-image"]'); bgElements.forEach(el => { const bg = window.getComputedStyle(el).backgroundImage; const match = bg.match(/url\(["']?([^"')]+)["']?\)/); if (match && isCandidateUrl(match[1])) { urls.add(normalizeUrl(match[1])); } }); return Array.from(urls); } function isCandidateUrl(url) { // 过滤掉过于短小的地址,大概率是图标 if (url.length < 20) return false; // 过滤掉dataURI,这种一般是内嵌小图 if (url.startsWith('data:')) return false; if (url.includes('avatar') || url.includes('logo')) return false; let path = ''; try { path = new URL(url).pathname; } catch (e) { return false; } // 图片扩展名或者CDN路径里带image标识的都算候选 return /\.(png|jpe?g|webp|gif)(\?|$)/i.test(path) || /image/i.test(url); } function normalizeUrl(url) { // 去掉URL里常见的追踪参数,让文件名更干净 try { const u = new URL(url); u.search = ''; return u.toString(); } catch (e) { return url; } }

这里我特意用currentSrc而不是src,是因为不少前端框架做了响应式图片处理,currentSrc拿到的是浏览器实际加载的那个地址,更可靠。过滤条件里的avatar和logo是为了避免把用户头像和平台图标也当成生成图下载下来,这个在实际使用中非常关键,不然你批量下载完发现里面混了一堆没用的头像。

2.3 批量下载与自动去水印

图片地址拿到手,接下来就是下载。这一步我用了GM_download,它是油猴脚本下载指定URL内容的标准方式,支持url和name两个核心参数。为了不让浏览器拦截,我会把并发数控制在合理范围,同时添加一个简单的重试机制:

function queueDownload(imageUrls) { const limit = CONFIG.allowMultipleDownloads; let index = 0; function next() { if (index >= imageUrls.length) return; const current = imageUrls[index]; index++; const fileName = buildFileName(current); GM_download({ url: current, name: fileName, saveAs: false, onerror: (err) => { console.warn('下载失败,尝试重试: ', current, err); setTimeout(() => { GM_download({ url: current, name: buildRetryFileName(fileName), saveAs: false }); }, 1200); }, ontimeout: () => { console.warn('下载超时,跳过: ', current); } }); if (index < imageUrls.length) { setTimeout(next, 400); } } next(); }

buildFileName是什么?因为豆包默认的图片URL都是无意义的一串字符,下载下来之后文件名很难管理。我这里是根据时间戳加序号生成可读性强的文件名,比如doubao_20250721_001.png:

let sequenceCounter = 0; function buildFileName(url) { sequenceCounter++; const datePart = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const extMatch = url.match(/\.(png|jpe?g|webp|gif)/i); const ext = extMatch ? extMatch[1] : 'jpg'; return `doubao_${datePart}_${String(sequenceCounter).padStart(3, '0')}.${ext}`; } function buildRetryFileName(originalName) { return originalName.replace(/(\.[^.]+)$/, `_retry$1`); }

去水印这步,是很多人关心的重点。你可能想知道脚本怎么直接操作图片。这里要提醒一下:纯浏览器脚本没法对文件做像素级处理。所以更现实的方案是两步走——先下载,再用本地工具统一裁边。不过我们可以在脚本里做得更聪明一点:针对水印固定出现在右下角的情况,我们先把下载下来的图片路径收集起来,生成一个批处理清单,之后用本地小工具根据清单做裁剪。

脚本这部分代码会检测图片文件名,把需要裁剪的图片标记出来,并生成一个crop_list.txt清单文件,里面包含每张图片的路径和裁剪参数。这样用户只要在本地运行一个简单的批处理命令,就能自动完成角落裁剪。

function generateCropList(imageUrls) { let listContent = ''; imageUrls.forEach((url, idx) => { const fileName = `doubao_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_${String(idx + 1).padStart(3, '0')}`; // 假设水印在右下角,裁剪宽度和高度的比例为CONFIG.cropRatio const cropPixels = calculateCropPixels(url); listContent += `${fileName}.${getExt(url)} ${cropPixels.width} ${cropPixels.height}\n`; }); // 通过油猴的下载能力,把这个清单下载下来 const blob = new Blob([listContent], { type: 'text/plain' }); const objUrl = URL.createObjectURL(blob); GM_download({ url: objUrl, name: 'crop_list.txt', saveAs: false }); } function getExt(url) { const m = url.match(/\.(png|jpe?g|webp|gif)/i); return m ? m[1] : 'jpg'; } function calculateCropPixels(url) { // 这里做简化处理:参考常见生成图尺寸,默认按1920x1080计算 // 更精确的方式是在图片加载后读取naturalWidth和naturalHeight const referenceWidth = 1920; const referenceHeight = 1080; return { width: Math.round(referenceWidth * CONFIG.cropRatio), height: Math.round(referenceHeight * CONFIG.cropRatio) }; }

当然,如果不想用裁剪方案,还有个更干净的路径:把CONFIG.enableCrop设为false,这样脚本只负责批量下载原图,不生成清单,后面你自己用任意图片工具简单处理就行。两种流程看个人需要。

2.4 设置菜单和操作入口

为了让脚本不只有“后台默默跑”这一种形态,我还加了两个菜单项:一个用来执行批量下载原图,另一个用来生成裁剪清单。这样你可以按需触发,不用一进入页面就自动下载。

function registerMenu() { GM_registerMenuCommand('下载当前页所有生成图', () => { const urls = collectImageUrls(); if (urls.length === 0) { console.warn('未发现可下载的图片,确认是否在展示生成图的页面'); return; } queueDownload(urls); }); GM_registerMenuCommand('生成裁剪清单(自动裁水印)', () => { const urls = collectImageUrls(); if (urls.length === 0) { console.warn('未发现可下载的图片'); return; } generateCropList(urls); }); }

调用注册函数:

registerMenu();

这样,油猴的菜单里就会出现两个入口,鼠标点一下就能触发操作,比每次都打开控制台手动粘贴代码要优雅得多。

3. 实操步骤:从安装到跑通全流程

3.1 安装油猴扩展并启用脚本

先确保浏览器里装了油猴扩展。这一步比较简单,直接在扩展商店里搜索,安装完成后,点击工具栏里的油猴图标,选择“添加新脚本”,它会打开一个内置编辑器。

把上面骨架和功能代码合并成一个完整文件,粘贴进去,按Ctrl+S保存,脚本就会自动生效。保存的时候最好给脚本起一个容易认的名字,比如“豆包批量图片助手”,方便后续在已安装列表里找到。

注意:脚本生效的前提是,页面域名和@match规则匹配。如果你访问的是豆包的某个特殊子域名或测试环境,记得在脚本的@match里补上对应的域名规则,否则脚本不会运行。

3.2 首次运行:注意浏览器的多文件下载授权

这是我第一次跑的时候遇到的坑。脚本触发下载时,浏览器会弹一个提示:“此网站正尝试下载多个文件”之类的。很多浏览器默认是阻止的,如果你不点允许,后面所有图片都会卡住。

正确的操作是:在第一次运行脚本时,在弹出的下载确认框里勾选“允许自动下载多个文件”,或者直接在浏览器设置里把这个站点的多文件下载权限设为允许。不同浏览器位置不同,但逻辑都一样。

3.3 验证下载效果:图片到底带不带水印

第一次跑完,你可能会发现两种情况:

  • 情况一:下载下来的图片本身是干净的。这种情况通常是因为你之前在页面里打开过大图预览,图片地址指向的是原始文件。
  • 情况二:下载下来的图带水印。别急着失望,这时候裁剪清单就派上用场了。

如果你用的是裁剪方案,脚本会下载一个crop_list.txt。打开这个文件,格式类似这样:

doubao_20250721_001.jpg 38 22 doubao_20250721_002.jpg 38 22 doubao_20250721_003.jpg 38 22

第一个字段是文件名,第二个是裁剪宽度像素,第三个是裁剪高度像素。随后用一个本地的批量裁剪小工具,读取这个清单,就能把右下角对应的区域裁干净。我实测下来,裁掉大约2%比例的水印区域后,图片主体几乎不受影响,视觉上非常自然。

3.4 没有代码基础怎么改参数

对不懂代码的人来说,改脚本里的CONFIG参数就够了,其他基本不用动:

  • enableCrop: true—— 想关闭裁剪功能就改为false。
  • cropRatio: 0.02—— 水印比较大、位置更靠内的时候,可以调到0.03或0.05。
  • allowMultipleDownloads: 10—— 想并行下载更多图片,可以将数字调大,但别超过15,否则可能触发浏览器拦截或网络拥塞。

4. 常见问题与排查技巧实录

4.1 脚本没生效怎么办

排除顺序从简单到复杂:

  • 确认油猴图标在页面加载时有没有变成“已启用”状态。
  • 确认@match是否匹配当前域名,必要时直接改成@match *://*doubao*这种宽泛规则测试。
  • 打开浏览器开发者工具,切换到控制台,看看有无红色报错。最常见的是脚本引用了GM_download但没声明权限。
  • 页面是动态加载内容的,首次进入列表时可能瞬间没有加载完图片,脚本跑太早就会漏图。可以把@run-at改为document-end,或者等页面滚动加载完后再点菜单触发。

4.2 下载中途停了或漏图

我遇到过几次类似情况,原因基本有三个:

  • 页面还在持续滚动加载新图片,脚本只抓了当前视口内的图。解决办法是先滚动页面,把所有内容加载出来后,再点击下载菜单。
  • 几张图因为网络问题下载失败。脚本里我加了重试,但是重试也失败的话,它会继续跑后面的任务。此时可以重新执行一次,新执行不会重复下载已有的图片(因为脚本会生成新的文件名,实际上会重复下载),更好的办法是手动挑出漏掉的地址,单张补下。
  • 浏览器由于安全策略丢掉了部分下载任务。这个比较棘手,建议把并发数量调低一些,适度降低失败概率。

4.3 裁剪清单生成正常,但本地工具处理报错

清单文件如果从浏览器下载下来,可能会被加了一行隐藏的CRLF换行,某些工具解析时可能报错。保险起见,可以在读清单时统一按行分割,并去除空白。另外,如果你的图片实际尺寸并不是1920x1080,而是2048x1536这种比例,裁剪像素数就不太精确。遇到这种情况,可以先把清单里的裁剪像素按比例换算一下,或者在本地工具里把裁剪参数改成百分比模式。

简单的计算方式:

原图宽度 / 清单里的参考宽度 = 比例修正 实际裁剪宽度 = 清单裁剪宽度 * 比例修正

比如原图是2048宽,参考宽度是1920,那么实际裁剪宽度就是 38 * (2048 / 1920) ≈ 40像素。改一下工具里的参数就行。

4.4 关于版权与合规的提醒

这个方案做的是“管理你的个人素材库”,不是让你去爬取别人的作品。批量下载时建议只针对自己账号内生成的图片操作,不要随意抓取平台其他用户的内容,也不要把去水印后的图用作商业用途,除非你确认自己的使用场景符合平台条款和相关法律。合规这块,自己心里有数就好。

5. 一些想补充的细节与最终建议

脚本可以再做一些个性化调整,比如加上右键菜单支持、把下载的文件按时间分目录整理。这个就留给你自己探索了。我个人的习惯是:每天早上打开豆包,把前一天生成的图片一次性跑脚本下载,再用裁剪清单统一裁边,整个过程不到一分钟,比手动省了太多精力。

一个小技巧是,如果你经常用同一台电脑处理图片,可以在本地准备一个固定的工作目录,把下载目录和裁剪后的输出目录分开。这样原始文件保留一份,处理后的一份,万一裁错了还能从原图恢复。

最后想说,这套插件的核心价值不是“黑科技”,而是把重复劳动压缩到几乎为零。批量提取、自动命名、统一裁剪,每一步单看都不复杂,但组合在一起,确实能让日常出图效率提升不少。希望这篇里的思路和代码能给你们省点时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:53:11

MatchNet复现指南:小样本图像块匹配的工业级实践

1. 项目概述&#xff1a;为什么MatchNet值得花时间复现&#xff1f;MatchNet不是那种刷几篇顶会就消失的“网红模型”&#xff0c;它在2016年提出时&#xff0c;就踩中了计算机视觉里一个特别实在的痛点——局部图像块&#xff08;patch&#xff09;之间的匹配精度与泛化能力难…

作者头像 李华
网站建设 2026/10/13 7:52:51

打破幽灵数据竞争:深入 UnsafeCell 与 Sync 语义的本质边界

在 Rust 的类型宇宙中&#xff0c;有一道被无数初学者敬畏的不可逾越之墙&#xff1a;共享引用&#xff08;&T&#xff09;绝对不可变&#xff0c;可变引用&#xff08;&mut T&#xff09;绝对独占。编译器基于这条铁律&#xff0c;在编译期施展激进的别名分析&#xf…

作者头像 李华
网站建设 2026/10/13 7:52:48

Claude Code plan模式详解:先方案后代码的AI编程实战

1. 计划模式到底解决了一个什么问题先聊聊我为什么会专门为 plan 模式写一篇教程。用 Claude Code 的人大致会经历三个阶段&#xff1a;一开始觉得它能自动改代码很神奇&#xff0c;什么任务都直接丢给它跑&#xff1b;接着在某次比较大的改动中被它“带偏”&#xff0c;改着改…

作者头像 李华
网站建设 2026/10/13 7:52:34

软件项目验收报告模板:量化标准与签字流程的关键要点

简介&#xff1a;软件项目验收报告模板.doc是一款面向IT项目经理、开发团队、验收评审人员及项目管理办公室的规范化文档工具&#xff0c;用于在项目完成后系统梳理建设全过程、确认交付成果是否满足合同约定与行业标准。模板按标准验收流程编排&#xff0c;涵盖文档修订历史记…

作者头像 李华
网站建设 2026/10/13 7:52:14

MySQL表结构查询全攻略:从DESC到information_schema的进阶实践

1. 从一条 SQL 聊起&#xff1a;为什么“看表结构”这件事值得写一篇长文 先问个问题&#xff1a;你在 MySQL 里想知道一张表长什么样&#xff0c;第一反应是敲什么命令&#xff1f;我猜八成是 DESC 或者 SHOW CREATE TABLE 。这两个确实最常用&#xff0c;但如果你只停留在…

作者头像 李华