尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

油猴脚本实战:原生JavaScript屏蔽百度广告与隐藏知乎标题栏

油猴脚本实战:原生JavaScript屏蔽百度广告与隐藏知乎标题栏 你有没有过这种时候在百度上搜个东西前几条全是顶着“广告”小牌子的推广链接真正想找的答案被挤到八百里外好不容易打开知乎想专心看几个回答页面顶部的标题栏和一堆导航元素却偏偏占着屏幕不动翻半天内容都像是在跟网页玩捉迷藏。我以前都是忍忍算了直到我用上了油猴脚本才发现这些烦人的元素其实可以彻底收拾干净。这篇文章不绕弯子就是把我自己写的两个实用油猴脚本完整讲一遍一个用来屏蔽百度搜索结果页的广告一个用来隐藏知乎的标题栏。开发过程不依赖任何框架和构建工具纯原生 JavaScript 就能跑既适合对网页 DOM 操作感兴趣的人学习也适合只想拿现成代码改改参数、自己用的朋友直接抄作业。1. 项目概述与需求拆解1.1 油猴脚本是什么为什么选它油猴脚本更准确的叫法是用户脚本是一种运行在特定网页上下文的 JavaScript 代码。它本身不是一个独立的浏览器扩展而是要先安装一个“脚本管理器”比如 Tampermonkey、Violentmonkey、ScriptCat 都行。脚本管理器负责在合适的时候把我们的代码注入到目标页面里然后我们就能用 JS 去操作页面的 DOM把不想看到的东西去掉或者把想要的功能加上去。很多人一听“脚本”就害怕觉得是不是得先会什么高深技术才能碰。实际上油猴脚本的开发门槛低到什么程度只需要懂最基础的 HTML、CSS、JavaScript 选择器就能写出能用的东西。相比自己开发一个完整的浏览器扩展油猴脚本省掉了打包、权限声明、上架审核这一整套流程直接在管理器的编辑器里改代码、按 CtrlS 保存再去刷新网页就能看到效果这个“改完即用”的反馈速度是传统扩展完全比不了的。这也是我选择油猴方案来做这两个小需求的核心原因快且足够灵活。1.2 两个需求的本质拆解把这两个需求放到代码层面来看本质上就是两件事隐藏知乎标题找到页面里那个我们不想要的标题栏元素把它的 display 属性改成 none或者干脆把它从 DOM 里移除。屏蔽百度搜索广告识别搜索结果里哪些块是广告然后把对应容器隐藏掉。这里面的关键难点不在于“怎么 hide 一个元素”这种操作而在于“怎么稳定地找到要处理的那个元素”。百度搜索结果页和知乎页面都是重度前端渲染的动态页面DOM 结构随时可能被框架重新生成类名也经常带着一长串随机字符串。如果脚本只是写死一个 class 去隐藏很可能一刷新就失效。所以真正的重头戏是选择器的设计以及如何用 MutationObserver 监听页面变化、保证脚本在动态渲染下依然有效。1.3 技术选型为什么用原生 JavaScript MutationObserver我先说结论这两个脚本都不需要 Vue、React也不需要引入 jQuery。油猴脚本的运行环境本身就是浏览器原生提供的 document.querySelector、querySelectorAll、MutationObserver 已经足够覆盖所有需求。用原生 JS 的好处很实在脚本体积小、启动快、不会跟页面上已有的框架打架而且对新手来说更容易理解每一步到底在干什么。真正让我花心思的是动态渲染的问题。百度在用户滚动、翻页、甚至是输入新的关键词时搜索结果列表都会局部更新知乎是典型的单页应用页面切换时整个内容区域都会重新渲染。如果我们只在页面加载时执行一次清理函数那页面一更新广告和标题栏就会“原地复活”。这种场景下MutationObserver 就是最顺手的工具它可以监听指定的 DOM 节点子树变化一旦发现有新的广告节点被插入我们立刻再清理一遍。为这个需求选技术栈其实就是在选“处理动态 DOM 变化”的方案。2. 开发环境与脚本骨架搭建2.1 安装脚本管理器第一步是先装一个油猴管理插件。我用得最多的是 Tampermonkey它稳定、兼容性好Chrome、Edge、Firefox 都能装。如果你用的是国产浏览器也可以考虑 ScriptCat脚本猫它在中文社区里反馈也不错。这里以 Tampermonkey 为例。安装完成后浏览器工具栏会出现一个图标。点开它选择“添加新脚本”就会进入一个代码编辑器页面。Tampermonkey 默认会给一段脚本模板里面全是 // UserScript 开头的元信息我们可以直接把模板全部删掉换成自己的内容。这个编辑器虽然看起来简陋但是有基础的语法高亮写小脚本足够了。如果觉得不顺手也可以在外部编辑器里写好再粘贴进来。2.2 用户脚本的头部元信息每个油猴脚本的开头部分都有一段特殊注释叫元信息块脚本管理器读的就是这段注释。它的作用类似于给脚本写一份“说明书”我叫什么名字、运行在哪些网站、在什么时候执行、是否需要额外权限。下面是我这个脚本的头部信息// UserScript // name 百度广告屏蔽与知乎标题隐藏 // namespace http://tampermonkey.net/ // version 1.0.0 // description 屏蔽百度搜索结果页广告同时支持隐藏知乎页面标题栏 // author yourname // match https://www.baidu.com/* // match https://www.zhihu.com/* // grant none // run-at document-end // /UserScript这里的 match 是最容易出问题的地方。它决定了脚本只在哪些网址下运行规则是“协议://域名/路径”其中 * 表示任意内容。比如 https://www.baidu.com/* 匹配百度域名下所有路径但不会匹配包含第三方子域名的页面。如果你在地址栏看到的是 baidu.com 而不是 www.baidu.com还需要再加一条 match https://baidu.com/* 才能覆盖完整。grant none 表示脚本不需要 GM_ 开头的特殊能力这样可以减少浏览器对脚本的权限提示也能避免一些跨域问题。run-at document-end 表示等 DOM 解析完毕后再执行比默认时机更稳避免脚本跑到一半页面还没准备好。2.3 整体代码骨架如果你同时干两件事最好在代码里做一个简单的分流。根据当前 URL 的 host 判断是百度还是知乎然后分别进入各自的处理函数。这样两个功能互不干扰也不会出现“在知乎页面跑百度清理逻辑”这种乌龙。(function () { use strict; // 通用工具函数等待某个选择器对应的元素出现 function waitFor(selector, callback) { const el document.querySelector(selector); if (el) { callback(el); return; } const observer new MutationObserver(() { const target document.querySelector(selector); if (target) { observer.disconnect(); callback(target); } }); observer.observe(document.body, { childList: true, subtree: true }); // 为避免观察器无限挂下去可以加一个超时 setTimeout(() observer.disconnect(), 20000); } function init() { const host location.host; if (host.includes(baidu.com)) { initBaiduAdBlocker(); } else if (host.includes(zhihu.com)) { initZhihuTitleHider(); } } if (document.body) { init(); } else { document.addEventListener(DOMContentLoaded, init); } })();这里简单解释一下 waitFor 的作用。页面在刚加载时某些 DOM 节点还没生成直接 document.querySelector 很可能拿到 null。waitFor 做的事情是如果元素不存在就挂一个 MutationObserver 去监听 DOM 变化一旦目标出现就执行回调并在 20 秒后自动断开观察器。这个函数在后面的广告清理里也会用到属于一个可以复用的基础能力。3. 核心实战屏蔽百度搜索广告3.1 先搞清楚百度广告的 DOM 结构百度搜索广告的形态一直在变但底层的识别逻辑没有变广告结果块里一定会有一个写着“广告”的标签元素。问题是怎么找到这个标签并且从标签向上回溯到整个结果卡片。最简单的办法是打开百度搜一个词然后按 F12 打开开发者工具用左上角的“选取元素”图标点一下那个带“广告”字样的标签看看它在 DOM 里的位置。以我观察到的常见结构来说广告标签通常是嵌套在几层 div 里的最外层容器一般带有类似 result、container、wrapper、item 之类的类名。不同时间、不同账号看到的类名可能不一样所以不建议把类名写死而是用“先找文字再向上寻找容器”的方式这样适配性会好得多。3.2 第一版按“广告”文字反查容器我写清理函数的核心思路是从搜索内容列表 #content_left 里找出所有文本恰好是“广告”二字的叶子节点然后沿着它的父节点依次往上找最多找 6 层。如果在某层节点上看到类名里包含 result、container、wrapper、item 这些典型标志就认定这是承载广告的结果块直接隐藏它。function initBaiduAdBlocker() { const clean () { const contentLeft document.querySelector(#content_left); if (!contentLeft) return; // 找出文本节点内容恰好是“广告”且没有子元素的节点 const adLabels [...contentLeft.querySelectorAll(span, div)].filter((el) { return el.children.length 0 el.textContent.trim() 广告; }); for (const label of adLabels) { let node label; for (let i 0; i 6; i) { node node.parentElement; if (!node) break; const cls typeof node.className string ? node.className : ; if (/result|container|wrapper|item/.test(cls)) { if (!node.dataset.adHidden) { node.style.display none; node.dataset.adHidden true; } break; } } } }; clean(); const observer new MutationObserver(() clean()); observer.observe(document.body, { childList: true, subtree: true }); }为什么要限制 adLabels 是“叶子节点”因为如果一个 div 里既有“广告”二字又有别的业务内容说明它可能不是广告标签本身误删概率会高很多。真正的广告标签一般是一个纯文本的小 span或者只有一个文字节点的 div很少再嵌套别的东西。加上这个条件之后误伤率会明显下降。隐藏而不是移除是个重要细节。直接把节点从 DOM 里 remove 掉当然也能达到眼不见的目的但如果某些页面逻辑依赖这个节点的存在移除后可能会引发布局异常或者报错。用 style.display none 只是让它不可见节点还在原位置占位信息也还在但页面看起来就是干净了。另外用 dataset.adHidden 做一个标记可以避免同一个广告块被重复处理降低无谓的性能损耗。3.3 扩展识别“推广”和右侧广告栏百度广告的关键字并不止“广告”一个“推广”也经常出现特别是移动端或者某些特殊类型的搜索结果里。这两种情况下页面还会把广告明确标成“推广”。所以我把关键字列表做成数组方便以后添加const AD_KEYWORDS [广告, 推广]; const adLabels [...contentLeft.querySelectorAll(span, div)].filter((el) { if (el.children.length ! 0) return false; const text el.textContent.trim(); return AD_KEYWORDS.includes(text); });这里用 includes 而不是正则是因为关键字本身是固定字符串用数组维护更清晰以后想加“赞助”“推荐”之类的词只需要在数组里加一项就行。不过要提醒一句加关键字时一定要克制。如果你把“推荐”这种通用词加进去就很容易把百度自己的“相关推荐”模块也一并隐藏掉甚至误伤某些正常结果。我的建议是只保留“广告”和“推广”这两个最明确的词。右侧的广告横幅通常放在 #content_right 区域里。我不建议直接把整个 #content_right 隐藏掉因为百度右侧还有一些“百度热搜”“资讯”之类的模块不一定全是广告部分人可能还想看。更稳妥的做法是同样在 #content_right 里跑一遍文字反查逻辑找出带“广告”标签的块然后只隐藏这些块。代码和上面的 clean 基本一样只是把查找范围从 #content_left 换成 #content_right 即可。3.4 防止误伤的几个经验我一开始写这个脚本时把类名判断写得太宽只要看到一个容器类名带 item 就直接隐藏结果把一条正常搜索结果的子区域给藏了页面出现一大块空白。后来我把判断条件收紧了不再单靠类名而是以“找到广告文字标签”为前提类名只作为辅助验证。简单说就是先确定这个节点里真的有“广告”二字再去看它的外层是不是结果容器两者同时满足才隐藏。还有一个容易踩的坑是文本匹配范围。如果你直接对某个大 div 做 el.textContent.includes(广告)那几乎所有包含“广告投放”“广告行业”等字眼的正常内容都可能被命中。所以我用了 children.length 0 来限定必须是“没有子元素的叶子节点”并且比较时用严格相等 而不是 includes确保文本就是孤零零的“广告”二字。这两个条件一起作用误伤率才会降下来。4. 核心实战隐藏知乎标题4.1 知乎页面的标题栏到底藏在哪里说完了百度再说知乎。知乎页面是典型的单页应用DOM 结构比百度还要复杂一点。通常“知乎标题”指的是页面顶部的固定导航栏不管你怎么滚动它都悬在屏幕顶部白白占掉一块高度。也有朋友会觉得问题页左侧那个大标题区块太占地方想看回答还得先往下滑那其实也是标题只是位置不同。这两种场景我都可以处理关键是先定位到目标元素。打开知乎随便进一个问题页按 F12 就可以看到顶部导航栏通常有一个类似 AppHeader 的类名问题标题区则有 QuestionHeader、QuestionHeader-title 之类的类名。不同知乎改版阶段类名会变所以我不会把希望全押在一个选择器上而是准备一个选择器列表脚本运行时逐个去试命中哪个就处理哪个。这样即使某一个版本改版只要类名不是全变脚本依然能工作。4.2 选择器列表与隐藏逻辑我写的隐藏函数大概是下面这个样子function initZhihuTitleHider() { // 按优先级排列的候选选择器 const TITLE_SELECTORS [ .AppHeader, .QuestionHeader, .QuestionHeader-content, .QuestionHeader-title, .Sticky ]; const hide () { for (const selector of TITLE_SELECTORS) { const el document.querySelector(selector); if (el !el.dataset.titleHidden) { el.style.display none; el.dataset.titleHidden true; } } }; hide(); const observer new MutationObserver(() hide()); observer.observe(document.body, { childList: true, subtree: true }); }这个逻辑看起来比百度的还要简单但有一个细节需要注意知乎的问题页是动态渲染的通往问题页的每次路由切换都会重新生成很多 DOM 节点标题区域也会被重新创建。所以这里的 MutationObserver 监听不能只在页面加载完后调一次 hide 就结束而是要持续监听确保每一次路由变化后重新隐藏。我用 dataset.titleHidden 做标记也是为了避免已经隐藏过的节点被反复设置 display 属性虽然这不影响结果但能减少一丝丝不必要的 DOM 操作。理论上如果你只是想让标题不占空间把 display 设成 none 就够了。但如果你追求更好的视觉效果还需要处理布局偏移。比如隐藏顶部导航栏后下面内容区域可能还有 margin-top 在撑着这时页面顶部会出现一片空白。处理方法是把对应元素的 margin 一并置零比如给 body 设置一个额外的类写成 body.zh-focus .AppHeader { display: none !important; } 这种 CSS 方式可以同时解决隐藏和间距问题。油猴脚本里直接操作 CSS 也是可行的。4.3 加一个手动开关避免一刀切如果只是把标题永远隐藏很快你会发现一个问题有些时候你还是需要看到标题的比如想确认当前在哪个问题下、想点一下标题回到首页。所以我在脚本里加了一个小开关在页面右下角生成一个悬浮按钮点击后在“专注模式”和“普通模式”之间切换。专注模式下隐藏标题普通模式下全部恢复。function createToggleButton() { const btn document.createElement(button); btn.textContent 切换标题; btn.style.position fixed; btn.style.right 20px; btn.style.bottom 20px; btn.style.zIndex 999999; btn.addEventListener(click, () { document.body.classList.toggle(zh-focus-mode); clean(); }); document.body.appendChild(btn); }这里用 body 类名作为状态标记在 click 事件里切换类名再重新执行一次 clean。clean 函数里加一个判断如果 body 有 zh-focus-mode 类就隐藏标题如果没有就移除隐藏样式。这个方案比每次都改按钮文案更便于扩展。想要记住用户的偏好状态后面还可以接上 GM_setValue 存储刷新后依然保持用户选择。5. 常见问题与排查技巧实录5.1 脚本完全没生效先检查这几处如果你把脚本粘进去保存了刷新页面却发现什么都没发生八成不是代码逻辑问题而是脚本压根没运行。第一个要排查的是 match 规则看看当前页面地址是不是真的被你写的规则覆盖了。比如知乎现在有多种域名变体zhihu.com 和 www.zhihu.com 虽然都能访问但 match 规则里必须分别写清楚否则在其中一个域名下脚本就不会注入。第二个要排查的是 Tampermonkey 的脚本开关看看是不是不小心把整个脚本关闭了。还有一种情况是脚本启动了但执行时机太早。虽然我在元信息里写了 run-at document-end但某些页面里 DOM 结构在 document-end 之后仍然可能继续加载。如果脚本里直接 document.querySelector 然后操作元素很可能拿到 null后面自然没有效果。解决办法就是把操作放进 waitFor 函数里等待目标元素真正出现后再处理。这也是我在骨架代码里写 waitFor 的原因。5.2 刚隐藏完就被“原地复活”怎么办这个问题主要出现在百度上。滚动加载翻页后或者输入新关键词后搜索结果区域被局部重绘刚才隐藏的广告又冒出来了。原因很简单页面重新渲染时原本的 DOM 节点被删除然后新插入了全新的广告节点。这时候数据标记 dataset.adHidden 只停留在旧节点身上新节点完全没有标记所以清理函数又需要再跑一次。我用的 MutationObserver 监听整个 document.body任何子节点变化都会触发清理。这里要特别提醒一下防抖的问题。MutationObserver 回调可能会在页面变动时被连续触发几十次如果每次都去跑完整的清理逻辑页面会明显感觉到卡顿。比较好的做法是在回调里加一个 setTimeout 防抖比如 300 毫秒内只执行最后一次清理let cleanTimer null; const observer new MutationObserver(() { clearTimeout(cleanTimer); cleanTimer setTimeout(clean, 300); }); observer.observe(document.body, { childList: true, subtree: true });5.3 误伤了正常搜索结果怎么定位当发现某条正常结果被隐藏时先别急着改代码打开开发者工具看那个节点的 DOM 结构把它的类名和层级关系记录下来。然后对照我的代码找到是因为哪个条件把这条结果误判成了广告。通常有两种情况一是广告标签的叶子节点找不到反而在一个包含“广告”词组的正文节点上匹配了解决方法是继续收紧叶子节点的条件二是节点外层有 result 类名但文本里包含“广告投放”等词这时需要进一步限定“文本必须等于广告/推广而不是包含”。还有一个非常实用的调试技巧在 Tampermonkey 的编辑器里把脚本里的 node.style.display none 临时改成 node.style.outline 3px solid red刷新页面后你就能清楚地看到脚本到底找到了哪些节点。红色的边框会把这些节点框出来方便你判断是否有误杀。改回红色边框后再改回 display 就行。这个小技巧帮我省了很多排查时间。5.4 脚本冲突与多脚本并行油猴管理器通常允许同时启用多个脚本。如果页面卡顿除了防抖问题还要考虑是不是有多个脚本同时在监听 DOM。比如你同时装了别的百度去广告脚本两个脚本可能都在操作同一个节点互相打架就很容易出现内容反复闪烁甚至页面卡死。解决方法很简单同类功能保留一个就行或者用 exclude 规则把有冲突的站点排除掉。另外页面自己也可能有框架代码在监听 DOM我们的脚本频繁修改 DOM如果业务框架刚好要做虚拟 DOM 比对可能触发额外的重渲染就会造成闪烁。遇到这种情况可以把 display none 的方式改为添加 CSS 样式的方式用一个全局样式表去覆盖比如const style document.createElement(style); style.textContent #content_left div[class*result] { display: none; } ; document.head.appendChild(style);这两种方案各有优劣动态 JS 隐藏更灵活CSS 覆盖更稳定。真实场景中我倾向先用 JS 隐藏出问题时再切换成 CSS 方案去做对比。5.5 常见问题速查表现象可能原因解决办法脚本完全不运行match 规则没覆盖当前网址检查域名与路径匹配规则刚加载有效滚动后失效页面动态渲染导致节点重建用 MutationObserver 持续监听正常内容被错误隐藏关键词匹配过于宽泛要求叶子节点文本严格等于“广告”页面滚动和输入卡顿MutationObserver 回调太频繁回调加 300ms 防抖隐藏后顶部留下大块空白只隐藏了目标元素未处理 margin同时把容器 margin 清零知乎点击切换后状态丢失刷新后 body 类名重置用 GM_setValue 持久化状态6. 发布与后续扩展6.1 分享给别人的时候应该注意什么如果你觉得这两个脚本合起来还不错想分享给朋友或者传到 GreasyFork 之类的用户脚本社区建议先做几件小事给脚本起一个一看就懂的名字比如“百度去广告 知乎隐藏标题助手”描述里写清楚适用平台、效果截图最好放一两张处理前后的对比图版本号从 1.0.0 开始有改动再逐次递增。还有一个重要的事情是检查 match 规则是否符合预期千万不要用过于宽泛的 match比如 https:///否则脚本会在所有网站上运行既拖慢所有页面也容易让人怀疑你有恶意行为。从安全角度说社区上分享脚本基本靠作者自觉没什么强制审核所以你在复制别人的脚本时也要长个心眼最好大致扫一眼代码确认没有奇怪的请求地址。自己写脚本给自己用同样要保持这个习惯别为了省事从不可信的地方粘贴大段不明代码。6.2 后续可以扩展的方向现在这两个脚本是最基础的版本但在实际使用中可以扩展的地方其实很多。比如百度屏蔽那块现在只是硬编码了“广告”和“推广”两个关键词更好一点的做法是在脚本里给用户一个配置面板用简单 checkbox 切换哪些类型的模块需要隐藏比如“百度热搜”“百家号”“相关推荐”一键启停。知乎标题隐藏那边可以做成按页面类型区分在问题页只隐藏问题标题在首页只隐藏顶部导航不同规则分别生效。还有更进阶的路子不直接隐藏而是拖拽移动标题栏的位置或者把标题改成半透明悬停时出现。油猴脚本的能力边界只取决于你对 DOM 操作的理解遇到页面改版维护思路永远是“先定位新结构再调整选择器和匹配条件”。这也是这类脚本最核心的长期技能。我个人在实际操作中的体会是写这类脚本最大的收获并不是“屏蔽了几个广告”而是通过一次次跟页面 DOM 斗智斗勇慢慢摸清了浏览器、用户脚本和前端框架之间的运行关系。你不需要等别人给你做好一个工具只要愿意打开开发者工具多按几下 F12很多页面上的烦人元素都能亲手处理掉。最后再分享一个小技巧不要把自己的脚本藏起来随手备份到 Gist 或者自己的博客里下次换电脑或者网页改版时你会发现回头找代码比重新写一遍省太多事了。
返回列表