144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
# -*- 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()
|