// ==UserScript== // @name CSDN 广告净化 // @namespace https://blog.csdn.net/ // @version 1.5.0 // @description 隐藏 CSDN 博客页的广告与 AI/会员推广悬浮组件 // @author you // @match *://*.csdn.net/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; const DEBUG = false; // 改成 true 可在控制台看到命中日志 const log = (...a) => DEBUG && console.log('[CSDN净化]', ...a); let hiddenCount = 0; /* ========================================================= * 1. 隐藏规则 * --------------------------------------------------------- * match : JS 清理时用的选择器 * container : 需要整体移除的父容器;为空则只处理 match 元素本身 * remove : true 直接删除节点(不留空白),false 只 display:none * guard : 可选。返回 true 才算广告 —— 用于类名过于通用、 * 需要二次确认的场景,避免误伤同名元素 * css : 可选。自定义注入 CSS 的选择器(数组),或用 false 完全 * 不进 CSS。默认取 match + container。 * 凡是"类名通用"的规则都应在这里换成更高精度的选择器, * 例如 :has(...),做到精准命中、绝不误伤。 * ========================================================= */ const RULES = [ { name: '会员推广悬浮卡片', // ele.html 里选中的那个元素;同时按 href 兜底,防止 class 改名 match: '.cp-flash__title-hit, a[href*="mall.csdn.net/vip"]', container: '.cp-flash', remove: true, }, { name: '文章广告位(左/右/上/下)', // .simple-ad-slot 是广告容器组件,-left / -right 等为其变体 match: '.simple-ad-slot', container: '.simple-ad-slot', remove: true, }, { name: 'AI 写同款悬浮窗', // .cons 类名太通用,绝不能裸写进 CSS;CSS 侧用 :has() 锁定 // 「墨言 AI 图标」或「#rollTx 滚动文案」,JS 侧再用 guard 二次校验 match: 'span.cons', css: ['span.cons:has(.simple-moyan-mark)', 'span.cons:has(#rollTx)'], guard: (el) => !!el.querySelector('.simple-moyan-mark, #rollTx, .fix'), container: '', remove: false, // 该节点由页面 JS 动态增删,display:none 比删除更稳,不会让页面脚本报错 }, { name: '墨衍会员引流徽章', // 「墨衍会员 · AI 创作全网分发」徽章,跳转 mp.csdn.net/vip // 注意:其内部 .simple-moyan-mark 与上一条 AI 悬浮窗共用图标类名, // 因此绝不能把 .simple-moyan-mark 写进隐藏规则,只能锁外层容器。 match: '.simple-moyan-member-badge, a[href*="mp.csdn.net/vip"]', container: '.simple-moyan-member-badge', remove: true, }, { name: '底部吸底作者栏', // 滚动正文时从底部弹出的作者名片(头像/作者名/码龄粉丝原创/+关注)。 // 页面 JS 通过给它追加 is-show 类来触发展示,所以:见下面两个刻意的选择 // // ① remove: false —— 只 display:none,绝不删除节点。 // 该节点是页面 JS 常驻持有并通过 classList 切换的(wrap -> wrap is-show)。 // 一旦删除,它的滚动监听里 querySelector('#simple-sticky-userbar') // 就会拿到 null,classList.add 直接抛错,可能连带搞挂同批次的其它脚本。 // ② CSS 用 #id 选择器(权重 1,0,0)而非纯类选择器。 // 页面的 .simple-sticky-userbar-wrap.is-show 权重是 0,2,0;若它内部写了 // display !important,纯类选择器会因权重更低而失效。id 选择器权重更高, // 无论对方样式表加载顺序如何都能压住。JS 侧还会再补一层行内 !important。 match: '#simple-sticky-userbar, .simple-sticky-userbar-wrap', css: [ '#simple-sticky-userbar', '.simple-sticky-userbar-wrap', '.simple-sticky-userbar-wrap.is-show', ], container: '', remove: false, }, { name: '右侧栏整块(AI阅读助手 / 上下篇导航 / 滚动条)', // ele.html 明确标注:「全部需要清除,包括ai阅读助手、上下篇导航、滚动条」 // DOM 层级: #rightAside > .recommend-right_aside > #recommend-right // > (cknows 卡 / #groupfile / .article-previous 上下篇导航) // // 直接锁最外层 #rightAside 一次性全清,而不是逐个给内部子元素加规则 —— // 侧栏是 CSDN 最爱塞新推广模块的地方,锁外层后将来新增什么都自动覆盖, // 也不用为「滚动条」这类附属元素单独找选择器(随容器一起消失)。 // 注意 #rightAside 同时带 .recommend-right[data-type="recommend"], // 两个选择器指向同一节点,写成两个是防 id 改名的兜底。 // // remove: false —— 侧栏是页面滚动吸顶/定位脚本的作用对象, // 删除节点有可能让那些脚本查不到目标而抛错;display:none 同样能让 // flex 布局回收这块空间,视觉效果一致且无风险。 match: '#rightAside, .recommend-right[data-type="recommend"]', container: '#rightAside', remove: false, }, { name: 'C知道「AI 阅读助手」卡片', // 右侧栏内由 Swiper 轮播驱动的推广卡(核心速览/代码判官/知识延伸)。 // 该卡片父级 #rightAside 已被上面那条整块清除,所以本条属于冗余兜底: // 万一 CSDN 把这张卡挪到侧栏之外(他们常做 A/B 投放),这里仍然拦得住。 // remove: false —— 该节点挂着 Swiper 实例(自动轮播 + 尺寸观测), // 删除后 Swiper 内部仍会对已分离节点做 transform/尺寸计算,风险高于收益; // 只隐藏即可达到完全相同的视觉效果。 match: '.cknows-side-card, .js-aside-cknows', container: '', remove: false, }, { name: '右侧栏广告位', match: '.blog-right-advert', container: '.blog-right-advert', remove: true, }, { name: '顶部/工具条广告', match: '#dmp_ad, .toolbar-advert', container: '#dmp_ad, .toolbar-advert', remove: true, }, ]; /* ========================================================= * 2. 页面加载前注入 CSS —— 抢在渲染之前隐藏,杜绝闪屏 * 注意:每条选择器单独成一条规则。若把它们拼成一个大逗号列表, * 只要其中一条不被浏览器支持,整条规则会被整体丢弃,导致全部失效。 * ========================================================= */ function buildCss() { const blocks = []; for (const rule of RULES) { if (rule.css === false) continue; let list; if (Array.isArray(rule.css)) { list = rule.css; } else { list = [ ...rule.match.split(','), ...(rule.container ? rule.container.split(',') : []), ]; } for (const sel of new Set(list.map((s) => s.trim()).filter(Boolean))) { blocks.push( `${sel} {\n` + ` display: none !important;\n` + ` visibility: hidden !important;\n` + ` pointer-events: none !important;\n` + `}` ); } } return blocks.join('\n'); } const style = document.createElement('style'); style.id = 'csdn-adblock-style'; style.textContent = buildCss(); (document.head || document.documentElement).appendChild(style); log('CSS 已注入:', style.textContent.split('\n').length, '行'); /* ========================================================= * 3. JS 清理 —— 处理动态注入 / 内联样式覆盖 / 通用类名精确判定 * ========================================================= */ function hideElement(el, rule) { if (!el) return; if (rule.remove) { // 删除型:节点一旦移除就结束,用标记避免重复操作。 // (若页面之后又插入一个同类新节点,那是新元素、没有标记,会被正常处理) if (el.dataset.csdnAdblockDone === '1') return; el.dataset.csdnAdblockDone = '1'; if (el.isConnected) el.remove(); } else { // 隐藏型:不删除、只压 display:none,且必须是幂等的。 // 为什么不能打标记跳过?因为这类节点常带有页面动态计算的行内样式 // (如吸底栏的 left / width),页面重写 style 属性时会把我们的 // display:none 一并冲掉。每次清理都校验一次,才能自动补回。 if (el.style && el.style.getPropertyValue('display') === 'none' && el.style.getPropertyPriority('display') === 'important') { return; // 已是目标状态,无需重复操作 } el.style.setProperty('display', 'none', 'important'); } hiddenCount++; log(`已清理【${rule.name}】`, el); } function cleanup() { for (const rule of RULES) { let targets; try { targets = document.querySelectorAll(rule.match); } catch (e) { log(`选择器无效,已跳过【${rule.name}】`, rule.match); continue; } for (const el of targets) { if (el.dataset.csdnAdblockDone === '1') continue; // 通用类名兜底校验:不确认是广告就不动手。内部内容可能晚于 // 节点出现,所以每次清理都重新判断,而不是一次性定性。 if (typeof rule.guard === 'function' && !rule.guard(el)) continue; // 有 container 规则时,尽量向上找到广告卡片本体整体移除 let victim = el; if (rule.container) { let node = el; while (node && node !== document.body) { if (node.matches && node.matches(rule.container)) { victim = node; break; } node = node.parentElement; } } hideElement(victim, rule); // 同时确保命中元素本身也被处理掉 if (victim !== el) hideElement(el, { ...rule, remove: false }); } } } /* ========================================================= * 4. 持续监听 —— CSDN 的广告是异步插入的,需要反复清理 * ========================================================= */ function startObserver() { cleanup(); // 4.1 MutationObserver:DOM 一变就清理(合并到微任务,避免频繁触发) let scheduled = false; const observer = new MutationObserver(() => { if (scheduled) return; scheduled = true; queueMicrotask(() => { scheduled = false; cleanup(); }); }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'], }); // 4.2 兜底轮询:部分广告由定时器插入,MutationObserver 可能漏掉 let ticks = 0; const timer = setInterval(() => { cleanup(); // 前 30 秒高频清理,之后降频常驻 if (++ticks === 120) { clearInterval(timer); setInterval(cleanup, 3000); } }, 250); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startObserver, { once: true }); } else { startObserver(); } })();