Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- coding: utf-8 -*-
- """微信输入法(WeType) 敏感词库导出工具 —— macOS 通用版
- 用法:
- python3 export_sensitive_mac.py # 自动扫描本机所有 WeType 相关目录
- python3 export_sensitive_mac.py -o out.txt
- python3 export_sensitive_mac.py -f a.zh -f b.zh # 只处理指定文件
- python3 export_sensitive_mac.py -d 某目录 # 额外扫描某目录(可多次)
- python3 export_sensitive_mac.py --all-443 # 另存每个文件 443 trie 的全量词表
- 不依赖任何固定用户名:家目录用 Path.home(),输出里的路径以 ~ 显示。
- 默认输出到当前目录 ./wetype_sensitive_words.txt,按来源文件分块:
- # source1: /Library/Input Methods/WeType.app/.../cell_dict.ssrc.zh
- # count1 : 13362
- 词
- ...
- 扫描范围(只看名字含 wetype / com.tencent.inputmethod 的目录):
- /Library/Input Methods, ~/Library/Input Methods,
- /Library/Application Support, ~/Library/Application Support,
- ~/Library/Containers, ~/Library/Group Containers, ~/Library/Caches
- 凡含 "Ime trie header" 的文件都会尝试解析;能找到通过结构校验的 kind-440
- 屏蔽表(或文件名含 ssrc)的才算敏感词库,其余列在文件头的 skipped 里。
- 格式说明(沿用 Windows 版对 libwxhld 的逆向结论):
- [443 反向 trie][payload 子块链][32B meta]
- 443 SensitiveTrieDict : 反向 trie,提供可枚举明文
- 440 StaticInterceptSensitiveDict: HashMapReader,只存 32 位 check
- 导出结果 = 443 中 check 命中 440 的词。440 里没有明文对应的条目
- (例如在线增量更新只下发哈希)无法还原,只在块头列出 check 值。
- """
- import argparse
- import mmap
- import os
- import struct
- import sys
- import time
- from pathlib import Path
- HOME = Path.home()
- TRIE_MAGIC = b"Ime trie header\x00"
- NAME_KEYS = ("wetype", "com.tencent.inputmethod")
- SCAN_PARENTS = [
- Path("/Library/Input Methods"),
- HOME / "Library/Input Methods",
- Path("/Library/Application Support"),
- HOME / "Library/Application Support",
- HOME / "Library/Containers",
- HOME / "Library/Group Containers",
- HOME / "Library/Caches",
- ]
- MAX_FILE = 512 * 1024 * 1024
- M32 = 0xFFFFFFFF
- M64 = 0xFFFFFFFFFFFFFFFF
- K0 = 0xC3A5C85C97CB3127
- K1 = 0xB492B66FBE98F273
- K2 = 0x9AE16A3B2F90404F
- KMUL = 0x9DDFEA08EB382D69
- CHECK_SEED = 0x1DBBA5
- def pretty(p):
- """家目录替换成 ~,便于分享"""
- p = str(p)
- h = str(HOME)
- return "~" + p[len(h):] if p == h or p.startswith(h + os.sep) else p
- # ============================================================================
- # 一、443 反向 trie(SensitiveTrieDict)
- # ============================================================================
- class Bits:
- __slots__ = ("buf", "nbits", "pre")
- def __init__(self, buf, off, nbits):
- self.buf = memoryview(buf)[off:]
- self.nbits = nbits
- nw = (nbits + 63) // 64
- pre = [0] * (nw + 1)
- for i in range(nw):
- w = struct.unpack_from("<Q", self.buf, i * 8)[0]
- pre[i + 1] = pre[i] + w.bit_count()
- self.pre = pre
- def get(self, pos):
- if pos < 0 or pos >= self.nbits:
- return 0
- return (self.buf[pos >> 3] >> (pos & 7)) & 1
- def rank1(self, pos):
- if pos <= 0:
- return 0
- if pos > self.nbits:
- pos = self.nbits
- w = pos >> 6
- cnt = self.pre[w]
- rem = pos & 63
- if rem:
- word = struct.unpack_from("<Q", self.buf, w * 8)[0]
- cnt += (word & ((1 << rem) - 1)).bit_count()
- return cnt
- def select0(self, count):
- if count <= 0:
- return 0
- lo, hi = 0, self.nbits
- while lo < hi:
- mid = (lo + hi) // 2
- if mid - self.rank1(mid) < count:
- lo = mid + 1
- else:
- hi = mid
- return lo
- class Trie:
- __slots__ = ("louds", "term", "aux", "labels", "E", "width", "mask",
- "F_off", "F_len", "u1072", "nested")
- def select1_0based(bv, k):
- lo, hi = 0, bv.nbits
- kk = k + 1
- while lo < hi:
- mid = (lo + hi) // 2
- if bv.rank1(mid + 1) < kk:
- lo = mid + 1
- else:
- hi = mid
- return lo
- def load_trie(data):
- """解析 ssrc 容器内的嵌套 trie,返回最外层 t0(t0.nested 为下一层)。
- 主词库是 3 层;在线增量更新可能只有 1 层空 trie。
- """
- pos = data.find(TRIE_MAGIC)
- if pos < 0:
- raise ValueError("未找到 'Ime trie header'")
- p = pos + 16
- raw = []
- def u32():
- nonlocal p
- v = struct.unpack_from("<I", data, p)[0]
- p += 4
- return v
- def u64():
- nonlocal p
- v = struct.unpack_from("<Q", data, p)[0]
- p += 8
- return v
- def rd_bv():
- nonlocal p
- sz = u64()
- off = p
- p += 8 * (sz >> 3) + ((-sz) & 7)
- nbits = u32()
- ones = u32()
- it = u64()
- p += 12 * (it // 12) + ((-it) & 7)
- a = u64()
- p += 4 * (a >> 2) + ((-a) & 7)
- b = u64()
- p += 4 * (b >> 2) + ((-b) & 7)
- return dict(off=off, nbits=nbits, ones=ones)
- def rd_blob():
- nonlocal p
- n = u64()
- off = p
- p += n + ((-n) & 7)
- return off, n
- def rd_trie(depth):
- nonlocal p
- t = {}
- t["louds"] = rd_bv()
- t["term"] = rd_bv()
- t["aux"] = rd_bv()
- t["labels_off"], t["labels_len"] = rd_blob()
- t["E_off"], t["E_len"] = rd_blob()
- t["width"] = u32()
- t["mask"] = u32()
- u64() # ext_cnt
- t["F_off"], t["F_len"] = rd_blob()
- rd_bv() # sec3
- t["nested"] = (rd_trie(depth + 1)
- if (t["aux"]["ones"] and t["F_len"] == 0 and depth < 4) else None)
- S = u64()
- p += 12 * (S // 12) + ((-S) & 7)
- t["u1072"] = u32()
- u32() # pool_id
- if p > len(data):
- raise ValueError("trie 越界")
- raw.append(t)
- return t
- rd_trie(0)
- built = []
- for t in raw: # 追加顺序: 最内层 -> 最外层
- tr = Trie()
- tr.louds = Bits(data, t["louds"]["off"], t["louds"]["nbits"])
- tr.term = Bits(data, t["term"]["off"], t["term"]["nbits"]) if t["term"]["nbits"] else None
- tr.aux = Bits(data, t["aux"]["off"], t["aux"]["nbits"])
- tr.labels = data[t["labels_off"]:t["labels_off"] + t["labels_len"]]
- tr.E = Bits(data, t["E_off"], t["E_len"] * 8)
- tr.width = t["width"]
- tr.mask = t["mask"]
- tr.F_off = t["F_off"]
- tr.F_len = t["F_len"]
- tr.u1072 = t["u1072"]
- tr.nested = built[-1] if t["nested"] else None
- built.append(tr)
- return built[-1], len(built)
- def handle16(tr, state):
- r = tr.aux.rank1(state)
- v = 0
- for k in range(tr.width):
- v |= tr.E.get(r * tr.width + k) << k
- return ((v & tr.mask) << 8) | tr.labels[state]
- def children_states(tr, state):
- fc = tr.louds.select0(state + 1)
- out = []
- while fc < tr.louds.nbits and tr.louds.get(fc):
- out.append(tr.louds.rank1(fc + 1) - 1)
- fc += 1
- return out
- def parent_state(tr, state):
- return select1_0based(tr.louds, state) - state - 1
- def expand_chain(tr, h, out, data, depth=0):
- if depth > 64:
- return
- while True:
- if tr.aux.get(h):
- h16 = handle16(tr, h)
- if tr.nested is not None:
- expand_chain(tr.nested, h16, out, data, depth + 1)
- else:
- seg = h16
- while seg < tr.F_len and data[tr.F_off + seg] != 0:
- out.append(data[tr.F_off + seg])
- seg += 1
- else:
- out.append(tr.labels[h])
- if h <= tr.u1072:
- return
- h = parent_state(tr, h)
- if h < 0:
- return
- def iter_words(t0, data):
- t1 = t0.nested
- def step(state, path, depth):
- if depth > 200:
- return
- for c in children_states(t0, state):
- if c >= len(t0.labels):
- continue
- if t0.aux.get(c) and t1 is not None:
- out = list(path)
- expand_chain(t1, handle16(t0, c), out, data)
- else:
- out = path + [t0.labels[c]]
- if t0.term is not None and t0.term.get(c):
- yield bytes(out)
- yield from step(c, out, depth + 1)
- yield from step(0, [], 0)
- def decode_key(path):
- """key 域 -> 原词: 每字节 -1 -> UTF-8 -> 字符串反转"""
- return bytes((x - 1) & 0xFF for x in path).decode("utf-8")[::-1]
- # ============================================================================
- # 二、哈希族(CityHash64 / DefaultKeyCheckHash / DefaultKeyHash 5-12)
- # ============================================================================
- def rot(v, n):
- v &= M64
- return ((v >> n) | (v << (64 - n))) & M64
- def ror32(v, n):
- v &= M32
- return ((v >> n) | (v << (32 - n))) & M32
- def f32(b, i):
- return struct.unpack_from("<I", b, i)[0]
- def f64(b, i):
- return struct.unpack_from("<Q", b, i)[0]
- def bswap64(v):
- return int.from_bytes((v & M64).to_bytes(8, "little"), "big")
- def shift_mix(v):
- return (v ^ (v >> 47)) & M64
- def hash_len16(u, v, mul):
- a = ((u ^ v) * mul) & M64
- a ^= a >> 47
- b = ((v ^ a) * mul) & M64
- b ^= b >> 47
- return (b * mul) & M64
- def city_0_16(d):
- n = len(d)
- if n >= 8:
- mul = (K2 + 2 * n) & M64
- a = (f64(d, 0) + K2) & M64
- b = f64(d, n - 8)
- c = (rot(b, 37) * mul + a) & M64
- e = ((rot(a, 25) + b) * mul) & M64
- return hash_len16(c, e, mul)
- if n >= 4:
- mul = (K2 + 2 * n) & M64
- a = f32(d, 0)
- return hash_len16((n + (a << 3)) & M64, f32(d, n - 4), mul)
- if n > 0:
- a, b, c = d[0], d[n >> 1], d[n - 1]
- y = (a + (b << 8)) & M32
- z = (n + (c << 2)) & M32
- return (shift_mix(((y * K2) & M64) ^ ((z * K0) & M64)) * K2) & M64
- return K2
- def city_17_32(d):
- n = len(d)
- mul = (K2 + 2 * n) & M64
- a = (f64(d, 0) * K1) & M64
- b = f64(d, 8)
- c = (f64(d, n - 8) * mul) & M64
- dd = (f64(d, n - 16) * K2) & M64
- return hash_len16((rot((a + b) & M64, 43) + rot(c, 30) + dd) & M64,
- (a + rot((b + K2) & M64, 18) + c) & M64, mul)
- def city_33_64(d):
- n = len(d)
- mul = (K2 + 2 * n) & M64
- a = (f64(d, 0) * K2) & M64
- b = f64(d, 8)
- c = f64(d, n - 24)
- e0 = f64(d, n - 32)
- e = (f64(d, 16) * K2) & M64
- f = (f64(d, 24) * 9) & M64
- g = f64(d, n - 8)
- h = (f64(d, n - 16) * mul) & M64
- u = (rot((a + g) & M64, 43) + ((rot(b, 30) + c) & M64) * 9) & M64
- v = ((((a + g) & M64) ^ e0) + f + 1) & M64
- w = (bswap64((((u + v) & M64) * mul) & M64) + h) & M64
- x = (rot((e + f) & M64, 42) + c) & M64
- y = (((bswap64((((v + w) & M64) * mul) & M64) + g) & M64) * mul) & M64
- z = (e + f + c) & M64
- a2 = (bswap64(((((x + z) & M64) * mul) + y) & M64) + b) & M64
- b2 = (shift_mix(((((z + a2) & M64) * mul) + e0 + h) & M64) * mul) & M64
- return (b2 + x) & M64
- def weak_hash32_seeds(w, x, y, z, a, b):
- a = (a + w) & M64
- b = rot((b + a + z) & M64, 21)
- c = a
- a = (a + x + y) & M64
- b = (b + rot(a, 44)) & M64
- return (a + z) & M64, (b + c) & M64
- def weak_hash32_seeds_at(d, off, a, b):
- return weak_hash32_seeds(f64(d, off), f64(d, off + 8), f64(d, off + 16),
- f64(d, off + 24), a, b)
- def city_65_plus(d):
- n = len(d)
- x = f64(d, n - 40)
- y = (f64(d, n - 16) + f64(d, n - 56)) & M64
- z = hash_len16((f64(d, n - 48) + n) & M64, f64(d, n - 24), KMUL)
- v0, v1 = weak_hash32_seeds_at(d, n - 64, n, z)
- w0, w1 = weak_hash32_seeds_at(d, n - 32, (y + K1) & M64, x)
- x = (x * K1 + f64(d, 0)) & M64
- remaining = (n - 1) & ~63
- pos = 0
- while True:
- x = (rot((x + y + v0 + f64(d, pos + 8)) & M64, 37) * K1) & M64
- y = (rot((y + v1 + f64(d, pos + 48)) & M64, 42) * K1) & M64
- x ^= w1
- y = (y + v0 + f64(d, pos + 40)) & M64
- z = (rot((z + w0) & M64, 33) * K1) & M64
- v0, v1 = weak_hash32_seeds_at(d, pos, (v1 * K1) & M64, (x + w0) & M64)
- w0, w1 = weak_hash32_seeds_at(d, pos + 32, (z + w1) & M64,
- (y + f64(d, pos + 16)) & M64)
- x, z = z, x
- pos += 64
- remaining -= 64
- if remaining == 0:
- break
- return hash_len16(
- (hash_len16(v0, w0, KMUL) + (shift_mix(y) * K1) + z) & M64,
- (hash_len16(v1, w1, KMUL) + x) & M64, KMUL)
- def city64(d):
- n = len(d)
- if n <= 16:
- return city_0_16(d)
- if n <= 32:
- return city_17_32(d)
- if n <= 64:
- return city_33_64(d)
- return city_65_plus(d)
- def check_hash(word_bytes):
- return hash_len16((city64(word_bytes) - K2) & M64, CHECK_SEED, KMUL) & M32
- def key_hash_5_12(buf):
- n = len(buf)
- def b30(x, y):
- t = ror32((x * 0xCC9E2D51) & M32, 17)
- u = (y ^ ((t * 0x1B873593) & M32)) & M32
- return (5 * ror32(u, 19) + 0xE6546B64) & M32
- h = b30(n + f32(buf, 0), 5 * n)
- h = b30(5 * n + f32(buf, n - 4), h)
- h = b30(f32(buf, (n >> 1) & 4) + 9, h)
- v = h & M32
- v ^= v >> 16
- v = (v * 0x85EBCA6B) & M32
- v ^= v >> 13
- v = (v * 0xC2B2AE35) & M32
- v ^= v >> 16
- return v
- # ============================================================================
- # 三、440 HashMapReader(候选直接屏蔽表)
- # ============================================================================
- class Block:
- """i32 0x80000001 | i32 count | u32 bucket_off[count] | 桶...
- 桶: u32 n | u32 check[n] | u16 end[n] | blob
- """
- def __init__(self, data, flag_off):
- self.data = data
- self.base = flag_off
- flag = struct.unpack_from("<i", data, flag_off)[0]
- if flag >= 0:
- self.version = 0
- self.count = flag
- self.buckets_off = flag_off + 4
- else:
- self.version = flag & 0xFF
- self.count = struct.unpack_from("<i", data, flag_off + 4)[0]
- self.buckets_off = flag_off + 8
- def _off(self, h):
- return struct.unpack_from("<I", self.data, self.buckets_off + 4 * h)[0]
- def bucket(self, h):
- p = self.base + self._off(h)
- return p, struct.unpack_from("<I", self.data, p)[0]
- def bucket_checks(self, h):
- p, n = self.bucket(h)
- return struct.unpack_from("<%dI" % n, self.data, p + 4) if n else ()
- def bucket_size(self, h):
- p, n = self.bucket(h)
- end = struct.unpack_from("<H", self.data, p + 4 + 4 * n + 2 * (n - 1))[0] if n else 0
- return 4 + 6 * n + end
- def iter_checks(self):
- for h in range(self.count):
- yield from self.bucket_checks(h)
- def strict_ok(self):
- """桶必须紧挨着桶数组、首尾相接地排布(比原 sanity 严格得多,
- 能把随机命中的 0x80000001 排除掉,因此可放宽 count 下限)"""
- n = len(self.data)
- expect = (self.buckets_off - self.base) + 4 * self.count
- for h in range(self.count):
- off = self._off(h)
- if off != expect:
- return False
- p = self.base + off
- if p + 4 > n:
- return False
- k = struct.unpack_from("<I", self.data, p)[0]
- if k > 5000 or p + 4 + 6 * k > n:
- return False
- expect = off + self.bucket_size(h)
- if self.base + expect > n:
- return False
- return True
- def find_440_regions(data):
- out = []
- pat = b"\x01\x00\x00\x80"
- i = data.find(pat)
- while i >= 0:
- if i + 8 <= len(data):
- count = struct.unpack_from("<i", data, i + 4)[0]
- if 1 <= count <= 0x100000 and i + 8 + 4 * count <= len(data):
- try:
- blk = Block(data, i)
- if blk.version == 1 and blk.strict_ok():
- out.append(blk)
- except struct.error:
- pass
- i = data.find(pat, i + 1)
- return out
- PROBE = "鸡巴".encode("utf-8")
- def probe_hit(blk):
- return check_hash(PROBE) in blk.bucket_checks(key_hash_5_12(PROBE) % blk.count)
- # ============================================================================
- # 四、扫描与分析
- # ============================================================================
- def scan_dirs(extra_dirs):
- roots = []
- for parent in SCAN_PARENTS:
- try:
- for child in parent.iterdir():
- low = child.name.lower()
- if any(k in low for k in NAME_KEYS):
- roots.append(child)
- except (FileNotFoundError, PermissionError, NotADirectoryError):
- continue
- roots.extend(Path(d).expanduser() for d in extra_dirs)
- return roots
- def iter_candidate_files(roots):
- seen = set()
- for root in roots:
- walker = [(str(root), [], [root.name])] if root.is_file() else os.walk(root, onerror=lambda e: None)
- for dp, _dn, fns in walker:
- for fn in fns:
- p = os.path.join(dp, fn) if not root.is_file() else str(root)
- try:
- st = os.stat(p)
- except OSError:
- continue
- if not os.path.isfile(p) or st.st_size < 64 or st.st_size > MAX_FILE:
- continue
- key = (st.st_dev, st.st_ino)
- if key in seen:
- continue
- seen.add(key)
- try:
- with open(p, "rb") as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
- if mm.find(TRIE_MAGIC) >= 0:
- yield p
- except (OSError, ValueError):
- continue
- def analyze(path):
- """返回 dict;kind = 'sensitive' / 'skip'"""
- data = open(path, "rb").read()
- r = dict(path=path, size=len(data), words=set(), layers=0, regions=[],
- checks=set(), hits=[], unresolved=[], note="")
- try:
- t0, r["layers"] = load_trie(data)
- for p in iter_words(t0, data):
- try:
- r["words"].add(decode_key(p))
- except UnicodeDecodeError:
- pass
- except Exception as e: # noqa: BLE001
- r["note"] = "trie 解析失败: %s" % e
- by_word = {w: check_hash(w.encode("utf-8")) for w in r["words"]}
- word_checks = set(by_word.values())
- for blk in find_440_regions(data):
- checks = set(blk.iter_checks())
- if not checks:
- continue
- if probe_hit(blk) or (checks & word_checks) or "ssrc" in os.path.basename(path).lower():
- r["regions"].append(blk)
- r["checks"] |= checks
- if r["regions"]:
- r["kind"] = "sensitive"
- r["hits"] = sorted(w for w, c in by_word.items() if c in r["checks"])
- r["unresolved"] = sorted(r["checks"] - word_checks)
- elif "ssrc" in os.path.basename(path).lower() and r["words"]:
- r["kind"] = "sensitive"
- r["hits"] = sorted(r["words"])
- r["note"] = (r["note"] + "; " if r["note"] else "") + "未找到 440 表,输出 443 全量"
- else:
- r["kind"] = "skip"
- if not r["note"]:
- r["note"] = "无 440 屏蔽表(%d 个 trie 词,非敏感词库)" % len(r["words"])
- return r
- def main():
- ap = argparse.ArgumentParser(description="微信输入法(macOS) 敏感词库导出(多来源分块)")
- ap.add_argument("-f", "--file", action="append", default=[],
- help="只处理指定文件(可多次);不给则自动扫描")
- ap.add_argument("-d", "--dir", action="append", default=[],
- help="额外扫描的目录(可多次)")
- ap.add_argument("-o", "--output", default="wetype_sensitive_words.txt",
- help="输出 txt(默认 ./wetype_sensitive_words.txt)")
- ap.add_argument("--all-443", action="store_true",
- help="另存 <输出名>_all443.txt:每个文件 443 trie 全量词表(未经 440 过滤)")
- ap.add_argument("--no-header", action="store_true", help="不写文件总头(块头仍保留)")
- args = ap.parse_args()
- if args.file:
- files = [str(Path(f).expanduser()) for f in args.file]
- for f in files:
- if not os.path.isfile(f):
- raise SystemExit("文件不存在: %s" % f)
- else:
- roots = scan_dirs(args.dir)
- print("scan roots :")
- for r in roots:
- print(" %s" % pretty(r))
- files = sorted(iter_candidate_files(roots))
- if not files:
- raise SystemExit("没有找到含 'Ime trie header' 的文件。微信输入法装了吗?")
- results = [analyze(f) for f in files]
- sens = [r for r in results if r["kind"] == "sensitive"]
- skipped = [r for r in results if r["kind"] == "skip"]
- # 跨文件补全: 某文件 440 里只有哈希的条目,用其它文件的 443 词再试一次
- pool = {}
- for r in results:
- for w in r["words"]:
- pool.setdefault(check_hash(w.encode("utf-8")), w)
- for r in sens:
- found = [pool[c] for c in r["unresolved"] if c in pool]
- if found:
- r["hits"] = sorted(set(r["hits"]) | set(found))
- r["unresolved"] = [c for c in r["unresolved"] if c not in pool]
- r["note"] = (r["note"] + "; " if r["note"] else "") + \
- "其中 %d 词借助其它文件的明文还原" % len(found)
- lines = []
- if not args.no_header:
- lines += [
- "# 微信输入法敏感词库 (kind-440 StaticInterceptSensitiveDict, macOS)",
- "# generated: %s" % time.strftime("%Y-%m-%d %H:%M:%S"),
- "# sources : %d" % len(sens),
- "# total : %d (去重后 %d)" % (sum(len(r["hits"]) for r in sens),
- len(set().union(*[r["hits"] for r in sens])) if sens else 0),
- ]
- for r in skipped:
- lines.append("# skipped : %s (%s)" % (pretty(r["path"]), r["note"]))
- lines.append("")
- print("-" * 60)
- for i, r in enumerate(sens, 1):
- lines.append("# source%d: %s" % (i, pretty(r["path"])))
- lines.append("# count%d : %d" % (i, len(r["hits"])))
- if r["unresolved"]:
- lines.append("# unresolved%d: %d 条仅有哈希、文件内无明文 -> %s" % (
- i, len(r["unresolved"]),
- " ".join("0x%08x" % c for c in r["unresolved"][:64])
- + (" ..." if len(r["unresolved"]) > 64 else "")))
- if r["note"]:
- lines.append("# note%d : %s" % (i, r["note"]))
- lines.extend(r["hits"])
- lines.append("")
- print("source%d : %s" % (i, pretty(r["path"])))
- print(" trie : %d 层, %d 词" % (r["layers"], len(r["words"])))
- print(" 440 : %d 表, %d 条 check" % (len(r["regions"]), len(r["checks"])))
- print(" hits : %d unresolved: %d" % (len(r["hits"]), len(r["unresolved"])))
- for r in skipped:
- print("skipped : %s (%s)" % (pretty(r["path"]), r["note"]))
- with open(args.output, "w", encoding="utf-8", newline="\n") as f:
- f.write("\n".join(lines).rstrip("\n") + "\n")
- print("written : %s" % pretty(os.path.abspath(args.output)))
- if args.all_443:
- all_out = os.path.splitext(args.output)[0] + "_all443.txt"
- al = []
- for i, r in enumerate([x for x in results if x["words"]], 1):
- al += ["# source%d: %s" % (i, pretty(r["path"])),
- "# count%d : %d" % (i, len(r["words"]))]
- al += sorted(r["words"]) + [""]
- with open(all_out, "w", encoding="utf-8", newline="\n") as f:
- f.write("\n".join(al).rstrip("\n") + "\n")
- print("written : %s" % pretty(os.path.abspath(all_out)))
- if __name__ == "__main__":
- if sys.version_info < (3, 10):
- raise SystemExit("需要 Python 3.10+(用到 int.bit_count)")
- main()
Add Comment
Please, Sign In to add comment