Via Adblock 规则分析

解析Adblock规则,是否值得在Via浏览器上订阅,评分仅供娱乐,自行斟酌。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Via Adblock 规则分析
// @namespace    https://viayoo.com/
// @version      1.19
// @description  解析Adblock规则,是否值得在Via浏览器上订阅,评分仅供娱乐,自行斟酌。
// @author       Grok & Via
// @match        *://*/*
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    console.log('Adblock Rule Analyzer 脚本已加载,URL:', location.href);

    // 使用 GM_getValue 存储自动识别开关,默认关闭
    let autoDetectRawText = GM_getValue('autoDetectRawText', false);

    // 注册菜单项
    GM_registerMenuCommand("分析当前页面规则", analyzeCurrentPage);
    GM_registerMenuCommand("分析自定义链接规则", analyzeCustomLink);
    GM_registerMenuCommand(`自动识别纯文本链接解析 (${autoDetectRawText ? '开启' : '关闭'})`, toggleAutoDetect);

    // 简洁的 toast 调用函数
    const toast = msg => window.via?.toast?.(msg);

    // 检查是否是纯文本页面并直接处理
    function handleRawTextPage() {
        if (!autoDetectRawText) return false;
        const url = location.href;
        if (url.match(/\.(txt|list|rules|prop)$/i) || url.includes('raw.githubusercontent.com')) {
            console.log('检测到纯文本页面:', url);
            toast('正在分析Adblock规则中……')
            fetchContent(url);
            return true;
        }
        return false;
    }

    // 切换自动识别开关
    function toggleAutoDetect() {
        autoDetectRawText = !autoDetectRawText;
        GM_setValue('autoDetectRawText', autoDetectRawText);
        toast(`自动识别纯文本链接解析已${autoDetectRawText ? '开启' : '关闭'},刷新页面后生效`);
        // 更新菜单显示
        GM_registerMenuCommand(`自动识别纯文本链接解析 (${autoDetectRawText ? '开启' : '关闭'})`, toggleAutoDetect);
    }

    // 在脚本启动时检查是否需要自动处理
    if (handleRawTextPage()) {
        return;
    }

    // 通用 fetch 函数
    async function fetchContent(url) {
        try {
            const response = await fetch(url, {
                method: 'GET',
                credentials: 'omit',
                cache: 'no-store'
            });
            if (!response.ok) {
                throw new Error(`网络请求失败,状态码: ${response.status} (${response.statusText})`);
            }
            const contentType = response.headers.get('Content-Type') || '';
            if (!contentType.includes('text/')) {
                throw new Error('非文本内容,无法解析 (Content-Type: ' + contentType + ')');
            }
            const content = await response.text();
            console.log('内容获取成功,长度:', content.length);
            analyzeContent(content, url);
        } catch (e) {
            console.error('内容获取失败:', e);
            let errorMsg = '无法获取内容:';
            if (e.message.includes('Failed to fetch')) {
                errorMsg += '网络请求失败,可能是链接不可访问或被浏览器阻止(检查 CORS 或网络连接)。';
            } else {
                errorMsg += e.message;
            }
            errorMsg += '\n请确保链接有效且指向 Adblock 规则文件。';
            alert(errorMsg);
        }
    }

    async function analyzeCurrentPage() {
        toast('分析当前页面');
        fetchContent(location.href);
    }

    function analyzeCustomLink() {
        console.log('分析自定义链接');
        const url = prompt('请输入Adblock规则文件的直链(如 https://raw.githubusercontent.com/...)');
        if (!url || !url.trim()) {
            alert('未输入有效的链接');
            return;
        }
        if (!url.match(/^https?:\/\/.+/)) {
            alert('链接格式无效,请输入以 http:// 或 https:// 开头的完整 URL');
            return;
        }
        toast(`解析链接中……`);
        fetchContent(url);
    }

    function normalizeNewlines(text) {
        return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
    }

    function parseHeader(content) {
        const header = {
            title: '未知标题',
            description: '未添加任何描述',
            version: '未知版本',
            lastModified: '未知时间',
            expires: '未给出更新周期',
        };
        const headerLines = content.split('\n')
            .filter(line => line.trim().startsWith('!'))
            .map(line => line.trim().substring(1).trim());

        headerLines.forEach(line => {
            if (line.startsWith('Title:')) header.title = line.substring(6).trim();
            else if (line.startsWith('Description:')) header.description = line.substring(12).trim();
            else if (line.startsWith('Version:')) header.version = line.substring(8).trim();
            else if (line.startsWith('TimeUpdated:') || line.startsWith('Last modified:') || line.startsWith('Update Time:')) {
                header.lastModified = line.split(':').slice(1).join(':').trim();
            } else if (line.startsWith('Expires:')) header.expires = line.substring(8).trim();
        });
        return header;
    }

    function analyzeContent(content, source) {
        if (!content.startsWith('[Adblock') && !content.startsWith('![Adblock')) {
            toast(`这不是一个标准的Adblock规则文件(未找到[Adblock开头),来源: ${source}`);
            console.log('非Adblock文件,来源:', source);
            return;
        }
        content = normalizeNewlines(content);
        const header = parseHeader(content);
        const lines = content.split('\n')
            .filter(line => line.trim() && !line.trim().startsWith('!') && !line.trim().startsWith('['));

        const stats = {
            cssRules: {
                normal: 0,
                exception: 0,
                hasNotPseudo: 0,
                hasSpecialPseudo: 0,
                hasSpecialPseudoNotAfter: 0
            },
            domainRules: {
                count: 0,
                duplicateRules: 0
            },
            unsupported: 0,
            extendedRules: {
                scriptInject: 0,
                adguardScript: 0,
                htmlFilter: 0,
                cssInject: 0,
                other: 0
            }
        };

        const extendedPatterns = {
            scriptInject: /(##|@#+)\+js\(/,
            adguardScript: /#@?%#/,
            htmlFilter: /\$\$/,
            cssInject: /#@?\$#/,
            specialPseudo: /:matches-property\b|:style\b|:-abp-properties\b|:-abp-contains\b|:min-text-length\b|:matches-path\b|:contains\b|:has-text\b|:matches-css\b|:matches-css-before\b|:matches-css-after\b|:if\b|:if-not\b|:xpath\b|:nth-ancestor\b|:upward\b|:remove\b/,
            other: /\$(\s*)(redirect|rewrite|csp|removeparam|badfilter|empty|generichide|match-case|object|object-subrequest|important|popup|document)|,(\s*)(redirect=|app=|replace=|csp=|denyallow=|permissions=)|:matches-property\b|:style\b|:-abp-properties\b|:-abp-contains\b|:min-text-length\b|:matches-path\b|:contains\b|:has-text\b|:matches-css\b|:matches-css-before\b|:matches-css-after\b|:if\b|:if-not\b|:xpath\b|:nth-ancestor\b|:upward\b|:remove\b|redirect-rule/
        };

        const rulePatternMap = new Map();

        lines.forEach(line => {
            const trimmed = line.trim();

            if (extendedPatterns.scriptInject.test(trimmed)) {
                stats.extendedRules.scriptInject++;
                stats.unsupported++;
            } else if (extendedPatterns.adguardScript.test(trimmed)) {
                stats.extendedRules.adguardScript++;
                stats.unsupported++;
            } else if (extendedPatterns.htmlFilter.test(trimmed)) {
                stats.extendedRules.htmlFilter++;
                stats.unsupported++;
            } else if (extendedPatterns.cssInject.test(trimmed)) {
                stats.extendedRules.cssInject++;
                stats.unsupported++;
            } else if (extendedPatterns.other.test(trimmed)) {
                stats.extendedRules.other++;
                stats.unsupported++;
            } else if (trimmed.startsWith('##') || trimmed.startsWith('###')) {
                stats.cssRules.normal++;
                if (/:has|:not/.test(trimmed)) stats.cssRules.hasNotPseudo++;
                if (extendedPatterns.specialPseudo.test(trimmed)) stats.cssRules.hasSpecialPseudo++;
            } else if (trimmed.startsWith('#@#') || trimmed.startsWith('#@##')) {
                stats.cssRules.exception++;
                if (/:has|:not/.test(trimmed)) stats.cssRules.hasNotPseudo++;
                if (extendedPatterns.specialPseudo.test(trimmed)) stats.cssRules.hasSpecialPseudo++;
            } else if (trimmed.startsWith('||')) {
                stats.domainRules.count++;
                let rulePattern = trimmed;
                let domains = [];
                const domainMatch = trimmed.match(/[,|$]domain=([^$|,]+)/);
                if (domainMatch) {
                    rulePattern = trimmed.replace(/[,|$]domain=[^$|,]+/, '').replace(/[,|$].*$/, '');
                    domains = domainMatch[1].split('|');
                }
                if (rulePatternMap.has(rulePattern)) {
                    const ruleData = rulePatternMap.get(rulePattern);
                    ruleData.count++;
                    stats.domainRules.duplicateRules++;
                    domains.forEach(domain => ruleData.domains.add(domain));
                } else {
                    rulePatternMap.set(rulePattern, {
                        domains: new Set(domains),
                        count: 1
                    });
                }
            }

            // 检测不在合法位置的特殊伪类
            if (extendedPatterns.specialPseudo.test(trimmed)) {
                if (!trimmed.match(/^(##|###|#@#|#@##|#?#|\$\$)/)) {
                    stats.cssRules.hasSpecialPseudoNotAfter++;
                }
            }
        });

        const totalCssRules = stats.cssRules.normal + stats.cssRules.exception;
        const totalExtendedRules = stats.extendedRules.scriptInject + stats.extendedRules.adguardScript +
            stats.extendedRules.htmlFilter + stats.extendedRules.cssInject + stats.extendedRules.other;

        let score = 0;
        let cssCountScore = Math.max(0, totalCssRules <= 5000 ? 35 : totalCssRules <= 7000 ? 35 - ((totalCssRules - 5000) / 2000) * 10 : totalCssRules <= 9999 ? 25 - ((totalCssRules - 7000) / 2999) * 15 : 10 - ((totalCssRules - 9999) / 5000) * 10);
        score += cssCountScore;

        let cssPseudoScore = stats.cssRules.hasNotPseudo <= 30 ? 15 : stats.cssRules.hasNotPseudo <= 100 ? 10 : stats.cssRules.hasNotPseudo <= 120 ? 5 : 0;
        score += cssPseudoScore;

        let domainCountScore = Math.max(0, stats.domainRules.count <= 100000 ? 30 : stats.domainRules.count <= 200000 ? 30 - ((stats.domainRules.count - 100000) / 100000) * 10 : stats.domainRules.count <= 500000 ? 20 - ((stats.domainRules.count - 200000) / 300000) * 15 : 5 - ((stats.domainRules.count - 500000) / 500000) * 5);
        score += domainCountScore;

        let domainDuplicateScore = Math.max(0, stats.domainRules.duplicateRules <= 100 ? 10 : stats.domainRules.duplicateRules <= 300 ? 10 - ((stats.domainRules.duplicateRules - 50) / 150) * 5 : 5 - ((stats.domainRules.duplicateRules - 200) / 200) * 5);
        score += domainDuplicateScore;

        let extendedScore = totalExtendedRules === 0 ? 10 : totalExtendedRules <= 100 ? 10 - (totalExtendedRules / 100) * 5 : totalExtendedRules <= 300 ? 5 - ((totalExtendedRules - 100) / 200) * 5 : Math.max(-10, 0 - ((totalExtendedRules - 300) / 300) * 10);
        score += extendedScore;

        let specialPseudoPenalty = stats.cssRules.hasSpecialPseudo > 0 ? -40 : 0;
        score += specialPseudoPenalty;

        let specialPseudoNotAfterPenalty = stats.cssRules.hasSpecialPseudoNotAfter > 0 ? -10 : 0;
        score += specialPseudoNotAfterPenalty;

        score = Math.max(1, Math.min(100, Math.round(score)));

        const cssPerformance = totalCssRules <= 5000 ? '✅CSS规则数量正常,可以流畅运行' : totalCssRules <= 7000 ? '❓CSS规则数量较多,可能会导致设备运行缓慢' : totalCssRules < 9999 ? '⚠️CSS规则数量接近上限,可能明显影响设备性能' : '🆘CSS规则数量过多,不建议订阅此规则';
        const domainPerformance = stats.domainRules.count <= 100000 ? '✅域名规则数量正常,可以流畅运行' : stats.domainRules.count <= 200000 ? '❓域名规则数量较多,但仍在可接受范围内' : stats.domainRules.count <= 500000 ? '🆘域名规则数量过多,可能会导致内存溢出 (OOM)' : '‼️域名规则数量极多,强烈不建议使用,可能严重影响性能';

        const report = `
Adblock规则分析结果(来源: ${source}):
📜Adblock规则信息:
  标题: ${header.title}
  描述: ${header.description}
  版本: ${header.version}
  最后更新: ${header.lastModified}
  更新周期: ${header.expires}
---------------------
💯规则评级: ${score}/100
(评分仅供参考,具体以Via变动为主)
📊各部分得分:
  CSS数量得分: ${Math.round(cssCountScore)}/35
  CSS伪类得分: ${cssPseudoScore}/15
  域名数量得分: ${Math.round(domainCountScore)}/30
  重复规则得分: ${Math.round(domainDuplicateScore)}/10
  扩展规则加减分: ${Math.round(extendedScore)} (±10)
  特殊伪类惩罚: ${specialPseudoPenalty} (Adguard/uBlock特殊伪类)
  特殊伪类不按语法: ${specialPseudoNotAfterPenalty} (未使用正确语法)
---------------------
🛠️总规则数: ${lines.length}
👋不支持的规则: ${stats.unsupported}
📋CSS通用隐藏规则:
  常规规则 (##, ###): ${stats.cssRules.normal}
  例外规则 (#@#, #@##): ${stats.cssRules.exception}
  含:has/:not伪类规则: ${stats.cssRules.hasNotPseudo}
  含Adguard/uBlock特殊伪类: ${stats.cssRules.hasSpecialPseudo}
  特殊伪类未使用正确语法: ${stats.cssRules.hasSpecialPseudoNotAfter}
  总CSS规则数: ${totalCssRules}
  性能评估: ${cssPerformance}
🔗域名规则 (||):
  总数: ${stats.domainRules.count}
  重复规则数: ${stats.domainRules.duplicateRules}
  性能评估: ${domainPerformance}
✋🏼uBlock/AdGuard 独有规则:
  脚本注入 (##+js): ${stats.extendedRules.scriptInject}
  AdGuard脚本 (#%#): ${stats.extendedRules.adguardScript}
  HTML过滤 ($$): ${stats.extendedRules.htmlFilter}
  CSS注入 (#$#): ${stats.extendedRules.cssInject}
  其他扩展规则 ($redirect等): ${stats.extendedRules.other}
  总计: ${totalExtendedRules}
注:uBlock/AdGuard 独有规则及特殊伪类在传统 Adblock Plus 中不受支持
    `;
        alert(report);
        console.log(report);
    }
})();