Greasy Fork

广告终结者

完整功能广告拦截器,支持内容关键词过滤与智能检测,增加小说网站UA伪装功能

目前为 2025-02-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         广告终结者
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  完整功能广告拦截器,支持内容关键词过滤与智能检测,增加小说网站UA伪装功能
// @author       TMHhz
// @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     *://*.www.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     *://*.www.gov.cn/*
// @exclude     *://*.thepaper.cn/*
// @exclude     *://*.xinhuanet.com/*
// @exclude     *://*.china.com/*
// @exclude     *://*.guancha.cn/*
// @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      GPLv3
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @connect      self
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ======================= 新增小说网站检测与UA修改 =======================
    (function detectNovelSite() {
        const novelKeywords = [
            'novel', 'xiaoshuo', '小说', '阅读', 
            'book', '章节', '文学', '小说网',
            'txt', 'download', '免费小说'
        ];
        
        const isNovelSite = () => {
            // URL检测
            const urlCheck = novelKeywords.some(k => 
                window.location.href.toLowerCase().includes(k)
            );
            
            // 标题检测
            const titleCheck = novelKeywords.some(k => 
                document.title.toLowerCase().includes(k)
            );
            
            // 内容特征检测
            const contentCheck = () => {
                const metaKeywords = document.querySelector('meta[name="keywords"]')?.content || '';
                const metaDescription = document.querySelector('meta[name="description"]')?.content || '';
                return novelKeywords.some(k => 
                    metaKeywords.includes(k) || metaDescription.includes(k)
                );
            };
            
            return urlCheck || titleCheck || contentCheck();
        };

        if (isNovelSite()) {
            // 塞班系统典型UA
            const symbianUA = 'NokiaN8-00/5.0 (Symbian/3; Series60/5.2 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/533.4 (KHTML, like Gecko) BrowserNG/7.3.1.37';
            
            // UA伪装注入
            Object.defineProperty(window.navigator, 'userAgent', {
                value: symbianUA,
                writable: false,
                configurable: false
            });
        }
    })();

    // ======================= 原核心代码保持不变 =======================
    // 核心配置对象
    const CONFIG = {
        maxLogs: 150,
        adKeywords: [
            'ad', 'ads', 'advert', 'banner', 'popup', '推广', '广告', 'gg', 'advertisement', 'sponsor', '推荐', 'adv', 'guanggao', 'syad', 'bfad', '男男', '女女', '弹窗', '悬浮', '浮动', '浮窗', '葡京', 'pop', 'sticky', 'fixed', 'tip', 'tips', 'adbox', 'adsense', 'adserver', 'advertmarket', 'advertising', 'cookie-sync', '偷拍', '黑料', '横幅', '乱伦'
        ],
        protectionRules: {
            dynamicIdLength: 12,
            zIndexThreshold: 50,
            maxFrameDepth: 3,
            textAdKeywords: ['限时优惠', '立即下载', '微信', 'vx:', 'telegram', '偷拍', '黑料']
        },
        contentFilter: {
            scanDepth: 3,
            minLength: 2,
            maxKeywords: 50,
            timeout: 300
        },
        defaultSettings: {
            dynamicSystem: true,
            layoutSystem: true,
            frameSystem: true,
            mediaSystem: true,
            textSystem: true,
            thirdPartyBlock: true,
            contentFilter: true
        }
    };

    // ======================= 工具类 =======================
    class AdUtils {
        static safeRemove(node, module, reason) {
            if (!node?.parentNode || this.isWhitelisted(node)) return false;
            
            try {
                Logger.logRemoval({
                    module,
                    element: {
                        tag: node.tagName,
                        id: node.id,
                        class: node.className,
                        html: node.outerHTML?.slice(0, 200)
                    },
                    reason
                });
                node.parentNode.removeChild(node);
                return true;
            } catch(e) {
                console.warn('元素移除失败:', e);
                return false;
            }
        }

        static isWhitelisted(element) {
            return element.closest('[data-protected]') || 
                   this.hasWhitelistContent(element);
        }

        static shouldBlockByContent(element) {
            if (!Config.get('contentFilter')) return false;
            
            const text = this.getCleanText(element);
            const blacklist = KeywordManager.getBlacklist();
            const whitelist = KeywordManager.getWhitelist();

            if (whitelist.some(k => text.includes(k))) return false;
            return blacklist.some(k => text.includes(k));
        }

        static getCleanText(element) {
            const rawText = this.extractText(element).trim();
            return this.normalizeText(rawText);
        }

        static extractText(element, depth = 0) {
            if (depth > CONFIG.contentFilter.scanDepth) return '';
            return Array.from(element.childNodes).map(n => {
                if (n.nodeType === Node.TEXT_NODE) return n.textContent;
                if (n.nodeType === Node.ELEMENT_NODE) {
                    return this.extractText(n, depth + 1);
                }
                return '';
            }).join(' ');
        }

        static normalizeText(text) {
            return text
                .replace(/\s+/g, ' ')
                .replace(/[【】《》「」“”‘’]/g, '')
                .toLowerCase();
        }

        static hasWhitelistContent(element) {
            const text = this.normalizeText(element.textContent);
            return KeywordManager.getWhitelist().some(k => text.includes(k));
        }
    }

    // ======================= 核心系统 =======================
    class CoreSystem {
        constructor() {
            this.initObservers();
            this.initialClean();
            this.setupContentScanner();
            this.injectProtectionStyles();
        }

        initObservers() {
            new MutationObserver(mutations => {
                mutations.forEach(m => {
                    m.addedNodes.forEach(n => {
                        if(n.nodeType === 1) this.processElement(n);
                    });
                });
            }).observe(document, {childList: true, subtree: true});
        }

        initialClean() {
            this.checkElements('*', el => this.processElement(el));
            this.checkIframes();
            this.checkThirdParty();
            this.scanForContent(document.documentElement);
        }

        processElement(el) {
            // 动态检测系统
            if(Config.get('dynamicSystem')) {
                this.checkDynamicId(el);
                this.checkAdAttributes(el);
            }

            // 布局检测系统
            if(Config.get('layoutSystem')) {
                this.checkZIndex(el);
                this.checkFixedPosition(el);
            }

            // 媒体检测系统
            if(Config.get('mediaSystem')) {
                this.checkImageAds(el);
                this.checkFloatingAds(el);
            }

            // 文本广告检测
            if(Config.get('textSystem')) {
                this.checkTextAds(el);
            }
        }

        setupContentScanner() {
            if (!Config.get('contentFilter')) return;
            
            new MutationObserver(mutations => {
                mutations.forEach(m => {
                    m.addedNodes.forEach(n => {
                        if (n.nodeType === 1) this.scanForContent(n);
                    });
                });
            }).observe(document, {
                childList: true,
                subtree: true
            });
        }

        scanForContent(element) {
            if (AdUtils.shouldBlockByContent(element)) {
                AdUtils.safeRemove(element, 'ContentFilter', {
                    type: '内容关键词匹配',
                    detail: '黑名单内容触发'
                });
            }

            Array.from(element.children).forEach(child => 
                this.scanForContent(child)
            );
        }

        checkDynamicId(el) {
            const id = el.id || '';
            if(id.length > CONFIG.protectionRules.dynamicIdLength || /\d{5}/.test(id)) {
                AdUtils.safeRemove(el, 'DynamicSystem', {
                    type: '动态ID检测',
                    detail: `异常ID: ${id.slice(0, 20)}`
                });
            }
        }

        checkAdAttributes(el) {
            ['id', 'class', 'src'].forEach(attr => {
                const val = el.getAttribute(attr) || '';
                if(CONFIG.adKeywords.some(k => val.includes(k))) {
                    AdUtils.safeRemove(el, 'DynamicSystem', {
                        type: '广告属性检测',
                        detail: `${attr}=${val.slice(0, 30)}`
                    });
                }
            });
        }

        checkZIndex(el) {
            const zIndex = parseInt(getComputedStyle(el).zIndex);
            if(zIndex > CONFIG.protectionRules.zIndexThreshold) {
                AdUtils.safeRemove(el, 'LayoutSystem', {
                    type: '高堆叠元素',
                    detail: `z-index=${zIndex}`
                });
            }
        }

        checkFixedPosition(el) {
            const style = getComputedStyle(el);
            if(style.position === 'fixed' && el.offsetWidth < 200) {
                AdUtils.safeRemove(el, 'LayoutSystem', {
                    type: '固定定位元素',
                    detail: `尺寸: ${el.offsetWidth}x${el.offsetHeight}`
                });
            }
        }

        checkImageAds(el) {
            if(el.tagName === 'IMG' && (el.src.includes('ad') || el.src.endsWith('.gif'))) {
                AdUtils.safeRemove(el, 'MediaSystem', {
                    type: '图片广告',
                    detail: `图片源: ${el.src.slice(0, 50)}`
                });
            }
        }

        checkFloatingAds(el) {
            const rect = el.getBoundingClientRect();
            const style = getComputedStyle(el);
            if(['fixed', 'sticky'].includes(style.position) && 
              (rect.top < 10 || rect.bottom > window.innerHeight - 10)) {
                AdUtils.safeRemove(el, 'MediaSystem', {
                    type: '浮动广告',
                    detail: `位置: ${rect.top}px`
                });
            }
        }

        checkTextAds(el) {
            const text = el.textContent?.toLowerCase() || '';
            if (CONFIG.protectionRules.textAdKeywords.some(k => text.includes(k))) {
                AdUtils.safeRemove(el, 'TextSystem', {
                    type: '文本广告',
                    detail: `关键词: ${text.slice(0, 50)}`
                });
            }
        }

        checkIframes() {
            if(!Config.get('frameSystem')) return;
            
            document.querySelectorAll('iframe').forEach(iframe => {
                let depth = 0, parent = iframe;
                while((parent = parent.parentNode)) {
                    if(parent.tagName === 'IFRAME') depth++;
                }
                if(depth > CONFIG.protectionRules.maxFrameDepth) {
                    AdUtils.safeRemove(iframe, 'FrameSystem', {
                        type: '深层嵌套框架',
                        detail: `嵌套层级: ${depth}`
                    });
                }

                const container = iframe.closest('div, section');
                if(container && !AdUtils.isWhitelisted(container)) {
                    AdUtils.safeRemove(container, 'FrameSystem', {
                        type: '广告容器',
                        detail: 'iframe父容器'
                    });
                }
            });
        }

        checkThirdParty() {
            if(!Config.get('thirdPartyBlock')) return;
            
            document.querySelectorAll('script, iframe').forEach(el => {
                try {
                    const src = new URL(el.src).hostname;
                    const current = new URL(location.href).hostname;
                    if(!src.endsWith(current)) {
                        AdUtils.safeRemove(el, 'ThirdParty', {
                            type: '第三方资源',
                            detail: `源域: ${src}`
                        });
                    }
                } catch {}
            });
        }

        injectProtectionStyles() {
            GM_addStyle(`
                [style*="fixed"], [style*="sticky"] { 
                    position: static !important 
                }
                iframe[src*="ad"] { 
                    display: none !important 
                }
                .ad-shield-protected {
                    border: 2px solid #4CAF50 !important;
                }
            `);
        }

        checkElements(selector, fn) {
            document.querySelectorAll(selector).forEach(fn);
        }
    }

    // ======================= 配置系统 =======================
    class Config {
        static get currentDomain() {
            return location.hostname.replace(/^www\./, '');
        }

        static get allKeys() {
            return Object.keys(CONFIG.defaultSettings);
        }

        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(
                Config.allKeys.map(k => [k, status])
            );
            GM_setValue('config', data);
        }
    }

    // ======================= 关键词管理 =======================
    class KeywordManager {
        static getStorageKey(type) {
            return `content_${type}_${Config.currentDomain}`;
        }

        static getBlacklist() {
            return this.getKeywords('blacklist');
        }

        static getWhitelist() {
            return this.getKeywords('whitelist');
        }

        static getKeywords(type) {
            const raw = GM_getValue(this.getStorageKey(type), '');
            return this.parseKeywords(raw);
        }

        static parseKeywords(raw) {
            return raw.split(',')
                .map(k => k.trim())
                .filter(k => k.length >= CONFIG.contentFilter.minLength)
                .slice(0, CONFIG.contentFilter.maxKeywords)
                .map(k => k.toLowerCase());
        }

        static updateKeywords(type, keywords) {
            const valid = [...new Set(keywords)]
                .map(k => k.trim())
                .filter(k => k.length >= CONFIG.contentFilter.minLength)
                .slice(0, CONFIG.contentFilter.maxKeywords);

            GM_setValue(
                this.getStorageKey(type),
                valid.join(',')
            );
        }
    }

    // ======================= 用户界面 =======================
    class UIController {
        static init() {
            this.registerMainMenu();
            this.registerModuleCommands();
            this.registerContentMenu();
            this.registerUtilityCommands();
        }

        static registerMainMenu() {
            const allEnabled = Config.allKeys.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', '第三方拦截'],
                ['contentFilter', '内容过滤系统']
            ];

            modules.forEach(([key, name]) => {
                GM_registerMenuCommand(
                    `${name} [${Config.get(key) ? '✅' : '❌'}]`,
                    () => this.toggleModule(key, name)
                );
            });
        }

        static registerContentMenu() {
            GM_registerMenuCommand('🔠 内容过滤管理', () => {
                GM_registerMenuCommand('➕ 添加黑名单关键词', () => 
                    this.handleAddKeyword('blacklist'));
                GM_registerMenuCommand('➕ 添加白名单关键词', () => 
                    this.handleAddKeyword('whitelist'));
                GM_registerMenuCommand('📋 显示当前关键词', () => 
                    this.showCurrentKeywords());
                GM_registerMenuCommand('🗑️ 清除所有关键词', () => 
                    this.clearKeywords());
            });
        }

        static registerUtilityCommands() {
            GM_registerMenuCommand('📜 查看拦截日志', () => this.showLogs());
            GM_registerMenuCommand('🧹 清除当前日志', () => Logger.clear());
            GM_registerMenuCommand('⚙️ 重置所有配置', () => this.resetConfig());
        }

        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 handleAddKeyword(type) {
            const promptText = type === 'blacklist' 
                ? '输入要屏蔽的关键词(支持中文):' 
                : '输入要豁免的关键词:';
            
            const input = prompt(promptText);
            if (!input) return;

            const current = KeywordManager.getKeywords(type);
            KeywordManager.updateKeywords(type, [...current, input]);
            this.showNotification(
                `已添加${type === 'blacklist' ? '黑' : '白'}名单关键词:${input}`
            );
        }

        static showCurrentKeywords() {
            const black = KeywordManager.getBlacklist();
            const white = KeywordManager.getWhitelist();
            
            alert(`【当前内容过滤规则 - ${location.hostname}】
            
■ 黑名单 (${black.length}个):
${black.join(', ') || '无'}

■ 白名单 (${white.length}个):
${white.join(', ') || '无'}`);
        }

        static clearKeywords() {
            if (!confirm('确定清除所有关键词吗?')) return;
            
            ['blacklist', 'whitelist'].forEach(type => {
                GM_setValue(KeywordManager.getStorageKey(type), '');
            });
            this.showNotification('已清除所有关键词');
        }

        static resetConfig() {
            if (!confirm('确定重置所有配置吗?')) return;
            
            const data = GM_getValue('config') || {};
            delete data[Config.currentDomain];
            GM_setValue('config', data);
            this.showNotification('配置已重置');
            setTimeout(() => location.reload(), 500);
        }

        static showLogs() {
            const logs = Logger.getLogs();
            alert(logs.length ? 
                `📃 最近${CONFIG.maxLogs}条拦截记录:\n\n${logs.map(l => 
                    `[${l.time}] ${l.module}\n类型: ${l.type}\n元素: ${l.element}`
                ).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({
                time: new Date().toLocaleTimeString(),
                module: data.module,
                type: data.reason.type,
                detail: data.reason.detail,
                element: `${data.element.tag}#${data.element.id}`
            });
            GM_setValue('logs', logs.slice(-CONFIG.maxLogs));
        }

        static getLogs() {
            return GM_getValue('logs', []);
        }

        static clear() {
            GM_setValue('logs', []);
            UIController.showNotification('日志已清空');
        }
    }

    // ======================= 初始化 =======================
    (function init() {
        new CoreSystem();
        UIController.init();
    })();
})();