// ==UserScript== // @name 网页广告拦截器 // @namespace http://tampermonkey.net/ // @version 6.8.0 // @author DeepSeek&GLM // @description 一个手机端via浏览器适用的强大广告拦截器。注入时机分三档:电脑端浏览器配合脚本猫/Violentmonkey/Tampermonkey(动态模式)多为解析前早注入,仅开「拦截内嵌脚本」即可拦截多数广告链;手机端Firefox/雨见+篡改猴等真扩展为半早注入;Via等套壳/WebView浏览器为加载完成后晚注入。因此某些网页在电脑端仅开「拦截内嵌脚本」即可去广告,手机端套壳浏览器上却大概率失效——此时若拦截日记有记录而广告仍残留,可开启「清空残留容器」尝试隐藏;更建议开启「拦截动态脚本」+「拦截第三方资源」,或直接开启「覆盖层拦截」「苹果设备模拟」。 // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_listValues // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @connect * // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ===== 注入时机判档 ===== // 【功能】脚本启动时读取一次 document.readyState, 区分两类宿主: // 早注入档 = document-start 注入(readyState=loading, 主流管理器扩展); // 晚注入档 = 页面加载完成后注入(readyState=interactive/complete, 套壳浏览器)。 // 【机制】晚注入档下解析期已结束: 解析期专用的竞速布置按判档跳过; // 零操作跳转保护以「注入后窗口」替代「加载期」; 存量 DOM 由初始扫描直接接管。 var _rs0 = 'loading'; try { _rs0 = document.readyState || 'loading'; } catch (e) {} var _LATE_REGIME = _rs0 !== 'loading'; var _INIT_AT = Date.now(); var _ZERO_TOUCH_LATE_MS = 10000; // ===== 轻量扩展求值引擎 ===== // 支持计算样式谓词 :matches-css / :matches-css-before / :matches-css-after // (prop: value, 值支持精确/通配符*/正则 /re/, 大小写不敏感), 基底用原生 querySelectorAll 求值; // :has()/:not() 为浏览器原生伪类, 路由层把纯原生的这类规则分流进样式表轨; // :contains()/:has-text()/:upward()/:nth-ancestor()/:xpath() 等扩展伪类不支持, // 含这些伪类的规则按原样跳过(不生效)。 var MiniExtendedCss = (function () { var _PRED_RE = /:matches-css(-before|-after)?\(\s*([a-zA-Z-]+)\s*:\s*([^)]*)\)/g; var _UNSUP_RE = /:(has|contains|has-text|upward|nth-ancestor|xpath|if|if-not|abp-has)\(/; // 宽基底(*)规则 MO 重扫限频窗: 新增节点风暴时星号子树求值(限界候选×计算样式)是 // 扩展引擎最贵的一段, 窗口内的 MO 批次跳过星号扫描, 由补扫定时器对根节点一次兜底 var _STAR_THROTTLE_MS = 500; function splitList(s) { var out = []; var cur = ''; for (var i = 0; i < s.length; i++) { var c = s.charAt(i); if (c === ',') { if (cur.trim()) out.push(cur.trim()); cur = ''; } else cur += c; } if (cur.trim()) out.push(cur.trim()); return out; } // 解析单条扩展选择器: 剥掉 :matches-css* 谓词得原生基底; 返回 null 表示非扩展/不支持 function parseSelector(sel) { var preds = []; var rest = sel.replace(_PRED_RE, function (m, pseudo, prop, val) { preds.push({ pseudo: pseudo ? pseudo.replace(/^-/, '') : null, prop: prop.trim().toLowerCase(), value: val.trim() }); return ''; }); if (!preds.length) return null; if (_UNSUP_RE.test(rest)) return null; var base = rest.trim(); if (!base) base = '*'; return { base: base, preds: preds }; } function cssVal(el, pseudo, prop, memo) { try { var mkey = (pseudo || '') + '|' + prop; var em = null; if (memo) { em = memo.get(el); if (em && em.has(mkey)) return em.get(mkey); } var v; if (pseudo) { if (!_globals.getComputedStyle) return null; v = _globals.getComputedStyle(el, pseudo === 'before' ? '::before' : '::after').getPropertyValue(prop); } else { v = _globals.getComputedStyle ? _globals.getComputedStyle(el).getPropertyValue(prop) : null; } if (memo) { if (!em) { em = new Map(); memo.set(el, em); } em.set(mkey, v); } return v; } catch (e) { return null; } } // 谓词值三态匹配: 精确(无通配符)/通配符(*/跨任意段)/正则(/re/ 包裹, 无 flags), 大小写不敏感; // z-index 纯数字走数值相等快路径; 编译结果缓存在谓词对象上(扫描热路径只编译一次) function buildPredMatcher(pred) { if (pred._m) return pred._m; var val = String(pred.value); var m; if (pred.prop === 'z-index' && /^\d+$/.test(val)) { m = { t: 'zi', n: parseInt(val, 10) }; } else if (val.length > 2 && val.charAt(0) === '/' && val.charAt(val.length - 1) === '/') { try { m = { t: 're', r: new RegExp(val.slice(1, -1), 'i') }; } catch (e) { m = null; } if (!m) m = { t: 'exact', s: val.toLowerCase() }; } else if (val.indexOf('*') !== -1) { var parts = val.split('*'); for (var k = 0; k < parts.length; k++) parts[k] = parts[k].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); try { m = { t: 're', r: new RegExp('^' + parts.join('.*') + '$', 'i') }; } catch (e2) { m = { t: 'exact', s: val.toLowerCase() }; } } else { m = { t: 'exact', s: val.toLowerCase() }; } pred._m = m; return m; } function predOk(el, pred, memo) { var v = cssVal(el, pred.pseudo, pred.prop, memo); if (v === null || v === undefined) return false; var m = buildPredMatcher(pred); if (m.t === 'zi') { var n = parseInt(v, 10); return !isNaN(n) && n === m.n; } if (m.t === 're') { try { return m.r.test(String(v)); } catch (e) { return false; } } return String(v).toLowerCase() === m.s; } function ruleOk(el, parsed, memo) { for (var i = 0; i < parsed.preds.length; i++) { if (!predOk(el, parsed.preds[i], memo)) return false; } return true; } function MiniExtendedCss(opts) { this._rules = []; this._hidden = []; this._mo = null; try { var sheet = (opts && opts.styleSheet) || ''; var lines = sheet.split(String.fromCharCode(10)); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; var br = line.indexOf('{'); if (br <= 0) continue; var list = splitList(line.slice(0, br)); for (var j = 0; j < list.length; j++) { var parsed = parseSelector(list[j]); if (parsed) this._rules.push({ parsed: parsed, sel: list[j] }); } } } catch (e) {} this._hasStarRule = false; for (var _ri = 0; _ri < this._rules.length; _ri++) { if (this._rules[_ri].parsed.base === '*') { this._hasStarRule = true; break; } } this._lastStarScanAt = 0; this._starTimer = null; this._churnAt = []; this._churnN = []; this._susp = []; } // 宽基底(*)规则 MO 限频补扫: 限频窗内跳过的星号扫描由这个定时器对根节点一次兜底, // 覆盖窗内累积的全部新增节点; 非星号规则(窄基底)不受限频, 照常即时求值 MiniExtendedCss.prototype._scheduleStarCatchup = function () { var self = this; if (self._starTimer) return; self._starTimer = _setTimeout(function () { self._starTimer = null; try { self._scan(_document.documentElement || _document, null); } catch (e) {} }, _STAR_THROTTLE_MS); }; // 猫鼠熔断: 单条扩展规则在滚动窗内增量隐藏计数, 超阈值即冻结该规则本页的增量求值, // 阈值高于引擎单轮候选上限 —— 单轮扫描永不触发, 持续多轮重建(约>60个/秒)才熔断, // 已隐藏元素保持原状(样式观察器维持), 刷新自动恢复。仅统计变更驱动路径, 初始全量 // 扫描不计 —— 正常页面远达不到阈值, 触发即意味着页面在持续批量重建同类元素 MiniExtendedCss.prototype._noteHide = function (idx, fromMo) { if (!fromMo) return; var now = Date.now(); if (!this._churnAt[idx] || now - this._churnAt[idx] > 10000) { this._churnAt[idx] = now; this._churnN[idx] = 0; } this._churnN[idx]++; if (this._churnN[idx] > 600 && !this._susp[idx]) { this._susp[idx] = true; try { if (typeof LogManager !== 'undefined' && LogManager.add) LogManager.add('elementCssBlock', null, { type: 'CSS_EXT_CHURN_BREAK', detail: '扩展规则短窗隐藏超限(页面在持续批量重建同类元素), 本页暂停其增量求值, 刷新恢复: ' + String((this._rules[idx] && this._rules[idx].sel) || '').slice(0, 120) }); } catch (e) {} } }; MiniExtendedCss.prototype._hide = function (el) { try { if (el.getAttribute && el.getAttribute('data-ab-ext') === '1') return; // 脚本自身 UI 豁免(标记面板宿主已 markSafe): 宽形态规则不得把自家拾取面板当广告藏掉 if (typeof _isSafe === 'function' && _isSafe(el)) return; if (typeof _hasSafeAncestor === 'function' && _hasSafeAncestor(el)) return; if (el.setAttribute) el.setAttribute('data-ab-ext', '1'); el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); this._protect(el); this._hidden.push(el); } catch (e) {} }; MiniExtendedCss.prototype._scan = function (root, memo, fromMo) { try { if (!root) return; // 无谓容器标签整棵跳过: 这类节点自身不可能满足扩展谓词(不可见/无计算形态), // 省掉"每新增节点 × 每规则 qSA+computed 求值"里的整段空转 if (root.nodeType === 1 && (root.tagName === 'SCRIPT' || root.tagName === 'STYLE' || root.tagName === 'LINK' || root.tagName === 'META' || root.tagName === 'NOSCRIPT' || root.tagName === 'BR' || root.tagName === 'HR')) return; var self = this; // 全量基底(*)规则的子树候选整批只查一次, 多条 * 规则共享同一集合 —— // 子树查询数与规则数解耦, N 条宽版规则共用一次 qSA var starSub = null; for (var i = 0; i < self._rules.length; i++) { var p = self._rules[i].parsed; if (self._susp && self._susp[i]) continue; try { if (root.matches && root.matches(p.base) && ruleOk(root, p, memo)) { self._noteHide(i, fromMo); if (!self._susp[i]) self._hide(root); } } catch (e) {} if (p.base === '*') { // 宽基底 MO 限频: 限频窗内的 MO 批次跳过星号子树求值, 由补扫定时器兜底 if (fromMo && self._hasStarRule) { var _nowS = Date.now(); if (_nowS - self._lastStarScanAt < _STAR_THROTTLE_MS) { self._scheduleStarCatchup(); continue; } self._lastStarScanAt = _nowS; } // 全量基底(去标签化的形态规则): 命中元素常是新增容器内的子节点, 必须扫子树; // 限界预筛 class/id 持有者(伪元素层样式必须以 class/id 为目标)防超大子树卡段; // 非渲染元素(script/style/link/meta/noscript/br/hr)的伪元素无计算形态, // position 恒不等于 fixed, 候选直接剔除(结果不变, 省去无效求值) if (!starSub) { starSub = []; if (root.querySelectorAll) { try { var _sub = root.querySelectorAll('*[class],[id]'); for (var _sk = 0; _sk < _sub.length && _sk < 500; _sk++) { var _tg = (_sub[_sk].tagName || '').toUpperCase(); if (_tg === 'SCRIPT' || _tg === 'STYLE' || _tg === 'LINK' || _tg === 'META' || _tg === 'NOSCRIPT' || _tg === 'BR' || _tg === 'HR') continue; starSub.push(_sub[_sk]); } } catch (e) {} } } for (var _si2 = 0; _si2 < starSub.length; _si2++) { var _el2 = starSub[_si2]; try { if (_el2.getAttribute && _el2.getAttribute('data-ab-ext') === '1') { if (_el2.style && _el2.style.getPropertyValue('display') !== 'none') self._applyHideStyles(_el2); continue; } if (ruleOk(_el2, p, memo)) { self._noteHide(i, fromMo); if (!self._susp[i]) self._hide(_el2); } } catch (e) {} } continue; } if (p.base.indexOf(' ') !== -1) continue; if (!root.querySelectorAll) continue; var els = root.querySelectorAll(p.base); for (var j = 0; j < els.length; j++) { var el = els[j]; try { if (el.getAttribute && el.getAttribute('data-ab-ext') === '1') { // 已标记元素被页面改回可见(如整串 cssText 重写)时重新隐藏 if (el.style && el.style.getPropertyValue('display') !== 'none') self._applyHideStyles(el); continue; } if (ruleOk(el, p, memo)) { self._noteHide(i, fromMo); if (!self._susp[i]) self._hide(el); } } catch (e) {} } } } catch (e) {} }; // 隐藏样式补写(保护观察者与重隐共用; 不重复入 _hidden/不打标记) MiniExtendedCss.prototype._applyHideStyles = function (el) { el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); }; // 每个被隐藏元素挂 style 属性观察: 页面改写内联样式(整串 cssText 重写等)后自动补隐; // 单元素补隐上限 50 次防互改死循环; 元素离树即断开观察 MiniExtendedCss.prototype._protect = function (el) { var self = this; if (!self._protectors) self._protectors = []; if (self._protectors.length > 400) return; var n = 0; var mo = new _MutationObserver(function () { try { if (!el.isConnected) { mo.disconnect(); return; } if (el.style && el.style.getPropertyValue('display') !== 'none') { if (++n > 50) { mo.disconnect(); return; } self._applyHideStyles(el); } } catch (e) {} }); try { mo.observe(el, { attributes: true, attributeFilter: ['style'] }); } catch (e) { return; } self._protectors.push(mo); }; MiniExtendedCss.prototype.apply = function () { var self = this; try { this._scan(_document.documentElement || _document); } catch (e) {} if (this._mo) { try { this._mo.disconnect(); } catch (e) {} } try { this._mo = new MutationObserver(function (muts) { try { // 同批求值记忆: 主规则+变体镜像规则常读同一元素同一属性, computed 只取一次; // 记忆随回调结束丢弃, 不跨回调缓存(页面样式会变) var memo = null; try { memo = new Map(); } catch (e) {} for (var m = 0; m < muts.length; m++) { var nodes = muts[m].addedNodes; for (var n = 0; n < nodes.length; n++) { var node = nodes[n]; if (!node || node.nodeType !== 1) continue; self._scan(node, memo, true); } } } catch (e) {} }); this._mo.observe(_document.documentElement, { childList: true, subtree: true }); } catch (e) {} }; MiniExtendedCss.prototype.dispose = function () { if (this._mo) { try { this._mo.disconnect(); } catch (e) {} this._mo = null; } if (this._starTimer) { try { _clearTimeout(this._starTimer); } catch (e) {} this._starTimer = null; } this._lastStarScanAt = 0; this._churnAt = []; this._churnN = []; this._susp = []; if (this._protectors) { for (var pi = 0; pi < this._protectors.length; pi++) { try { this._protectors[pi].disconnect(); } catch (e2) {} } this._protectors = []; } for (var i = 0; i < this._hidden.length; i++) { try { var el = this._hidden[i]; if (!el || !el.style) continue; if (el.removeAttribute) el.removeAttribute('data-ab-ext'); el.style.removeProperty('display'); el.style.removeProperty('visibility'); el.style.removeProperty('pointer-events'); } catch (e) {} } this._hidden = []; }; // 面板/卡片计数同款查询(只匹配不隐藏) MiniExtendedCss.query = function (selList) { var out = []; try { var seen = null; try { seen = new Set(); } catch (e) {} var items = splitList(String(selList || '')); for (var i = 0; i < items.length; i++) { var parsed = parseSelector(items[i]); if (!parsed) continue; var els = _document.querySelectorAll(parsed.base); for (var j = 0; j < els.length; j++) { if (ruleOk(els[j], parsed)) { if (seen) { if (!seen.has(els[j])) { seen.add(els[j]); out.push(els[j]); } } else out.push(els[j]); } } } } catch (e) {} return out; }; return MiniExtendedCss; })(); // ===== 轻量引擎定义结束 ===== // ======================== 调试辅助 ======================== // 【功能】全脚本统一的异常/调试日志入口 _debugCatch:所有 try/catch 的失败信息都从这里输出。 // 【机制】仅当 设置→高级→调试日志(DEBUG) 开启时才输出到 F12→Console([AB] 前缀),并自动附带调用位置行号; // 自身带防递归保护,catch 块里调用它不会二次抛错。 // 【用户体验】排查问题三步:① 开启 DEBUG → ② 触发问题 → ③ F12 Console 看 [AB] 日志; // 拦截记录看 设置面板→「拦截日志」(每条可点「加白」放行)。 // 轻量扩展求值引擎: MiniExtendedCss 以 var 声明在本 IIFE 作用域内, 直接引用 let _ExtCss = MiniExtendedCss; // 支持 :matches-css/-before/-after 计算样式谓词 const _debugCatch = function(e, ctx) { if (_debugCatch._isRunning) return; _debugCatch._isRunning = true; try { if (typeof currentConfig !== 'undefined' && currentConfig.DEBUG && typeof console !== 'undefined') { // 自动附带调用位置(行号), 无需各调用点手动传入截断上下文串 var _loc = ''; try { var _st = new Error().stack || ''; var _ln = _st.split('\n')[2]; if (_ln) _loc = ' @' + _ln.trim().slice(0, 90); } catch(_e2) {} console.warn('[AB]', ctx || '', _loc, e && e.message ? e.message : e); } } catch(_) { try { console.error('[AB] _debugCatch error:', _ && _.message ? _.message : _); } catch(__) {} } finally { _debugCatch._isRunning = false; } }; // ======================== 初始样式注入 ======================== // 【功能】页面一启动就注入全局隐藏类 ._uh 的样式定义。 // 【机制】覆盖层拦截等 JS 隐藏路径给元素加上 ._uh 类 → visibility:hidden + opacity:0 + pointer-events:none, // 保留原布局尺寸(不引发页面跳动),且不可点击;加白恢复时移除该类即可 // (CSS 样式表轨与扩展引擎的隐藏走内联样式+专属标记, 不经此类)。 // 【注意】此样式独立于任何模块,模块全关也保留;data-hs 属性用于识别脚本自身的样式标签。 (function () { try { var s = document.createElement('style'); s.setAttribute('data-hs', 'true'); s.textContent = '._uh{visibility:hidden!important;opacity:0!important;pointer-events:none!important}'; // 真早注入档 documentElement/head 可能为 null(CDP 预注/瞬时响应): 直接挂 document 会把本样式 // 当成文档根, 后续解析内容全部被弃(页面全废)。有根挂根; 无根挂 MO 等根出现再插。 var _hsHost = document.head || document.documentElement; if (_hsHost) { _hsHost.appendChild(s); } else { var _hsMo = new MutationObserver(function (_muts, _obs) { try { var _h = document.head || document.documentElement; if (_h) { _obs.disconnect(); _h.appendChild(s); } } catch (eHs2) { try { _obs.disconnect(); } catch (eHs3) {} } }); _hsMo.observe(document, { childList: true }); } } catch(e) { _debugCatch(e, "初始隐藏样式注入失败"); } })(); // ======================== 默认配置 ======================== // 【功能】定义脚本的全部默认状态:模块开关、高级配置项、CSP 规则模板、内置黑名单关键词、启发式正则、标题黑白名单。 // 【机制】面板里的开关/设置修改后写入 GM 存储(按域名隔离),刷新后生效;「全局重置」恢复本文件默认值。 // 【注意】内置黑名单是广告特征字符串(统计站域名/混淆代码片段),命中即拦,可在「脚本黑名单」面板单独移除; // 启发式黑名单默认关闭(误杀风险较高,需在高级设置手动开启)。 const DEFAULT_MODULE_STATE = { removeInlineScripts: false, removeExternalScripts: false, interceptThirdParty: false, blockDynamicScripts: false, manageCSP: false, overlayIntercept: false, scriptBlacklistMode: true, }; const DEFAULT_CONFIG_STATE = { inlineScriptStrictMode: false, dynamicScriptStrictMode: false, thirdPartyStrictMode: false, thirdPartyStrictMethod: false, funnelStrictMode: false, spoofUAEnabled: false, simplePlatformSpoof: false, residualCleanupEnabled: false, iframeUIFix: false, redirectBlockerEnabled: true, builtinBlacklistEnabled: true, redirectBlockerCompatibilityMode: true, heuristicBlacklistEnabled: false, crossTagPatternThreshold: 3, trackingParamsCleanerEnabled: true, redirectTitleBlacklistEnabled: true, redirectTitleWhitelistEnabled: true, redirectBlockAllEnabled: false, redirectWhitelistBlockMode: false, DEBUG: false, adLinkClickBlock: false, logNoiseReduction: true, overlayStrictMode: false, staticScriptContentCheck: true, }; const DEFAULT_CSP_RULES_TEMPLATE = [ { id: 1, name: '只允许同源外部脚本', rule: "script-src 'self'", enabled: false }, { id: 2, name: '只允许同源外部样式', rule: "style-src 'self'", enabled: false }, { id: 3, name: '只允许同源图片', rule: "img-src 'self'", enabled: false }, { id: 4, name: '只允许同源框架', rule: "frame-src 'self'", enabled: false }, { id: 5, name: '只允许同源媒体', rule: "media-src 'self'", enabled: false }, { id: 6, name: '只允许同源对象与嵌入', rule: "object-src 'self'", enabled: false }, { id: 7, name: '只允许同源网络请求', rule: "connect-src 'self'", enabled: false }, ]; // 内置黑名单关键词:常见广告/统计脚本特征串(如统计站域名、混淆代码片段), // 脚本内容或地址命中任意一条即拦截。 const BUILTIN_BLACKLIST_KEYWORDS = [ 'hm.baidu.com', 'tongji()', 'tj()', 'push.js', 'cnzz', 'histats.com', 'https://hongosi.xn--', '{return void 0!==b[a]?b[a]:a}).join("")}', '${scripts[randomIndex]}', '${scripts[Math.random()', 'https://"+Date.parse(new Date())+', 'https://"+(new Date().getDate())+"', 'https://{randomstr}.', 'new Function(t)()', 'new Function(b)()', 'new Function(c)()', 'new Function(t);', 'new Function(b);', 'new Function(c);', "new Function('d',e)", 'new Function(document[', 'new Function(function(p,a,c,k,e,d)', 'function guanggao(', 'nginx pc', 're::80(0[3-9]|10)/', 're::88(87|91)/', ':21467/', ':690/', ':6681/', ':21233/', '2147483646', 'OJ:5LF_XDRT4', '.insertAdjacentElement("beforebegin",_el)', 'Math.floor(2147483648 * Math.random());', 'Math.floor(Math.random()*url.length)', 'Math.floor(Math.random() * urls.length)', "new Date()['getTime']()", 'newDate=new window', 'Math.floor(((new Date()).getTime()', '&&navigator[', '=navigator;', 'navigator.platform){setTimeout(function', 'disableDebugger', 'blockDeveloperTools', '["Date"]())[\'getTime\']()', '\')', '<\\/\'+\'s\'+\'c\'+\'ri\'+\'pt\'+\'>\')', '(\'#htmlContenthtml\').html', 'D.createElement(\'span\');', 'window.$m(', '{win:false,mac:false,xll:false}', 'parseInt(Math[\'random\'', '999999999', 'writeln(\'