// ==UserScript== // @name 百度搜索广告净化 // @namespace https://scriptcat.org/users/pwhscr // @version 1.0.0 // @description 移除百度搜索结果页的推广/广告条目,恢复干净的结果列表。多重信号识别,支持动态加载结果,附带拦截计数与一键还原。 // @author pwhscr // @license MIT // @homepageURL https://userscripts-toolkit.app.workbuddy.host/ // @supportURL https://userscripts-toolkit.app.workbuddy.host/ // @match https://www.baidu.com/* // @match https://baidu.com/* // @match https://m.baidu.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const STORE_KEY = 'baidu_adblock_stats'; // 不同脚本管理器对布尔值的存取方式不一致(有的原样返回,有的返回 'true'/'false' 字符串),统一收口 function readBool(key, def) { const v = GM_getValue(key, def); if (v === false || v === 'false' || v === 0 || v === '0') return false; if (v === true || v === 'true' || v === 1 || v === '1') return true; return !!def; } // 兼容「存储的是字符串」与「存储的是对象」两种 GM 实现 function readJSON(key) { const raw = GM_getValue(key, ''); if (!raw) return {}; if (typeof raw === 'string') { try { return JSON.parse(raw); } catch (e) { return {}; } } return typeof raw === 'object' ? raw : {}; } // ---------- 状态 ---------- const state = { enabled: readBool('enabled', true), // 用户点了「显示」后进入暂停态:不再自动收敛,否则他刚展开就又被收回去 paused: false, hidden: 0, total: 0, hiddenSinceSave: 0, // 记录被隐藏的节点,方便「临时显示」 records: [], stash: [], }; // ---------- 广告识别 ---------- // 思路:不用单一 class 判断(百度改版频繁),而是多信号加权。 // 实测(2026-09)新版百度在结果块内部标注: // 广告 // 注意 data-tuiguang 挂在块内的 span 上,容器本身不带这个属性。 // 容器自身或内部出现这些 class 即视为广告位 const AD_CLASS_TOKENS = [ 'ec-tuiguang', 'ecfc-tuiguang', 'ec_ads', 'ec_wise_ad', 'ec_ad_', 'wise_ad', 'result-ad', 'ad-result', ]; const AD_LABEL_RE = /^(广告|广告·|商业推广|推广|赞助|品牌推广|广告位)[\s·]*$/; // 百度自然结果带的锚点属性,作为「不是广告」的保护条件 const NATURAL_ATTR = /mu=|data-log=/; // 官方广告标注元素(新版挂在结果块内部,老版在容器自身) const MARK_SEL = '[data-tuiguang], .ec-tuiguang, .ecfc-tuiguang'; /** 容器自身、或容器内的标注元素是否带广告 class */ function hasStrongClass(el) { const own = ((el.getAttribute('class') || '') + ' ' + (el.getAttribute('id') || '')).toLowerCase(); if (own.trim()) { if (AD_CLASS_TOKENS.some((t) => own.includes(t))) return true; // ec_r_ 系列是前缀式命名,不能整词匹配 if (/(^|\s)ec_r_/.test(own)) return true; } const inner = el.querySelector( '.ec-tuiguang, .ecfc-tuiguang, .ec_ads, .ec_wise_ad, [class*="ec_ad_"]'); return !!inner; } /** 块内是否出现「广告」文字标签(限制尺寸,避免误伤正文里提到"广告"的结果) */ function hasAdLabel(el) { const nodes = el.querySelectorAll('span, div, a, em, i'); for (const n of nodes) { const txt = (n.textContent || '').trim(); // 标签本身极短;长文本说明是摘要/标题,不是标注 if (!txt || txt.length > 6 || !AD_LABEL_RE.test(txt)) continue; const r = n.getBoundingClientRect(); // 量得到尺寸且明显偏大 → 是内容块而非角标 if (r.width > 0 && r.height > 0 && (r.width > 90 || r.height > 34)) continue; return true; } return false; } /** 块内是否有百度的推广跳转链接(自然结果用的是 /link?url=,不是 baidu.php?url=) */ function hasPromotionLink(el) { for (const a of el.querySelectorAll('a[href]')) { const href = a.getAttribute('href') || ''; if (/baidu\.php\?url=/i.test(href) || a.hasAttribute('data-landurl')) return true; } return false; } /** 判定某个结果块是不是广告 */ function isAdBlock(el) { if (!el || el.nodeType !== 1) return false; if (el.dataset.cleanerHidden) return false; // 抓到的其实是上层容器时,一律不动——避免整个结果区被隐藏 if (looksLikeContainer(el)) return false; // ① 广告标注 class(自身或内部) if (hasStrongClass(el)) return true; // ② 块内(或块自身)存在官方广告标记属性 // 实测新版挂在块内的标注 span 上,老版挂在结果容器自身,两种都要覆盖 if (el.hasAttribute('data-tuiguang') || el.querySelector('[data-tuiguang]')) return true; // ③ 「广告」文字标签 if (hasAdLabel(el)) return true; // ④ 推广跳转链接(带自然结果锚点的块不看这条,避免误伤) if (!NATURAL_ATTR.test(el.outerHTML.slice(0, 400)) && hasPromotionLink(el)) return true; return false; } // ---------- 收集候选块 ---------- // 结果块的形态随版本变化: // 旧版:#content_left > div.result // 新版:#content_left > section > div.result (中间多包了一层) // 必须先下钻到真正的结果块,否则会把整个结果区当成"一条广告"整块隐藏。 const BLOCK_SEL = ':scope > .result, :scope > .result-op, :scope > .c-container, :scope > [class*="EC_result"]'; function collectRoots() { const roots = []; const left = document.querySelector('#content_left'); if (left) { for (const child of left.children) { const nested = child.querySelectorAll(BLOCK_SEL); if (nested.length) { for (const n of nested) roots.push(n); } else { roots.push(child); } } } // 顶部/底部/右侧的独立广告位 for (const id of ['#content_top', '#content_bottom', '#content_right', '#brand_ad', '#top-ad']) { const box = document.querySelector(id); if (box) roots.push(...box.children); } // 兜底 if (!roots.length) { roots.push(...document.querySelectorAll('.result, .result-op, .c-container')); } // 过滤掉空壳占位(innerText 对不可见元素返回空,用 textContent 兜底) return roots.filter((n) => n && n.nodeType === 1 && ((n.innerText || n.textContent || '').trim().length > 0)); } /** 安全阀:如果候选块里塞了很多条广告标记,说明抓到的是上层容器而不是单条广告。 * 宁可漏杀,也不能把整个结果区干掉。 */ function looksLikeContainer(el) { try { const marks = el.querySelectorAll(MARK_SEL); if (marks.length >= 3) return true; const txt = el.innerText || el.textContent || ''; if (txt.length > 6000) return true; } catch (e) { /* 忽略 */ } return false; } /** * 从官方广告标注向上找它所属的那一条结果块。 * 这是主力手段——新版百度给结果块用的是 CSS Modules 哈希 class(如 u2a6nht), * 靠 class 名字根本认不出来,但标注元素本身是可靠的锚点。 */ function blockOfMarker(marker, zone) { let cur = marker; let best = null; for (let i = 0; i < 12; i++) { if (!cur || cur.nodeType !== 1 || cur === zone) break; // 注意:querySelectorAll 只查后代,标注挂在容器自身时要单独算上 const marks = (cur.matches(MARK_SEL) ? 1 : 0) + cur.querySelectorAll(MARK_SEL).length; const len = (cur.textContent || '').trim().length; // 标记变多或文本量暴涨 → 已经越过单条结果的边界 if (marks > 2 || len >= 2500) break; if (marks <= 2 && len > 8) best = cur; cur = cur.parentElement; } return best || marker; } // ---------- 执行净化 ---------- const ZONES = ['#content_left', '#content_right', '#content_top', '#content_bottom']; function clean(reason) { if (!state.enabled || state.paused) return 0; const targets = new Set(); // ① 主力:官方广告标注 → 反推它所在的那条结果 for (const z of ZONES) { const zone = document.querySelector(z); if (!zone) continue; for (const m of zone.querySelectorAll(MARK_SEL)) { const block = blockOfMarker(m, zone); if (block) targets.add(block); } } // ② 兜底:class 特征、「广告」文字标签、推广跳转链接 for (const el of collectRoots()) { if (isAdBlock(el)) targets.add(el); } let n = 0; for (const el of targets) { if (!el || !el.isConnected || el.dataset.cleanerHidden) continue; hide(el); n++; } if (n) { state.hidden += n; state.total += n; state.hiddenSinceSave += n; saveStats(); paintBadge(); } return n; } function hide(el) { if (el.dataset.cleanerHidden) return; el.dataset.cleanerHidden = '1'; el.dataset.cleanerPrevDisplay = el.style.display || ''; el.style.display = 'none'; state.records.push(el); } /** 临时显示被拦截的结果 */ function restore() { if (state.records.length) { state.stash = state.records.filter((el) => el.isConnected); } for (const el of state.stash) { el.style.display = el.dataset.cleanerPrevDisplay || ''; delete el.dataset.cleanerHidden; } state.records = []; paintBadge(); } /** 从「临时显示」切回净化状态 */ function rehide() { state.paused = false; state.records = state.stash.filter((el) => el.isConnected); for (const el of state.records) { el.style.display = 'none'; el.dataset.cleanerHidden = '1'; } state.stash = []; paintBadge(); } // ---------- 统计 ---------- function saveStats() { try { const today = new Date().toISOString().slice(0, 10); const data = readJSON(STORE_KEY); data[today] = (data[today] || 0) + state.hiddenSinceSave; state.hiddenSinceSave = 0; GM_setValue(STORE_KEY, JSON.stringify(data)); } catch (e) { /* 忽略 */ } } // ---------- 界面 ---------- let badge = null; function paintBadge() { if (!badge) return; const btn = badge.querySelector('.bd-toggle'); const label = badge.querySelector('.bd-label'); const count = badge.querySelector('.bd-count'); if (state.paused) { badge.style.display = 'flex'; badge.classList.add('paused'); label.textContent = '已还原'; count.textContent = String(state.stash.length); btn.textContent = '隐藏'; return; } badge.classList.remove('paused'); label.textContent = '已净化'; btn.textContent = '显示'; if (!state.records.length) { badge.style.display = 'none'; return; } badge.style.display = 'flex'; count.textContent = String(state.records.length); } function buildBadge() { if (badge || !document.body) return; badge = document.createElement('div'); badge.id = 'baidu-ad-cleaner-badge'; badge.innerHTML = [ '', '已净化', '0', '条广告', '', ].join(''); const css = document.createElement('style'); css.textContent = ` #baidu-ad-cleaner-badge{position:fixed;right:16px;bottom:16px;z-index:2147483000;display:none; align-items:center;gap:6px;padding:7px 11px;border-radius:999px;background:rgba(24,28,36,.92); color:#e8eaed;font:12px/1.4 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif; box-shadow:0 4px 16px rgba(0,0,0,.22);backdrop-filter:blur(8px);user-select:none} #baidu-ad-cleaner-badge .bd-dot{width:7px;height:7px;border-radius:50%;background:#41d18a} #baidu-ad-cleaner-badge .bd-count{font-weight:700;color:#41d18a} #baidu-ad-cleaner-badge.paused .bd-dot{background:#f0a531} #baidu-ad-cleaner-badge.paused .bd-count{color:#f0a531} #baidu-ad-cleaner-badge .bd-toggle{margin-left:4px;padding:2px 8px;border:1px solid rgba(255,255,255,.28); border-radius:999px;background:transparent;color:#e8eaed;font-size:11px;cursor:pointer} #baidu-ad-cleaner-badge .bd-toggle:hover{background:rgba(255,255,255,.14)}`; document.head.appendChild(css); document.body.appendChild(badge); badge.querySelector('.bd-toggle').addEventListener('click', () => { if (!state.paused) { state.paused = true; restore(); } else { rehide(); clean('resume'); } // 每轮点击最多生效一次,避免用户连点导致状态错位 setTimeout(paintBadge, 0); }); paintBadge(); } // ---------- 观察动态加载 ---------- let timer = null; function scheduleClean() { if (timer) return; timer = setTimeout(() => { timer = null; clean('mutation'); }, 260); } function watch() { const obs = new MutationObserver(scheduleClean); obs.observe(document.documentElement, { childList: true, subtree: true }); } // ---------- 启动 ---------- function boot() { buildBadge(); clean('init'); // 首屏后几秒是有广告注入的高峰期,多扫几轮 [400, 900, 1800, 3200].forEach((t) => setTimeout(() => clean('delayed'), t)); watch(); // 单页/翻页时重新激活 window.addEventListener('popstate', scheduleClean); } // ---------- 菜单 ---------- try { GM_registerMenuCommand(state.enabled ? '停用净化(本页生效)' : '启用净化(本页生效)', () => { state.enabled = !state.enabled; GM_setValue('enabled', state.enabled); state.paused = false; if (!state.enabled) { restore(); state.stash = []; } else { state.stash = []; clean('menu'); } paintBadge(); }); GM_registerMenuCommand('查看累计统计', () => { const data = readJSON(STORE_KEY); const today = new Date().toISOString().slice(0, 10); const sum = Object.values(data).reduce((a, b) => a + (parseInt(b, 10) || 0), 0); alert('广告净化统计\n\n今日:' + (data[today] || 0) + ' 条\n累计:' + sum + ' 条\n\n(按自然日统计)'); }); } catch (e) { /* GM_registerMenuCommand 不可用时忽略 */ } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();