# -*- coding: utf-8 -*- """更新 pac_domains.json:从 v2fly/domain-list-community (geosite) 拉取境外服务 分类,与现有手工条目合并,生成新的强制走隧道域名后缀表。 用法: python update_pac_domains.py # 拉取并更新(先自动备份) python update_pac_domains.py --dry-run # 只统计,不写文件 geosite 数据格式(data/<分类>,每行一条规则): include:xxx 引用其他分类(递归解析) example.com 匹配域名及其子域(与我们的后缀匹配一致) +example.com 同上(additive) .example.com 仅精确匹配(不匹配子域)-> 跳过 -example.com 排除 -> 跳过 /regex/ 正则 -> 跳过 example.com @cn 属性标签 -> 去掉标签 无点的单标签(如 chrome)-> 无实际主机名会命中,跳过 """ import argparse import json import sys import urllib.request from datetime import datetime from pathlib import Path BASE_URL = "https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/" # 需要强制走隧道的境外服务分类(geosite 分类名),可按需增删 CATEGORIES = ["google", "apple", "telegram", "openai", "twitter", "facebook", "github", "netflix", "discord", "spotify", "whatsapp", "paypal"] SCRIPT_DIR = Path(__file__).resolve().parent LITE_DIR = SCRIPT_DIR.parent.parent PAC_FILE = LITE_DIR / "pac_domains.json" BACKUP_DIR = LITE_DIR / "backups" # 本地 pac_domains.json 已不再随项目保留:缺失时从 CDN 拉当前版本作合并基底 CDN_URL = "https://cdn.rrrosmontis.icu/RRPP/PAC/pac_domains.json" def fetch(cat, tries=2): for i in range(tries): try: with urllib.request.urlopen(BASE_URL + cat, timeout=20) as r: return r.read().decode("utf-8", "replace").splitlines() except Exception: if i == tries - 1: raise def resolve(cat, seen=None): """递归展开 include:,返回原始行列表;循环引用用 seen 防死循环。""" seen = seen or set() if cat in seen: return [] seen.add(cat) lines = [] try: for raw in fetch(cat): line = raw.strip() if line.startswith("include:"): lines += resolve(line[len("include:"):].strip(), seen) else: lines.append(line) except Exception as e: print(f" [warn] 拉取 {cat} 失败: {e}") return lines def parse_domains(lines): """过滤 geosite 行,只保留可映射为后缀匹配的纯域名。""" out = set() for line in lines: s = line.strip() if not s or s.startswith("#") or s.startswith("/"): continue if s.startswith("-") or s.startswith("."): continue s = s.lstrip("+").split("@")[0].strip() if "." not in s: continue out.add(s.lower()) return out def main(): parser = argparse.ArgumentParser(description="更新 pac_domains.json") parser.add_argument("--dry-run", action="store_true", help="只统计,不写文件") args = parser.parse_args() if PAC_FILE.is_file(): cur = set(json.loads(PAC_FILE.read_text("utf-8"))["domains"]) else: # 项目目录已不保留该文件:从 CDN 拉当前版本作合并基底(保留其中的手工条目) print(f"本地未找到 {PAC_FILE},从 CDN 拉取当前版本作为合并基底") try: with urllib.request.urlopen(CDN_URL, timeout=20) as r: cur = set(json.loads(r.read().decode("utf-8"))["domains"]) except Exception as e: print(f"拉取失败: {e}") sys.exit(1) print(f"拉取 geosite 分类: {', '.join(CATEGORIES)}") merged = set() for cat in CATEGORIES: doms = parse_domains(resolve(cat)) merged |= doms print(f" {cat:<12} -> {len(doms):>5} 条") new = merged - cur kept = cur & merged only_cur = cur - merged print(f"\n合并后总数: {len(merged | cur)}(geosite {len(merged)} + 现有 {len(cur)})") print(f"新增 {len(new)},与现有重叠 {len(kept)},现有手工条目 {len(only_cur)} 条全部保留") if args.dry_run: print("--dry-run: 未写文件") return final = sorted(merged | cur) now = datetime.now().strftime("%Y-%m-%d %H:%M") comment = ( f"强制走代理隧道的域名后缀(IP 智能分流的补充规则):命中这些域名的连接即使" f"解析到国内 IP 也强制走隧道。匹配规则:host == 条目 或 host 以 \".条目\" 结尾。" f"由 others/update_pac_domains/update_pac_domains.py 从 v2fly geosite " f"({', '.join(CATEGORIES)}) 生成于 {now},手工条目已保留;" f"客户端按文件内 version/date 从 CDN 在线更新到 %LOCALAPPDATA%\\RRPP。" ) out = {"_comment": comment, "version": datetime.now().strftime("%Y%m%d%H%M"), "date": datetime.now().strftime("%Y-%m-%d"), "domains": final} BACKUP_DIR.mkdir(exist_ok=True) bak = BACKUP_DIR / f"pac_domains.json.bak_{datetime.now().strftime('%Y%m%d_%H%M%S')}" bak.write_bytes(PAC_FILE.read_bytes()) print(f"已备份: {bak}") PAC_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n", "utf-8") size = PAC_FILE.stat().st_size / 1024 print(f"已写入: {PAC_FILE}({len(final)} 条,{size:.0f} KB)") if __name__ == "__main__": main()