Explorar o código

验证闸门: 控制台 GBK 编不出 ✔ 时降级为 '?', 别把"通过"报成失败

同一天踩了两次同一个坑: 校验结论"全部一致 ✔"的 print 在中文 Windows 控制台 (代码页 936)
抛 UnicodeEncodeError: 'gbk' codec can't encode character '\u2714' —— 进程退出码变成 1,
于是**闸门报失败而实际是通过的**。第一次在 portal_build.py (逐字节一致却 exit 1),
第二次在 page_fingerprint.py --diff (10 个端点全部回到基线却 exit 1)。

新增 src/console.py: soft() 把 stdout/stderr 的编码错误策略降级为 replace (编不出的符号写成 '?'),
只降 errors 不改 encoding —— 控制台 GBK 时中文照旧可读, UTF-8 时一切正常。

接入 4 个"以退出码讲话"的闸门 + portal_build.py:
  page_fingerprint.py   页面/取数面指纹比对 (回归闸门)
  check_portability.py  跨平台/相对路径门禁
  rebuild_from_raw.py   原始数据→产物的可重建性验收
  scan_stations.py      场站目录扫描辨识
  portal_build.py       门户拆装 (逐字节校验)

实测: 四个闸门现在通过时都返回 0 —— page_fingerprint --diff 对基线打印"全部一致"、
check_portability 打印"通过"、scan_stations 正常列场站、rebuild_from_raw 入口正常。
zhouyang.xie hai 1 mes
pai
achega
24be505ef0

+ 5 - 0
scripts/check_portability.py

@@ -119,4 +119,9 @@ def main() -> int:
 
 
 if __name__ == '__main__':
+    # 控制台 GBK 编不出 ✔ 时降级为 '?' (见 src/console.py): 否则"通过 ✔"这行 print 会抛
+    # UnicodeEncodeError、退出码变 1 —— 门禁报成失败而实际是通过的 (2026-09-11 踩过)。
+    sys.path.insert(0, str(ROOT))
+    from src import console
+    console.soft()
     sys.exit(main())

+ 4 - 0
scripts/page_fingerprint.py

@@ -142,4 +142,8 @@ def main() -> int:
 
 
 if __name__ == '__main__':
+    # 控制台 GBK 编不出 ✔ 时降级为 '?' (见 src/console.py): 否则"全部一致"这一行 print 会抛
+    # UnicodeEncodeError、退出码变 1 —— 闸门报成失败而实际是通过的 (2026-09-11 踩过)。
+    from src import console
+    console.soft()
     sys.exit(main())

+ 4 - 7
scripts/portal_build.py

@@ -239,13 +239,10 @@ def do_check() -> int:
 
 
 if __name__ == '__main__':
-    # 控制台可能是 GBK (Windows 中文默认 936): 正文里的 ✔/✘ 等符号编不出来会直接抛 UnicodeEncodeError
-    # (校验结论明明通过了却因为 print 崩掉, 退出码变成 1)。降级为 '?' 而不是崩。
-    for _s in (sys.stdout, sys.stderr):
-        try:
-            _s.reconfigure(errors='replace')
-        except Exception:
-            pass
+    # 控制台可能是 GBK (Windows 中文默认 936): 正文里的 ✔/✘ 编不出来会直接抛 UnicodeEncodeError
+    # (校验结论明明通过了却因为 print 崩掉, 退出码变成 1)。降级为 '?' 而不是崩。见 src/console.py。
+    from src import console
+    console.soft()
     ap = argparse.ArgumentParser()
     g = ap.add_mutually_exclusive_group()
     g.add_argument('--extract', action='store_true', help='从现有 portal.html 拆出 portal_src/ (一次性)')

+ 4 - 0
scripts/rebuild_from_raw.py

@@ -206,4 +206,8 @@ def main() -> int:
 
 
 if __name__ == '__main__':
+    # 控制台 GBK 编不出 ✔/✘ 时降级为 '?' (见 src/console.py): 校验通过却因 print 崩掉、
+    # 退出码变 1, 会被误读成"重建失败" (2026-09-11 踩过)。
+    from src import console
+    console.soft()
     sys.exit(main())

+ 3 - 0
scripts/scan_stations.py

@@ -72,4 +72,7 @@ def main() -> int:
 
 
 if __name__ == '__main__':
+    # 控制台 GBK 编不出 ✔ 时降级为 '?' (见 src/console.py)。
+    from src import console
+    console.soft()
     sys.exit(main())

+ 29 - 0
src/console.py

@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""控制台输出兜底 —— 中文 Windows 默认控制台代码页是 936 (GBK), 而脚本正文里常用 ✔ ✘ → 等符号。
+
+踩过的坑: `scripts/page_fingerprint.py` 比对结果"全部一致", 却因为最后一行 print 里的 ✔
+编不出来抛 `UnicodeEncodeError: 'gbk' codec can't encode character '\\u2714'`, 进程以退出码 1 结束 ——
+**闸门报告失败, 而实际是通过的**。2026-09-11 因此误判过一次 (同一坑在 portal_build.py 也踩了一次)。
+
+所以: 凡是"以退出码讲话"的脚本 (回归闸门/校验/巡检), 入口先调 `soft()`, 把 stdout/stderr 的
+错误处理降级为 replace (编不出的符号写成 `?`), 而不是让整条校验因为一个装饰性符号崩掉。
+
+只降 errors, 不改 encoding: 控制台是 GBK 时中文照旧可读; 控制台是 UTF-8 时一切正常。
+"""
+from __future__ import annotations
+
+import sys
+
+
+def soft(streams=None) -> None:
+    """把 stdout/stderr 的编码错误策略降级为 'replace' (幂等, 失败静默)。"""
+    for s in (streams or (sys.stdout, sys.stderr)):
+        try:
+            s.reconfigure(errors='replace')
+        except Exception:
+            pass
+
+
+def setup() -> None:
+    """别名, 语义同 soft (给"入口初始化"读起来更顺)。"""
+    soft()