Greasy Fork

Greasy Fork is available in English.

广告终结者

广告终结者(移除相关功能)

当前为 2025-02-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         广告终结者
// @namespace    http://tampermonkey.net/
// @version      2.8
// @description  广告终结者(移除相关功能)
// @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_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // ======================= 小说网站UA伪装 =======================
    (function detectNovelSite() {
        const novelKeywords = [
            'novel', 'xiaoshuo', '小说', '阅读', 
            'book', '章节', '文学', '小说网',
            'txt', 'download', '免费小说'
        ];
        
        const isNovelSite = () => {
            const urlCheck = novelKeywords.some(k => 
                window.location.href.toLowerCase().includes(k)
            );
            const titleCheck = novelKeywords.some(k => 
                document.title.toLowerCase().includes(k)
            );
            const metaKeywords = document.querySelector('meta[name="keywords"]')?.content || '';
            const metaDescription = document.querySelector('meta[name="description"]')?.content || '';
            const contentCheck = novelKeywords.some(k => 
                metaKeywords.includes(k) || metaDescription.includes(k)
            );
            return urlCheck || titleCheck || contentCheck;
        };

        if (isNovelSite()) {
            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';
            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', '偷拍', '黑料']
        },
        defaultSettings: {
            dynamicSystem: true,
            layoutSystem: true,
            frameSystem: true,
            mediaSystem: true,
            textSystem: true,
            thirdPartyBlock: 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 handleScriptContent(script) {
            const content = script.innerHTML.toLowerCase();
            const foundKeywords = CONFIG.adKeywords.filter(k => 
                content.includes(k.toLowerCase().trim())
            );

            if (foundKeywords.length > 0) {
                Logger.logRemoval({
                    module: 'ScriptFilter',
                    element: {
                        tag: script.tagName,
                        id: script.id,
                        class: script.className,
                        html: script.outerHTML.slice(0, 200)
                    },
                    reason: {
                        type: '脚本关键词拦截',
                        detail: `匹配关键词: ${foundKeywords.join(', ')}`
                    }
                });
                script.remove();
                return true;
            }
            return false;
        }

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

    // ======================= 核心拦截系统 =======================
    class CoreSystem {
        constructor() {
            this.observerConfig = {
                childList: true, 
                subtree: true,
                attributeFilter: ['id', 'class', 'style', 'src']
            };
            this.processedElements = new WeakSet();
            this.initObservers();
            this.initialClean();
            this.injectProtectionStyles();
        }

        debounce(func, wait) {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        }

        initObservers() {
            const processMutations = this.debounce(this.handleMutations.bind(this), 100);
            new MutationObserver(mutations => processMutations(mutations))
                .observe(document, this.observerConfig);
        }

        handleMutations(mutations) {
            const elements = new Set();
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1 && !this.processedElements.has(node)) {
                        elements.add(node);
                        this.processedElements.add(node);
                    }
                }
            }
            this.batchProcessElements([...elements]);
        }

        batchProcessElements(elements) {
            if (!elements.length) return;
            
            const BATCH_SIZE = 25;
            for (let i = 0; i < elements.length; i += BATCH_SIZE) {
                const batch = elements.slice(i, i + BATCH_SIZE);
                requestIdleCallback(() => {
                    batch.forEach(el => this.processElement(el));
                }, { timeout: 500 });
            }
        }

        initialClean() {
            this.checkPriorityElements(['script', 'iframe', 'img', 'div']);
            requestIdleCallback(() => this.checkElements('*'), { timeout: 1000 });
        }

        checkPriorityElements(selectors) {
            selectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                this.batchProcessElements([...elements]);
            });
        }

        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);
            }
        }

        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;
                    top: auto !important;
                    bottom: auto !important;
                }
                iframe[src*="ad"], .ad-container { 
                    display: none !important;
                    height: 0 !important;
                    width: 0 !important;
                    opacity: 0 !important;
                }
                .ad-shield-protected {
                    border: 2px solid #4CAF50 !important;
                    padding: 5px !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 UIController {
        static init() {
            this.registerMainMenu();
            this.registerModuleCommands();
            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', '第三方拦截']
            ];

            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.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 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();
        console.log('✅ 广告拦截系统已激活 - 当前域名:', location.hostname);
    })();
})();