“ponytail”这个项目名,听起来像一个很简单的小玩意,实际上也确实很小。它是我在连续第三个月被“帮我把这个页面里的图片整理成表格”这种需求轰炸之后,写的一个浏览器书签小工具:只要点一下收藏栏里的按钮,就能把当前网页里的图片地址、图片尺寸、ALT 信息全部提取出来,去重、排序、然后一次性复制成 Markdown 或者 JSON。如果你也经常需要从网页里批量收集图片素材、整理竞品页面里的资源清单、或者做设计灵感归档,那这个工具的很多设计思路和取舍,应该能帮你省下不少时间。
1. 一点背景:为什么是 ponytail 而不是一个复杂工具
1.1 最先被戳中的痛点
先说痛点。做前端或者内容运营的同学应该都有过这种经历:看到一个参考页面,想把它上面的 banner 图、产品图、ICON 全部保存下来。常规操作是什么?打开浏览器开发者工具,切到 Network 面板,按图片类型过滤,然后在几十条请求里把 URL 一个一个复制出来。碰到懒加载页面还要把滚动条滚到底,截图里的图标和背景图混在一起,根本分不清哪张是哪张。运气好十分钟搞定,运气不好一下午就没了。更尴尬的是,这种需求没有什么技术难度,就是纯体力活,但真的会让你怀疑人生。
在我印象里最夸张的一次,是一个活动页里有四十多张图片,包含轮播图、背景纹理、角落装饰和社交图标。运营同事给我的需求是“把这些图列一个带名字、带尺寸的清单,我拿去跟供应商对素材”。我当时用开发者工具手点了将近一个小时,复制到 Excel 之后发现还有七八张是重复的,只是路径大小写不一样。那一刻我就在想,这种活必须让脚本去干,而且要干得比人靠谱。
1.2 “马尾辫”这个名字的由来
为什么叫 ponytail?其实没什么高深的含义。前端页面里的图片资源散落在很多标签里,有的是<img>,有的是<picture><source>,有的是元素的 background-image,还有的是懒加载插件塞进>(function () { 'use strict'; const results = new Map(); function addImg(src, width, height, alt) { if (!src || src.startsWith('data:') || src.startsWith('blob:')) return; const abs = new URL(src, location.href).href; if (!results.has(abs)) { results.set(abs, { src: abs, width: width || 0, height: height || 0, alt: alt || '' }); } } // 1. 处理普通 img,兼容懒加载属性 document.querySelectorAll('img').forEach(img => { const src = img.currentSrc || img.src || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.getAttribute('data-original'); addImg(src, img.naturalWidth, img.naturalHeight, img.alt); }); // 2. 处理 picture 下的 source document.querySelectorAll('picture source').forEach(source => { if (!source.srcset) return; const first = source.srcset.split(',')[0].trim().split(' ')[0]; addImg(first); }); // 3. 处理 background-image document.querySelectorAll('*').forEach(el => { const bg = getComputedStyle(el).backgroundImage; if (!bg || bg === 'none') return; const match = bg.match(/url\(["']?([^"')]+)["']?\)/); if (match) addImg(match[1]); }); // 4. 按像素面积倒序排序 const list = [...results.values()].sort((a, b) => { return ((b.width || 0) * (b.height || 0)) - ((a.width || 0) * (a.height || 0)); }); // 5. 输出到新窗口 const win = window.open('', '_blank', 'width=900,height=600'); if (!win) { alert('浏览器拦截了弹窗,请允许当前站点弹出窗口后重试。'); return; } const heads = list.map(item => `<tr><td><img src="${item.src}" width="80"></td> <td>${item.width}×${item.height}</td> <td><a href="${item.src}" target="_blank">${item.src}</a></td></tr>` ).join(''); win.document.write(`<html><head><title>ponytail</title> <meta charset="utf-8"></head><body> <table border="1">${heads}</table></body></html>`); win.document.close(); })();
上面代码里最值得说的是两个细节:第一个是addImg函数里对data:和blob:开头的地址直接过滤。这类图片要么是内联的 base64,要么是临时对象 URL,它们既不方便复制也不方便转存,收集出来没意义。第二个是背景图查询用的getComputedStyle,它能拿到元素最终计算出来的背景图,不会漏掉写在样式表里的图,但代价是要遍历所有元素,页面很大的时候会稍稍有点卡。这些都是用了几次之后踩到坑才慢慢调整出来的。
4. 在真实项目里怎么用它跑通需求
4.1 场景:运营要竞品页的图列表
先说一个我实际经历过的场景。运营同事拿到一个活动页链接,要求“把里面的素材图整理成一个表格,列清楚每张图是什么、多大、对应页面哪个位置”。以前这个是设计师或者前端来做,一张一张截图标注,非常拖沓。我用 ponytail 帮助她在几秒内打开结果窗口,然后用“只看宽高不小于 200px 的图”来过滤掉图标和装饰点,再把结果复制成 Markdown,粘贴进文档里。
她唯一需要多做的,是图一多的时候,还要根据文件名和尺寸去判断“哪张图对应页面里的哪个位置”。我会把 ALT 和文件名一起导出,ALT 往往能从代码层面反应该图在页面里的作用,比如 “banner-main” 就是主视觉,“icon-arrow” 就是箭头图标。这样一来,需求交接就变得非常顺利,她拿到清单后稍微改一下备注就能直接发出去。
这里有一个非常重要的经验:不要一开始就把所有图片不分青红皂白地全导出来。你先用筛选条件把大图拎出来,再看一眼缩略图和文件名,基本就能抓住页面主要视觉。小图标、背景纹理、重复装饰图属于噪音,留在结果里只会干扰阅读。在结果窗口里加“隐藏小于 200px 的图”这个默认开关,其实就是基于这个实战经验做的。
4.2 场景:前端要快速挑选素材
另一个场景是前端开发。之前做官网改版,我需要把一个旧页面里的 30 多张图片资源全部迁移到新的图片服务上。常规做法是看代码,一个个找 URL,再逐个下载上传,改完路径还要测试新环境下的加载效果。很繁琐对吧?有了 ponytail 之后,我先在旧页面点一下按钮,导出 JSON,然后写一条简单的 node 脚本把这些 URL 批量下载到本地,再调用图床接口传上去。整个过程只花几分钟。
这里面有个隐藏技巧:导出的 JSON 里保留了图片的原始像素尺寸。传到新 CDN 之后,如果 CDN 支持裁剪参数,你还可以按尺寸自动生成适配不同屏幕的宽高。比如一张原始 1920×1080 的 banner 图,用 JSON 里的尺寸信息就能精准确定是否要做移动端裁剪。这比人工打开每张图片看尺寸要准确得多。
如果你不想写 node 脚本,直接在结果窗口里点“下载全部”,让浏览器以附件方式逐个下载,也能应急。要注意的是,批量下载多个文件会触发浏览器的“允许下载多个文件”提示,我建议在弹窗里保持允许状态,一次性下载完,然后再恢复默认设置,避免误触安全提醒。
4.3 场景:设计师做灵感整理
设计师用 ponytail 的场景相对轻松一些。比如在 Dribbble、Behance、Pinterest 或者各种灵感站点上看到一组推荐,想把这些图收进自己的素材库,如果一张张右键另存为,要重复二十多次;截图又会被平台水印干扰。点一下书签,把所有大图都提取出来,然后按尺寸倒序排列,就能看到这个页面的视觉主体都是哪些,再复制到自己的采集文档里,统一加水印或者改名。
不过这里必须提个醒:灵感站点的图片很多有版权,只应该用于个人学习与收藏,不该直接拿来商用。ponytail 只是你的素材收集工具,它不能替你做版权判断。我也会在 README 里特意加了一句,强调不要拿这个工具去批量抓取别人的商业素材库,遵守目标站点的条款是使用者自己的责任。这是工具和人的界限,我个人觉得既然做工具,就得把边界写清楚。
4.4 结果不好看怎么办
有时候你会发现导出的 Markdown 里,文件名的可读性很差,都是一长串 CDN 上的随机字符。这种情况下建议先别急着拷贝,到结果窗口左上角的过滤框输入关键词,比如输入 “banner” 可以只留下文件名里带 banner 的图。如果你的需求是要整理“哪些图属于模块一”,按命名关键词筛选是最快的路径。由于 CDN 上的链接通常保留了原始上传时的文件名,虽然没有中文,但大多数时候含语义,grep 一下总比人肉看强。
如果页面里的图片大量是 WebP 或 AVIF 格式,而你的文档系统不支持预览,之前复制 Markdown 的方式就不太理想。这时候可以先导出 JSON,然后用一个简单的批量格式转换脚本把地址按实际需要替换成.jpg等格式。前提是源服务器支持格式回退,或者你的 CDN 有转换能力。这类变动已经超出 ponytail 的能力范围,但因为它导出的是结构化数据,后续加工就变得很容易。
5. 踩坑记录:ponytail 在真实浏览器里的那些坎
5.1 常见问题和速查表
用了这几年,我在不同浏览器、不同站点的环境中踩过不少坑。我把它们整理成一个速查表,方便你现查现用:
| 问题现象 | 常见原因 | 处理办法 |
|---|---|---|
| 点击书签没反应 | 书签代码被浏览器拦掉了,或者粘贴时断行 | 重新复制javascript:代码,确认无换行 |
| 结果窗口被弹窗拦截 | 浏览器安全策略默认拦截非用户点击弹窗 | 点击书签时不要离开页面,允许一次弹窗 |
| 图片显示为 hang | 图片所在域名存在防盗链 | 给结果窗口里的 img 增加referrerpolicy="no-referrer" |
| 抓不到懒加载图片 | 图片没有加载进 DOM,脚本只能读已加载的元素 | 先把页面滚动到底,再点书签 |
| 抓不到 iframe 里的图 | 脚本只在顶层 document 执行 | 在控制台切换到对应 iframe 上下文后重新运行 |
| 图标、分割线混进来 | 小尺寸图片没有被过滤 | 打开“隐藏小于 200px 的图” |
| 同一个链接出现多次 | 有不同 query 参数或大小写差异 | 先按 URL 去重,再按闭环比对 |
| 复制按钮报错 | 低版本浏览器不支持 Async Clipboard API | 回退到document.execCommand('copy') |
这张表是我最常发给同事看的文档。绝大多数问题都不是 bug,而是浏览器安全策略或者使用姿势不对。所以遇到异常先不要慌,按表里的顺序从头检查一遍,八成能解决。
5.2 从“识别不到图”到“把防盗链图显示出来”
最让人头疼的问题是“图片列表出来了,但缩略图全显示不了”。一开始我以为是 URL 抓错了,后来发现是防盗链策略:很多 CDN 会检查 HTTP Referer 头,只允许来源域名匹配的请求,而新窗口里的缩略图请求的 Referer 是空白的,所以图片服务器直接拒绝响应。解决办法不复杂,在缩略图的<img>标签上加上referrerpolicy="no-referrer",让浏览器在请求这张图时不携带 Referer。这样做能绕开一部分防盗链,但不能绕开需要登录鉴权的图片,那种图你抓到的只是登录页地址,没什么用。
如果是正常可以访问但就是不显示,还有一个隐藏原因:getComputedStyle(el).backgroundImage返回的 URL 可能是个相对路径,类似url("/assets/bg.png"),如果直接用这个字符串发起请求,会请求到当前脚本所在的新窗口的地址下,而不是原页面地址下。这就是为什么在收集阶段一定要用new URL(src, location.href)做一次标准化。这一步不做,后面所有相对路径的图都会失效。
5.3 关于安全边界的一点心里话
做这个工具的过程中,我也一直在思考它的安全边界。ponytail 本质上是一个在用户当前页面上下文中运行的脚本,它拥有当前页面的 DOM 访问权限,和你在控制台里手动执行代码的权限是一样的,不会比那更高。它不读取 Cookie,不发起跨域请求,不做 localStorage,除了弹出结果窗口外,改写不了任何外部资源。所以从权限角度讲,它没有被放大。
但反过来讲,如果你在一个不信任的页面上点击书签,脚本会暴露那个页面内所有图片的 URL,这也意味着这个页面本身就可能被其他恶意脚本入侵。因此我建议不要在完全不熟悉的敏感环境里随意使用任何书签工具,尤其是涉及登录态管理的页面。书签脚本是一种强大的自助查看工具,但不是加密沙箱。安全意识必须跟上,工具只是一个帮助你把头发扎起来的发圈,不会主动保护你不受周围环境影响。
6. 一些个人经验,以及往后的扩展想法
分享几个我用下来觉得最值得记住的经验。第一个经验:如果采集的结果量很大,先复制 JSON,再用脚本按文件名归类,不要急着在浏览器窗口里试图手动整理。遇到几百张图片的场景,人的耐心是撑不过十分钟的,脚本反而能精确完成。第二个经验:把书签代码存在自己的私人仓库或者笔记里,换电脑时第一时间恢复,不然临时找代码真的很痛苦。第三个经验:如果团队里有多人需要这个工具,你可以把 ponytail 的 HTML 安装页部署到一个内网静态站点上,同事打开后点一下“安装”,就能自动创建书签,省掉复制粘贴长代码的麻烦。
后续我其实还想给它加两个能力。一个是把结果按“页面内可见区域”分组,比如首屏图、滚动屏图、底部模块图,这样运营整理页面结构时能更直观。另一个是通过 Web Share API 把结果直接发送到手机端或者分享到团队聊天工具里,目前实现还没成熟,因为在浏览器新窗口里调 Web Share API 的兼容性还有坑。但想法很有意思,分享素材清单这件事,比复制粘贴要自然很多。
如果你也希望自己页面里的图片资源不再是一堆难看的乱发,不妨找个周末写一个十几行的书签,或者直接用我这套思路改一版适合自己的。工具本身不是重点,重点是你在整理信息时能不能摸到自己的节奏。我个人在实际项目里越来越依赖这种“轻量脚本 + 结构化输出”的组合,因为它把重复劳动变成了两次点击,而这正是我们做技术的人最值得花时间去优化的地方。