// ==UserScript== // @name 广告终结者 // @namespace http://tampermonkey.net/ // @version 2.9 // @description 强化检测拦截能力和使用体验优化 // @author DeepSeek // @match *://*/* // @exclude *://*.bing.com/* // @exclude *://*.iqiyi.com/* // @exclude *://*.qq.com/* // @exclude *://*.v.qq.com/* // @exclude *://*.sohu.com/* // @exclude *://*.mgtv.com/* // @exclude *://*.ifeng.com/* // @exclude *://*.pptv.com/* // @exclude *://*.sina.com.cn/* // @exclude *://*.56.com/* // @exclude *://*.cntv.cn/* // @exclude *://*.tudou.com/* // @exclude *://*.baofeng.com/* // @exclude *://*.le.com/* // @exclude *://*.pps.tv/* // @exclude *://*.fun.tv/* // @exclude *://*.baidu.com/* // @exclude *://*.ku6.com/* // @exclude *://*.tvsou.com/* // @exclude *://*.kankan.com/* // @exclude *://*.douyu.com/* // @exclude *://*.weibo.com/* // @exclude *://*.people.com.cn/* // @exclude *://*.cctv.com/* // @exclude *://*.gdtv.com.cn/* // @exclude *://*.ahtv.cn/* // @exclude *://*.tvb.com/* // @exclude *://*.tvmao.com/* // @exclude *://*.douban.com/* // @exclude *://*.163.com/* // @exclude *://*.bilibili.com/* // @exclude *://*.gov.cn/* // @exclude *://*.thepaper.cn/* // @exclude *://*.xinhuanet.com/* // @exclude *://*.china.com/* // @exclude *://*.jianshu.com/* // @exclude *://*.amazon.cn/* // @exclude *://*.cnblogs.com/* // @exclude *://*.cnstock.com/* // @exclude *://*.baike.com/* // @exclude *://*.guokr.com/* // @exclude *://*.360doc.com/* // @exclude *://*.qiushibaike.com/* // @exclude *://*.zol.com.cn/* // @exclude *://*.pconline.com.cn/* // @exclude *://*.pcpop.com/* // @exclude *://*.it168.com/* // @exclude *://*.gfan.com/* // @exclude *://*.feng.com/* // @exclude *://*.xiaomi.cn/* // @exclude *://*.10086.cn/* // @exclude *://*.10010.com/* // @license MIT // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; const REGEX = { dynamicId: /^(?:[0-9a-f]{16,}|[\dA-F-]{20,})$/i, adAttributes: /(?:ad(?:s|vert|vertisement|box|frame|container|wrap|content|label)?|推广|广告|gg|sponsor|推荐|guanggao|syad|bfad|弹窗|悬浮|葡京|banner|pop(?:up|under)|track(?:ing)?)[_-]?/i, thirdParty: /^(?!(.*\.)?(baidu\.com))/i, textAdKeywords: /限时(?:优惠|免费)|立即(?:下载|注册|咨询)|微信(?:号|客服)?[::]?|vx[::]\w+|telegram[::]\w+|免广告|偷拍|黑料|点击下载/, obfuscatedAds: { adUrl: /(?:\/[a-z0-9]{12,}\/|(?:adservice|tracking|promo)\.[a-z]{3,10}\.(?:com|net))(?:\/|$)(?:ads?|promo|banner|track|stat|click|log)/i, hashedFile: /[a-f0-9]{32,}\.(?:js|css|html)|(?:[a-z0-9]{20,}|[A-Z0-9]{20,})\.(?:js|css|html)/i, adFunction: /(?:get|fetch|load|show|display|render|set(?:Timeout|Interval))\(.*?(?:Ad|Banner|Popup)/i, dynamicCode: /(?:eval|new\s+Function|Function)\(.*?(?:decodeURIComponent|atob|fromCharCode)/i, iframeWrite: /(?:\.createElement\(['"]iframe['"]\)|document\.write\([\s\S]*?<(script|iframe|img).*?(src|href)=['"]?(?:[^'">]*?(?:ad|banner|popup))['"]?)/i, base64Data: /(?:data:.*?(?:ad|banner)|(?:[A-Za-z0-9+/]{4}){10,}(?:ad|banner)|data:image\/(?:png|jpeg|gif);base64,)/i, inlineScript: /]*>[\s\S]*?(?:ad|banner|popup|[a-zA-Z0-9]{8})[\s\S]*?<\/script>/i , encodedString: /(?:base64|hex)\([^)]+\)/i, unicodeCharEncoded: /\\u[0-9a-f]{4}(?:\\u[0-9a-f]{4}){3,}/gi } }; const CONFIG = { maxLogs: 30, protectionRules: { dynamicIdLength: 18, zIndexThreshold: 100, maxFrameDepth: 4, textAdKeywords: ['限时优惠','立即下载','微信','vx:','telegram','偷拍','黑料','扫码关注','点击下载'] }, defaultSettings: { dynamicSystem: true, layoutSystem: true, frameSystem: true, mediaSystem: true, textSystem: true, thirdPartyBlock: true, obfuscatedAdSystem: true }, perf: { mutationDebounce: 200, idleTimeout: 2500, batchSize: 40, styleCacheTTL: 600000, styleCacheMax: 75, mutationLimit: 150, observerOptions: { childList: true, subtree: true, attributes: true, attributeFilter: ['id', 'class', 'style', 'src', 'href'], attributeOldValue: false } }, mobileOptimizations: { lazyLoadImages: true, removeImagePlaceholders: true } }; const DOM = { metaKeywords: document.querySelector('meta[name="keywords"]'), metaDescription: document.querySelector('meta[name="description"]'), body: document.body }; const StyleCache = new Map(); let lastStyleCacheClear = Date.now(); function main() { new CoreSystem().init(); UIController.init(); console.log('✅ 广告拦截系统已激活 - 当前域名:', location.hostname); } class CoreSystem { constructor() { this.observer = new MutationObserver(this.processMutations.bind(this)); this.processed = new WeakSet(); this.pendingMutations = []; this.mutationTimer = null; this.idleController = null; } init() { this.injectStyles(); this.initialClean(); this.observer.observe(DOM.body, CONFIG.perf.observerOptions); if (CONFIG.mobileOptimizations.lazyLoadImages) { this.lazyLoadImages(); } if (CONFIG.mobileOptimizations.removeImagePlaceholders) { this.removeImagePlaceholders(); } } removeImagePlaceholders() { const images = document.querySelectorAll('img'); images.forEach(img => { img.addEventListener('load', () => { img.classList.add('loaded'); // 添加 loaded 类 }); }); const style = document.createElement('style'); style.textContent = ` img:not(.loaded) { background-color: #eee; /* 浅灰色占位符 */ min-height: 50px; /* 最小高度 */ min-width: 50px; /* 最小宽度 */ } `; document.head.appendChild(style); } lazyLoadImages() { const images = document.querySelectorAll('img[data-src]'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.removeAttribute('data-src'); observer.unobserve(img); } }); }); images.forEach(img => { observer.observe(img); }); } getCachedStyle(el) { if (Date.now() - lastStyleCacheClear > CONFIG.perf.styleCacheTTL) { StyleCache.clear(); lastStyleCacheClear = Date.now(); } if (StyleCache.size > CONFIG.perf.styleCacheMax) { const oldestKey = [...StyleCache.keys()][0]; StyleCache.delete(oldestKey); } const key = el.nodeName + Array.from(el.classList).join(''); if (StyleCache.has(key)) return StyleCache.get(key); const style = getComputedStyle(el); StyleCache.set(key, style); return style; } injectStyles() { const existing = document.getElementById('ad-blocker-styles'); if (existing) return; const style = document.createElement('style'); style.id = 'ad-blocker-styles'; let cssRules = []; if (Config.get('layoutSystem')) { cssRules.push(` [style*="fixed"], [style*="sticky"] { position: static!important; top: auto!important; bottom: auto!important; } `); } if (Config.get('frameSystem')) { cssRules.push(` iframe[src*="ad"], .ad-container { display: none!important; height: 0!important; width: 0!important; opacity: 0!important; } `); } style.textContent = cssRules.join('\n'); document.head.appendChild(style); } initialClean() { const elements = [ ...document.getElementsByTagName('script'), ...(Config.get('frameSystem') ? document.getElementsByTagName('iframe') : []), ...document.querySelectorAll('div, img, a, ins') ]; this.batchProcess(elements, true); } processMutations(mutations) { this.pendingMutations.push(...mutations); if (this.pendingMutations.length > CONFIG.perf.mutationLimit) { this.pendingMutations = this.pendingMutations.slice(-CONFIG.perf.mutationLimit); } if (this.mutationTimer) clearTimeout(this.mutationTimer); this.mutationTimer = setTimeout(() => { if (this.idleController) { this.idleController.abort(); this.idleController = null; } const nodes = new Set(); for (const { addedNodes } of this.pendingMutations.flat()) { for (const node of addedNodes) { if (node.nodeType === 1 && !this.processed.has(node)) { nodes.add(node); this.processed.add(node); } } } this.idleController = new AbortController(); this.batchProcess([...nodes], { signal: this.idleController.signal }); this.pendingMutations = []; }, CONFIG.perf.mutationDebounce); } async batchProcess(nodes, options = {}) { const processQueue = []; const processNode = (node) => { if (!node || node.nodeType !== 1) return; this.checkDynamicSystem(node); this.checkLayoutSystem(node); this.checkMediaSystem(node); this.checkTextSystem(node); this.checkFrameSystem(node); this.checkObfuscatedAds(node); this.checkThirdParty(node); if (node.children.length > 0) { processQueue.push(...node.children); } }; const visibleNodes = nodes.filter(n => { const rect = n.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }); for (const node of visibleNodes) { processNode(node); } const remainingNodes = nodes.filter(n => !visibleNodes.includes(n)); let index = 0; const processChunk = () => { if (options.signal?.aborted) return; const start = performance.now(); while (index < remainingNodes.length && performance.now() - start < 15) { processNode(remainingNodes[index]); index++; } if (index < remainingNodes.length) { requestIdleCallback(processChunk, { timeout: 200 }); } }; if (remainingNodes.length > 0) { requestIdleCallback(processChunk, { timeout: 200 }); } } checkDynamicSystem(el) { if (!Config.get('dynamicSystem')) return; const attrs = el.getAttributeNames().map(name => `${name}=${el.getAttribute(name)}` ).join(' '); if (REGEX.adAttributes.test(attrs)) { AdUtils.safeRemove(el, 'DynamicSystem', { type: '广告属性检测', detail: `属性: ${attrs.slice(0, 100)}` }); return; } if (el.id && (el.id.length > CONFIG.protectionRules.dynamicIdLength || REGEX.dynamicId.test(el.id))) { AdUtils.safeRemove(el, 'DynamicSystem', { type: '动态ID检测', detail: `异常ID: ${el.id}` }); return; } } checkLayoutSystem(el) { if (!Config.get('layoutSystem')) return; const style = this.getCachedStyle(el); const zIndex = parseInt(style.zIndex); if (zIndex > CONFIG.protectionRules.zIndexThreshold) { AdUtils.safeRemove(el, 'LayoutSystem', { type: '高堆叠元素', detail: `z-index=${zIndex}` }); return; } const rect = el.getBoundingClientRect(); if (style.position === 'fixed' && rect.width < 300) { AdUtils.safeRemove(el, 'LayoutSystem', { type: '固定定位元素', detail: `尺寸: ${rect.width}x${rect.height}` }); return; } } checkMediaSystem(el) { if (!Config.get('mediaSystem')) return; const style = this.getCachedStyle(el); const rect = el.getBoundingClientRect(); if (el.tagName === 'IMG' && (el.src.includes('ad') || el.src.match(/\.(gif|webp)(\?|$)/))) { AdUtils.safeRemove(el, 'MediaSystem', { type: '图片广告', detail: `图片源: ${el.src.slice(0, 50)}` }); return; } if (['fixed', 'sticky'].includes(style.position)) { const isTopBanner = rect.top < 50; const isBottomBanner = rect.bottom > window.innerHeight - 50; if (isTopBanner || isBottomBanner) { AdUtils.safeRemove(el, 'MediaSystem', { type: '浮动广告', detail: `位置: ${rect.top}px` }); return; } } } checkTextSystem(el) { if (!Config.get('textSystem')) return; const text = el.textContent?.toLowerCase() || ''; if (REGEX.textAdKeywords.test(text)) { AdUtils.safeRemove(el, 'TextSystem', { type: '文本广告', detail: `关键词: ${text.slice(0, 80)}` }); return; } } checkFrameSystem(el) { if (!Config.get('frameSystem')) return; if (el.tagName === 'IFRAME') { let depth = 0, parent = el; while ((parent = parent.parentElement) && depth <= CONFIG.protectionRules.maxFrameDepth) { if (parent.tagName === 'IFRAME') depth++; } if (depth > CONFIG.protectionRules.maxFrameDepth) { AdUtils.safeRemove(el, 'FrameSystem', { type: '深层嵌套框架', detail: `嵌套层级: ${depth}` }); return; } } } checkObfuscatedAds(el) { if (!Config.get('obfuscatedAdSystem')) return; const scripts = el.getElementsByTagName('script'); for (let script of scripts) { const src = script.src || script.innerHTML; if (Object.values(REGEX.obfuscatedAds).some(r => r.test(src))) { AdUtils.safeRemove(script, 'ObfuscatedAdSystem', { type: '混淆广告检测', detail: `源: ${src.slice(0, 80)}` }); return; } } } checkThirdParty(el) { if (!Config.get('thirdPartyBlock')) return; if (el.tagName === 'SCRIPT' || el.tagName === 'IFRAME') { const src = el.src || el.getAttribute('data-src') || ''; if (REGEX.thirdParty.test(src)) { AdUtils.safeRemove(el, 'ThirdPartyBlock', { type: '第三方资源拦截', detail: `源: ${src.slice(0, 80)}` }); return; } } } } class AdUtils { static safeRemove(node, module, reason) { if (!node?.parentNode) return false; try { Logger.logRemoval({ module, element: { tag: node.tagName, id: node.id, class: node.className, html: node.outerHTML?.slice(0, 300) }, reason }); node.style.display = 'none'; node.style.visibility = 'hidden'; node.style.opacity = '0'; node.setAttribute('data-ad-removed', 'true'); return true; } catch(e) { console.warn('元素隐藏失败:', e); return false; } } } class Config { static get currentDomain() { return location.hostname.replace(/^www\./, ''); } static get(key) { const data = GM_getValue('config') || {}; const domainConfig = data[this.currentDomain] || {}; return domainConfig[key] ?? CONFIG.defaultSettings[key]; } static set(key, value) { const data = GM_getValue('config') || {}; data[this.currentDomain] = { ...CONFIG.defaultSettings, ...data[this.currentDomain], [key]: value }; GM_setValue('config', data); } static toggleAll(status) { const data = GM_getValue('config') || {}; data[this.currentDomain] = Object.fromEntries( Object.keys(CONFIG.defaultSettings).map(k => [k, status]) ); GM_setValue('config', data); } static getTextAdKeywords() { const data = GM_getValue('textAdKeywords', {}); return data[this.currentDomain] || CONFIG.protectionRules.textAdKeywords; } static setTextAdKeywords(keywords) { const data = GM_getValue('textAdKeywords', {}); data[this.currentDomain] = keywords; GM_setValue('textAdKeywords', data); } } class UIController { static init() { this.registerMainMenu(); this.registerModuleCommands(); this.registerUtilityCommands(); this.registerTextAdKeywordsCommand(); } static registerMainMenu() { const allEnabled = Object.keys(CONFIG.defaultSettings).every(k => Config.get(k)); GM_registerMenuCommand( `🔘 主开关 [${allEnabled ? '✅' : '❌'}]`, () => this.toggleAllModules(!allEnabled) ); } static registerModuleCommands() { const modules = [ ['dynamicSystem', '动态检测系统 (ID/属性)'], ['layoutSystem', '布局检测系统 (定位/z-index)'], ['frameSystem', '框架过滤系统 (iframe)'], ['mediaSystem', '媒体检测系统 (图片/浮动)'], ['textSystem', '文本广告检测系统'], ['thirdPartyBlock', '第三方拦截'], ['obfuscatedAdSystem', '混淆广告检测系统'] ]; modules.forEach(([key, name]) => { GM_registerMenuCommand( `${name} [${Config.get(key) ? '✅' : '❌'}]`, () => this.toggleModule(key, name) ); }); } static registerUtilityCommands() { GM_registerMenuCommand('📜 查看拦截日志', () => this.showLogs()); GM_registerMenuCommand('🧹 清除当前日志', () => Logger.clear()); GM_registerMenuCommand('📝 管理文本关键词', () => this.manageTextAdKeywords()); } static manageTextAdKeywords() { const currentKeywords = Config.getTextAdKeywords(); const action = prompt( `当前文本关键词:\n${currentKeywords.join(', ')}\n\n` + `输入新的关键词以添加(支持中文逗号和英文逗号分隔),\n` + `输入"0"以清空关键词,\n` + `输入"1"以恢复默认关键词,\n` + `或直接按"确定"以取消。` ); if (action === null) return; if (action === '0') { Config.setTextAdKeywords([]); this.showNotification('文本关键词已清空'); } else if (action === '1') { Config.setTextAdKeywords(CONFIG.protectionRules.textAdKeywords); this.showNotification('文本关键词已恢复默认'); } else if (action) { const newKeywords = action.split(/[,,]/).map(k => k.trim()).filter(k => k); const updatedKeywords = [...new Set([...currentKeywords, ...newKeywords])]; Config.setTextAdKeywords(updatedKeywords); this.showNotification('文本关键词已更新'); } } static toggleModule(key, name) { const value = !Config.get(key); Config.set(key, value); this.showNotification(`${name} ${value ? '✅ 已启用' : '❌ 已禁用'}`); setTimeout(() => location.reload(), 500); } static toggleAllModules(status) { Config.toggleAll(status); this.showNotification(`所有模块已${status ? '启用' : '禁用'}`); setTimeout(() => location.reload(), 500); } static showLogs() { const logs = Logger.getLogs(); const currentDomainLogs = logs.filter(log => log.domain === Config.currentDomain); alert(currentDomainLogs.length ? `📃 最近${CONFIG.maxLogs}条拦截记录:\n\n${currentDomainLogs.map(l => `类型: ${l.type}\n元素: ${l.element}\n规则: ${l.detail}` ).join('\n\n')}` : '暂无拦截记录' ); } static showNotification(text, duration = 2000) { GM_notification({ title: '广告终结者', text: text, silent: true, timeout: duration }); } } class Logger { static logRemoval(data) { const logs = GM_getValue('logs', []); logs.push({ module: data.module, type: data.reason.type, detail: data.reason.detail, element: `${data.element.tag}#${data.element.id}#${data.element.class}`, domain: Config.currentDomain, timestamp: new Date().toISOString() }); GM_setValue('logs', logs.slice(-CONFIG.maxLogs)); } static getLogs() { return GM_getValue('logs', []); } static clear() { GM_setValue('logs', []); UIController.showNotification('日志已清空'); } } main(); })();