Via Adblock 规则分析
// ==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);
}
})();