优先用语义属性 :aria-label、role、>const OUTBOUND_LINK = /^https?:\/\//i; function isResultCard(anchor) { if (!anchor.href || !OUTBOUND_LINK.test(anchor.href)) return false; if (anchor.closest('header, nav, footer')) return false; const img = anchor.querySelector('img'); if (!img) return false; const w = img.naturalWidth || img.width || 0; return w >= 60; }这里有三层过滤:链接必须指向外部、不能在页头页脚导航区、内部必须有一张尺寸过得去的图。naturalWidth的判断是为了排除图标和小挂件,避免把界面上的装饰元素误认成结果卡片。这套启发式的好处是它对结构改版有一定钝感——只要页面还是“一堆指向外站的图片链接”这个形态,它就能工作。
3.3 从卡片数据推导原图地址 判断出卡片之后,接下来是取地址。这一段是整个脚本里最需要小心的地方,因为要处理各种脏数据:
function resolveOriginal(card) { const imageLink = card.querySelector('a[href]'); // 第一层:链接参数里带图片字段 try { const u = new URL(imageLink.href); for (const key of ['imgurl', 'image_url', 'mediaurl', 'url']) { const v = u.searchParams.get(key); if (v && /^https?:\/\//i.test(decodeURIComponent(v))) { return decodeURIComponent(v); } } } catch (e) { /* 地址异常,跳过 */ } // 第二层:缩略图上的 data 属性 const img = card.querySelector('img'); if (img) { for (const attr of ['data-src', 'data-iurl', 'data-original']) { const v = img.getAttribute(attr); if (v && /^https?:\/\//i.test(v)) return v; } // 第三层:去掉缩略图尺寸参数赌一次 const raw = img.currentSrc || img.src; if (raw) { const cleaned = raw.replace(/([?&])(w|h|width|height|q|quality)=\d+/g, '') .replace(/[?&]$/, ''); if (cleaned !== raw) return cleaned; } } return null; }这段代码有几个刻意的设计。try/catch包住 URL 解析是因为页面上偶尔会出现畸形链接,一个异常就能让整个脚本停摆。参数名的候选列表覆盖了多个版本的命名习惯,不是瞎猜,是几年下来积累的常见写法。第三层的参数剥离只处理明确的尺寸和质量参数,不做激进的路径替换,因为激进替换失败率太高,给用户一个能点但打不开的按钮,体验比没有按钮更差。
提示:第三层回退在实际使用中成功率大约只有一半,取决于图床是否支持无参数访问。如果你发现点击后图片很小或者报错,多半就是命中了这一层,这时候更应该关注第一层为什么没取到值。
3.4 动态加载页面:MutationObserver 的正确用法 搜索结果页是无限滚动的,首屏加载完之后往下滚会不断追加新卡片。如果只在脚本启动时扫一遍,用户滚两屏之后按钮就全没了。这时候需要MutationObserver,但直接用是最容易写出性能问题的:
let pending = false; const observer = new MutationObserver((records) => { if (pending) return; let hasNewCard = false; for (const r of records) { for (const node of r.addedNodes) { if (node.nodeType === 1) { hasNewCard = true; break; } } if (hasNewCard) break; } if (!hasNewCard) return; pending = true; requestAnimationFrame(() => { scan(document); pending = false; }); }); observer.observe(document.body, { childList: true, subtree: true });三个要点:只处理新增节点 ,不因为属性变化就重扫;用requestAnimationFrame合并短时间内的大量变更 ,一帧内最多跑一次扫描;扫描前先判断有没有新元素 ,纯文本改动直接忽略。我最初版本没做这些,页面滚到第三屏就开始卡,风扇呼呼转,问题就出在同一个卡片被反复扫描了几十遍。
扫描函数本身也要做幂等——加过按钮的卡片要打标记,第二次进来直接跳过:
function scan(root) { root.querySelectorAll('a[href]').forEach((a) => { if (!isResultCard(a)) return; const card = a.parentElement; if (!card || card.dataset.isrDone === '1') return; card.dataset.isrDone = '1'; const original = resolveOriginal(a); if (!original) return; card.appendChild(buildButton(original)); card.style.position = card.style.position || 'relative'; }); }dataset.isrDone这个标记是必须的,它让整个扫描过程变成幂等操作,重复调用不会重复加按钮。position: relative是为了让按钮的绝对定位有个参照,这里用||=的写法避免覆盖页面原有的定位设置。
3.5 按钮的样式与交互细节 按钮本身是个链接,不用div加onclick,因为原生链接天然支持中键新标签页打开、右键复制地址、拖拽,这些行为用户是期待的:
function buildButton(url) { const a = document.createElement('a'); a.className = 'isr-view-btn'; a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = '查看图片'; a.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); window.open(url, '_blank', 'noopener'); }); return a; }stopPropagation是必须的,结果卡片本身通常绑了点击跳转,不阻止冒泡的话,点按钮会导致卡片和按钮同时响应,弹出两个标签页,用户会以为中招了。rel="noopener noreferrer"是安全习惯,顺带把来源信息抹掉,有些站点的图片做了防盗链,不带来源反而更容易打开。
样式上我只写了几行,重点是别和页面原有视觉打架:
.isr-view-btn { position: absolute; top: 6px; left: 6px; z-index: 20; padding: 2px 8px; font-size: 12px; line-height: 18px; color: #fff; background: rgba(0, 0, 0, .62); border-radius: 4px; text-decoration: none; opacity: 0; transition: opacity .15s ease; } a:hover > .isr-view-btn, .isr-view-btn:hover, .isr-view-btn:focus { opacity: 1; }默认透明、悬停显现,这是我在多个页面增强工具里反复验证过的做法。按钮常驻会遮挡图片,尤其实在小尺寸缩略图上;完全隐藏又让人发现不了。悬停显现既保持了界面干净,又符合“我需要的时候它在”的直觉。
4. 三分钟装好:从拿到源码到按钮出现 4.1 拿到源码后的第一件事 拿到这个扩展的源码目录后,先别急着装,花二十秒看一眼目录结构。正常情况下只需要三个文件:manifest.json、content.js、style.css。如果目录里多出background.js、popup.html之类的东西,说明这个版本可能带了额外功能,装之前先扫一眼代码,确认没有你不想要的网络请求。这个习惯很重要,任何扩展在装之前都值得看一眼它到底要干什么。
打开manifest.json,把matches字段改成你实际要用的站点域名。改完保存,这一步大概三十秒。
4.2 加载解压扩展的完整步骤 浏览器自带的扩展商店里通常搜不到这类小众工具,得走开发者模式加载本地目录。流程是这样的:
地址栏输入扩展管理页地址,回车。不同内核的浏览器地址略有差异,常见的是chrome://extensions和edge://extensions。 找到页面右上角的“开发者模式”开关,打开。打开之后页面上会多出三个按钮。 点“加载已解压的扩展程序”,在弹出的目录选择框里选中扩展所在的文件夹,注意选文件夹本身,不是里面的某个文件。 加载成功后列表里会出现一条新卡片,卡片上如果有红色报错文字,说明配置有问题,先解决再往下走。 回到搜索结果页,强制刷新一次 (多数浏览器是Ctrl+F5或Cmd+Shift+R),因为内容脚本只在页面加载时注入,普通刷新可能会用到缓存。 整个过程顺利的话两分钟以内。剩下的时间用来验证。
4.3 验证:三个检查点 装完之后别只看结果,按顺序验证三件事,出问题能快速定位在哪一层:
脚本有没有注入 :在结果页打开控制台,输入document.querySelectorAll('[data-isr-done]').length,返回大于 0 说明扫描跑过了。地址有没有取到 :在控制台里手动调用一次resolveOriginal逻辑,看看返回的是不是以http开头的完整地址。按钮有没有渲染 :在页面元素面板里搜索isr-view-btn,能搜到就说明 DOM 操作成功,剩下的只是样式问题。这三个检查点对应三层失败:注入失败、数据失败、渲染失败。我遇到过太多次“按钮不出现”的情况,用这套顺序排查,通常两分钟内就能定位。
4.4 几个可以调的关键参数 脚本里有几个值是可以按需调的,我整理成表:
参数 默认值 作用 调整建议 图片最小宽度 60px 过滤小图标 界面密集的站点可提到 100px 扫描节流帧数 1 帧 控制重扫频率 页面卡顿时改为 3 到 5 帧 按钮显隐方式 悬停显现 视觉干扰控制 触屏设备改为常驻 地址回退层数 3 层 推导原图的尝试次数 追求稳定可减到 2 层
这些值没有绝对正确,取决于你用的站点和你的使用习惯。我个人的配置是把最小宽度提到 100px,因为搜索页里的小图标太多了,容易被误判。
5. 常见问题与排查实录 5.1 按钮完全不出现 这是最高频的问题,原因基本集中在三个地方。第一种是matches没匹配上,脚本压根没注入,控制台里看不到任何日志,这时候去看扩展管理页里那条扩展的详情,里面有个“检查视图”之类的入口,能看到注入状态。第二种是脚本注入但报错了,通常是选择器或 API 在不支持的环境里调用失败,打开控制台直接看红色报错,错误信息一般能直接指出行号。第三种最隐蔽:脚本跑完了,但isResultCard的判定全部返回 false,原因是页面结构变动导致过滤条件太严,比如链接不再是绝对地址、图片尺寸阈值不合适。这种要靠打断点,在过滤函数里逐个看返回值。
5.2 点击后 404 或者图片特别小 这类问题的本质是地址推导错了,但错了哪一层是有区别的。如果打开的是个网页而不是图片,说明命中了链接本身,第一层的参数解析失败了,去控制台打印一下链接的查询参数,看看字段名是不是变了。如果打开的是图片但尺寸很小,说明命中了第三层的参数剥离回退,图床并不支持无参数访问。如果直接 404,那多半是第二层取到的data属性是个已经失效的地址。
我的处理方式是在按钮上加一个数据标记,记录这个地址来自第几层,排查的时候一眼就能看出来。这个技巧在别的页面抓取项目里同样好用——给每一步的回退加上来源标记 ,出问题时省掉大量猜测。
5.3 页面滚动后明显卡顿 性能问题几乎都出在观察器上。第一类是没有做节流,DOM 一变就全量重扫,滚动时每秒几十次重扫,卡是必然的。第二类是没有幂等标记,同一批卡片被反复处理,按钮加了又加,虽然看不见但 DOM 越来越大。第三类是扫描范围太宽,从document开始递归查询所有链接,页面上几千个元素全走一遍。
修复顺序建议是先加幂等标记,再加节流,最后考虑缩小扫描范围。实测下来前两步就能解决九成卡顿,第三步往往不必要。
5.4 问题速查表 把上面这些整理成一张表,出问题时按行对照:
现象 最可能原因 快速验证 处理 无按钮无报错 matches 未匹配 控制台执行查询返回 0 修正域名规则并重新加载 控制台有报错 脚本语法或 API 问题 看报错行号 按报错修复 部分卡片有按钮 过滤条件过严 打印判定结果 放宽尺寸或链接判断 点击弹出两个页签 事件冒泡未阻断 观察点击行为 加 stopPropagation 按钮被图片盖住 层叠上下文问题 元素面板看 z-index 提高 z-index 或换定位父级 滚动后按钮消失 未监听动态节点 手动触发一次扫描 加 MutationObserver 页面明显变卡 重扫无节流 性能面板看调用频次 合并扫描请求 原图打不开 防盗链限制 换个浏览器新窗口试 加 referrerpolicy 属性
5.5 几条踩出来的经验 第一条:永远先怀疑选择器,再怀疑逻辑 。页面改版十次里有八次是选择器失效,逻辑本身没问题。所以我在脚本里把选择器集中放在文件顶部,改起来一行就能改完,不用满文件找。
第二条:不要在扩展里做网络请求 。我见过太多同类工具死在“先请求一下验证图片存在”这一步,跨域、超时、证书、限流,任何一个环节都能让按钮变灰。让浏览器去做它该做的事,扩展只管把地址交出去。
第三条:留一个手动刷新入口 。我在脚本里挂了个快捷键,按一下强制全量重扫一遍,遇到页面结构局部变化、观察器没触发的情况,按一下就好了。这个小功能我自己用得比任何自动化逻辑都频繁。
6. 稳定性维护与能力边界 6.1 选择器脆弱性是无法根治的 必须承认一个现实:任何依赖页面 DOM 的增强工具,本质上都是在别人的地基上盖房子,地基一改,房子就得重搭。这个扩展能做的只是把重搭的成本压到最低——选择器集中管理、回退分层清晰、失败时静默降级而不是报错崩溃。我在代码里刻意让所有异常都被吞掉,原因就在这:宁可某个卡片没按钮,也不能让一个异常把整页的脚本停掉。
从这个角度看,衡量这类工具好不好的标准不是“功能多强”,而是“坏了之后多容易修”。一个两百行、结构清晰、选择器集中的脚本,改起来十分钟;一个两千行、逻辑层层嵌套的工具,页面一改版基本等于重写。
6.2 哪些事情这个方案做不到 有几个边界得说清楚,免得期待过高。第一,它拿不到真正的原始分辨率 。如果图床只对外暴露某个尺寸的版本,脚本再聪明也变不出更大的图,它只能拿到页面数据里存在的那个地址。第二,它不处理防盗链 。有些站点的图片校验来源,直连会失败,这不是扩展能解决的层面。第三,它不绕过任何页面限制 。脚本只读页面上已经渲染出来的数据,不做额外抓取,所以那些需要登录、需要展开、需要点击才加载出来的内容,它看不到。
我个人的用法是把这个扩展和一套独立的批量导出脚本配合:搜索页里用扩展快速判断“这张图值不值得存”,值得的再走导出流程批量处理。两者分工明确,各干各的活。
6.3 想自己改的话,从哪下手 如果你想把这套逻辑迁到别的站点,改动点其实只有三处:matches里的域名、isResultCard里的卡片判定、resolveOriginal里的地址推导。前两处是配置,第三处是逻辑。我的建议是先把判定放宽,让所有可疑元素都打印出来,确认哪些是真的卡片,再逐步收紧条件——这个“先松后紧”的调试顺序,比一上来就写精确选择器要快得多。
至于功能扩展,比较有价值的方向有两个:一是把解析到的原图地址汇总成一个可导出的列表,解决批量归档的问题;二是在按钮旁边加一个尺寸提示,鼠标悬停时显示大概的分辨率,帮助在点开之前就判断值不值得。这两个我都试过,代码量都不大,加起来不到一百行。
最后分享一个我自己一直在用的小习惯:给脚本加一个版本号常量,每次改完手动加一,然后在按钮的 title 属性里带上它。这样在页面上悬停一下就知道当前跑的是哪一版,多设备同步的时候特别有用,省得改了半天发现改的不是正在生效的那个文件。
需要专业的网站建设服务?
联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标
立即咨询