# -*- coding: utf-8 -*- """页面文案守卫·口语化词 (2026-09-14 页面专业化包, 台账 015)。静态扫描, 不连库、不起浏览器。 ## 为什么要有它 用户的第一条意见是「页面文案口语化,不像交易系统」。板块标题写成「等我拍板」「我的持仓」, 处置词写成「出手」「休假」,第一二人称满页都是。改完之后要有一道机器检查, 免得日后又飘回口语。 ## 它检查什么 一份禁用词清单, 分三类: 第一二人称、拟人与口语、生造动词。**只查会显示给人看的文本** —— 先剥掉三种注释 (HTML 注释、JS 块注释、JS 行注释), 注释里为解释代码用到这些词不算。命中就打 行号、词、片段, 退出码 1。 剥行注释时跳过字符串内容与网址里的双斜线 (`https://`), 免得把字符串里的 `//` 当注释起点。 ## 豁免 确实要在显示文本里用某个词 (极少), 用成对标记圈起来并写原因: 模板里: ... 脚本里: // copy-guard:off 原因... ... // copy-guard:on 缺原因或不成对, 判红 (不许无声豁免)。 跑法: python3 scripts/test_page_copy_guard.py """ import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) PAGE = os.path.join(os.path.dirname(HERE), "app", "web", "static", "index.html") # 禁用词。命中即报 (在显示文本里)。分三类只为可读, 检查时一视同仁。 BANNED = [ # 第一二人称 "我的", "等我", "等你", "要你", "给你", "替你", "让你", "问过你", "你自己", "你设", "你改", "你点", "你来", "你现在", "你还没", "你做过", "你来定", "交你定", # 拟人与口语 "拍板", "点头", "盯着", "在盯", "盯的", "拿不准", "记一笔", "挤掉", "平安无事", # 生造动词与口语动词 "出手", "休假", "一跳", "发生了什么", ] OFF, ON = "copy-guard:off", "copy-guard:on" def _strip_line_comment(line: str) -> str: """去掉一行里的 JS 行注释 (// 到行尾), 但跳过字符串内的 // 与网址里的 ://。 走一遍字符, 记录当前是否在 ' " ` 里; 不在字符串里遇到 // 且前一个字符不是 : 就截断。""" q = None # 当前字符串引号, None 表示不在字符串里 i, n = 0, len(line) while i < n: ch = line[i] if q: if ch == "\\": i += 2 continue if ch == q: q = None else: if ch in "'\"`": q = ch elif ch == "/" and i + 1 < n and line[i + 1] == "/": if i > 0 and line[i - 1] == ":": # 网址 :// 不是注释 i += 2 continue return line[:i] i += 1 return line def _exempt_ranges(raw: str): """在原始文本上找成对豁免区。返回 (豁免行号集合, 报错列表)。缺原因或不成对判红。""" bad = [] lines = raw.split("\n") ranges = set() stack = [] for i, line in enumerate(lines, 1): if OFF in line: after = line.split(OFF, 1)[1] reason = re.sub(r"[-#>*/\s]+$", "", after).strip(" -#>*/") if not reason: bad.append(f"第 {i} 行:copy-guard:off 缺原因") stack.append(i) if ON in line: if not stack: bad.append(f"第 {i} 行:copy-guard:on 没有对应的 off") else: start = stack.pop() for k in range(start, i + 1): ranges.add(k) for s in stack: bad.append(f"第 {s} 行:copy-guard:off 没有对应的 on") return ranges, bad def main(): raw = open(PAGE, encoding="utf-8").read() exempt, bad = _exempt_ranges(raw) # 剥注释 (保行号) def _blank(m): return "\n" * m.group(0).count("\n") src = re.sub(r"", _blank, raw, flags=re.S) src = re.sub(r"/\*.*?\*/", _blank, src, flags=re.S) lines = src.split("\n") for i, line in enumerate(lines, 1): if i in exempt: continue text = _strip_line_comment(line) for w in BANNED: idx = text.find(w) if idx >= 0: seg = text.strip()[:100] bad.append(f"第 {i} 行:口语化词「{w}」出现在显示文本里。\n {seg}") # 内置自检: 只在注释里出现不算, 在字符串里出现要算 _self_ok(bad) if bad: print(f"页面文案守卫·口语化:发现 {len(bad)} 处") for m in bad: print(" FAIL", m) sys.exit(1) print(f"ALL OK — 页面文案守卫·口语化:{len(BANNED)} 个禁用词、注释剥离、成对豁免 全部通过") def _self_ok(bad): """自检样例: 注释里的禁用词不该报, 字符串里的该报。任一不符往 bad 里加一条。""" only_comment = " // 这里解释一下等我拍板的逻辑\n const x = 1;" in_string = " const t = '等我拍板';" # 注释行剥掉后不含禁用词 c = _strip_line_comment(only_comment.split("\n")[0]) if any(w in c for w in BANNED): bad.append("自检失败:行注释里的禁用词没有被剥掉") # 字符串行剥不掉 (没有 //), 仍含禁用词 s = _strip_line_comment(in_string) if not any(w in s for w in BANNED): bad.append("自检失败:字符串里的禁用词被误剥") # 网址不被当注释截断 u = _strip_line_comment(" const url = 'https://a.b/c'; const y = 2;") if "const y = 2" not in u: bad.append("自检失败:网址里的 // 被当成注释起点") if __name__ == "__main__": main()