您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Consumer-friendly ad & tracker removal using EasyList (cosmetic + network) + Peter Lowe hosts. Caches lists in localStorage (24h). Silent, standalone, preserves useful embeds (YouTube/Maps/Vimeo/etc.). Safe selectors only; avoids removing all scripts/iframes blindly.
// ==UserScript== // @name Smart Ad & Tracker Cleaner (EasyList + Peter Lowe) // @namespace https://example.com/tm/smart-ad-cleaner // @version 1.0.1 // @description Consumer-friendly ad & tracker removal using EasyList (cosmetic + network) + Peter Lowe hosts. Caches lists in localStorage (24h). Silent, standalone, preserves useful embeds (YouTube/Maps/Vimeo/etc.). Safe selectors only; avoids removing all scripts/iframes blindly. // @author Iamnobody // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @connect easylist.to // @connect pgl.yoyo.org // @run-at document-end // ==/UserScript== (function () { "use strict"; // -------- CONFIG -------- const EASYLIST_URL = "https://easylist.to/easylist/easylist.txt"; // cosmetic + network rules const PETER_LOWE_URL = "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext"; const CACHE_KEY = "sm_ac_cache_v1"; // stores { ts, easylistText, peterText, selectors[], domains[] } const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours const POLL_INTERVAL_MS = 3000; // periodically apply rules in first seconds const OBSERVE_DURATION_MS = 20000; // observe DOM for this many ms after load const MAX_SELECTOR_TRIES = 500; // limit to avoid heavy operations // Domains we MUST NOT remove (useful embeds) const ALLOW_DOMAINS = [ "youtube.com", "youtu.be", "google.com/maps", "maps.google.com", "googleusercontent.com", "gstatic.com", "vimeo.com", "player.vimeo.com", "openstreetmap.org", "docs.google.com", "drive.google.com", "twitter.com", "t.co", "facebook.com", "instagram.com", "maps.googleapis.com" ]; // Extra safe attribute selectors to remove if present (lightweight) const SAFE_ATTR_SELECTORS = [ "[data-ad]", "[data-ads]", "[data-ad-client]", "[data-ad-slot]", "[data-qa='ad']" ]; // -------- Storage helpers -------- function lsGet(key, fallback = null) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch (e) { return fallback; } } function lsSet(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* ignore quota errors silently */ } } // -------- Fetch lists (GM_xmlhttpRequest) -------- function fetchText(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "text", onload(resp) { if (resp.status >= 200 && resp.status < 300) resolve(resp.responseText); else reject(new Error("HTTP " + resp.status)); }, onerror(err) { reject(err); } }); }); } // -------- Parse rules -------- function parseEasyList(text) { // extract cosmetic selectors: lines starting with "##" or "#@#" const selectors = new Set(); const networkDomains = new Set(); const lines = text.split(/\r?\n/); for (const raw of lines) { const line = raw.trim(); if (!line || line.startsWith("!")) continue; // comment // cosmetic rule if (line.startsWith("##")) { const sel = line.slice(2).trim(); if (sel) selectors.add(sel); continue; } // cosmetic exception with #@# (rare) - skip if (line.startsWith("#@#")) { // could be exceptions, ignore for blocking continue; } // network rule like "||example.com^" or "||example.com^$third-party" // try to extract domain if (line.startsWith("||")) { const rest = line.slice(2); const match = rest.match(/^([^\^\/\:\$]+)/); if (match && match[1]) { networkDomains.add(match[1]); } continue; } // domain anchored rules like "example.com##.ad" - skip const anchorMatch = line.match(/^([^\#\/\:\s]+)\#\#/); if (anchorMatch && anchorMatch[1]) { networkDomains.add(anchorMatch[1]); } } return { selectors: Array.from(selectors), domains: Array.from(networkDomains) }; } function parsePeterLowe(text) { // hostfile lines: "0.0.0.0 domain.com" or "127.0.0.1 domain.com" const domains = new Set(); const lines = text.split(/\r?\n/); for (const raw of lines) { const line = raw.trim(); if (!line || line.startsWith("#")) continue; const parts = line.split(/\s+/); if (parts.length >= 2) { const d = parts[1].replace(/^\.*|\.*$/g, ""); if (d && d.indexOf(":") === -1) domains.add(d); } else { // fallback: if line looks like a domain only if (line.indexOf(".") > -1 && !line.includes(" ")) domains.add(line); } } return Array.from(domains); } // -------- Utility -------- function domainMatchInUrl(url, domain) { if (!url) return false; try { const u = url.toLowerCase(); return u.includes(domain.toLowerCase()); } catch (e) { return url.toLowerCase().indexOf(domain.toLowerCase()) !== -1; } } function isAllowedDomainInUrl(url) { if (!url) return false; for (const ad of ALLOW_DOMAINS) { if (url.indexOf(ad) !== -1) return true; } return false; } // -------- Apply rules safely -------- function applySelectors(selectors) { if (!selectors || selectors.length === 0) return; let tries = 0; for (const sel of selectors) { if (tries++ > MAX_SELECTOR_TRIES) break; try { // Some selectors from lists can be invalid in the page context; guard with try/catch const nodes = document.querySelectorAll(sel); if (!nodes || nodes.length === 0) continue; nodes.forEach(n => { // protect critical site areas: don't remove if it is a top-level embed from allowed domains let src = ""; if (n instanceof HTMLIFrameElement) src = n.src || ""; else if (n instanceof HTMLImageElement) src = n.src || ""; else src = (n.getAttribute && (n.getAttribute("src") || n.getAttribute("href"))) || ""; if (src && isAllowedDomainInUrl(src)) return; try { n.remove(); } catch (e) { /* ignore */ } }); } catch (e) { // ignore invalid selectors } } } function applyDomainBlocks(domains) { if (!domains || domains.length === 0) return; // Check scripts, images, iframes, link[href], source[src], audio/video, embed, object const tagAttrs = [ { tag: "script", attr: "src" }, { tag: "iframe", attr: "src" }, { tag: "img", attr: "src" }, { tag: "audio", attr: "src" }, { tag: "video", attr: "src" }, { tag: "source", attr: "src" }, { tag: "link", attr: "href" }, { tag: "embed", attr: "src" }, { tag: "object", attr: "data" }, { tag: "iframe", attr: "data-src" }, { tag: "*", attr: "data-ad-src" } // custom attribute common in some ad frameworks ]; // For performance, create a combined regex of domains (escaped), but cap length // We'll instead iterate over elements and check against domain list to avoid huge regex tagAttrs.forEach(({ tag, attr }) => { try { const nodes = tag === "*" ? document.querySelectorAll("[data-ad-src]") : document.getElementsByTagName(tag); if (!nodes || nodes.length === 0) return; Array.from(nodes).forEach(node => { try { const val = (node.getAttribute && node.getAttribute(attr)) || (node[attr] || ""); if (!val) return; const v = val.toLowerCase(); // allow if from allowed domains (preserve embeds) if (isAllowedDomainInUrl(v)) return; // Check any domain substring match for (const d of domains) { if (d.length < 3) continue; if (v.indexOf(d.toLowerCase()) !== -1) { // remove node (but avoid removing top-level <html> etc.) node.remove(); break; } } } catch (e) { /* ignore per node */ } }); } catch (e) { /* ignore per tag */ } }); // Additionally, remove elements that have obvious ad-related attributes (safe ones) SAFE_ATTR_SELECTORS.forEach(sel => { try { document.querySelectorAll(sel).forEach(n => n.remove()); } catch (e) { /* ignore invalid */ } }); } // -------- Core: fetch + cache + apply -------- async function prepareAndApply() { try { // Load cached let cache = lsGet(CACHE_KEY, null); const now = Date.now(); let needFetch = true; if (cache && cache.ts && (now - cache.ts) < CACHE_TTL_MS && cache.selectors && cache.domains) { needFetch = false; } if (needFetch) { // fetch both lists concurrently const [easyText, peterText] = await Promise.allSettled([fetchText(EASYLIST_URL), fetchText(PETER_LOWE_URL)]); let easy = ""; let peter = ""; if (easyText.status === "fulfilled") easy = easyText.value; if (peterText.status === "fulfilled") peter = peterText.value; const parsed = parseEasyList(easy); const peterDomains = parsePeterLowe(peter); // merge domain lists (unique) const domainsSet = new Set([...(parsed.domains || []), ...(peterDomains || [])]); cache = { ts: now, easyText: easy, peterText: peter, selectors: parsed.selectors || [], domains: Array.from(domainsSet) }; lsSet(CACHE_KEY, cache); } // apply rules now if (cache && cache.selectors) applySelectors(cache.selectors); if (cache && cache.domains) applyDomainBlocks(cache.domains); } catch (e) { // silent fail (per user choice) // console.error("Smart Ad Cleaner error:", e); } } // Run apply repeatedly for the first OBSERVE_DURATION_MS ms to catch late-load ads async function runCleaner() { await prepareAndApply(); const start = Date.now(); const interval = setInterval(async () => { await prepareAndApply(); if (Date.now() - start > OBSERVE_DURATION_MS) clearInterval(interval); }, POLL_INTERVAL_MS); // MutationObserver to catch dynamic insertions quickly const mo = new MutationObserver(muts => { // quick run, but keep it light — call prepareAndApply() which uses cached parsed selectors/domains prepareAndApply(); }); try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); // stop observer after OBSERVE_DURATION_MS setTimeout(() => mo.disconnect(), OBSERVE_DURATION_MS + 2000); } catch (e) { // ignore observe errors } } // Kick off (don't wait for load; run at document-end) runCleaner(); // Also schedule a daily background refresh of lists (non-blocking) (async function refreshCacheDaily() { const cache = lsGet(CACHE_KEY, null); const now = Date.now(); if (!cache || !cache.ts || (now - cache.ts) > CACHE_TTL_MS) { try { await prepareAndApply(); } catch (e) { /* silent */ } } })(); })();