如何解决UTF-16/UTF-32编码Python脚本中BOM与PEP 263的冲突,创建可正常编辑运行的Python 3脚本?
结论:几乎无法创建同时满足要求的UTF-16/UTF-32 Python脚本
很遗憾,结合Python的编码规范(PEP 263、PEP 3120)和Unicode标准的要求,你几乎不可能创建出同时符合「Python能正常执行」「vim/PyCharm开箱即用识别编码」的UTF-16/UTF-32编码Python 3脚本,核心矛盾和原因如下:
1. PEP 263与UTF-16/32 BOM的本质冲突
- Unicode标准明确要求:UTF-16/UTF-32文件必须带BOM(字节顺序标记)才能让编辑器、系统自动识别编码(无BOM的情况下,软件无法区分大端/小端字节序,只能手动指定编码)。
- 但PEP 263规定:Python脚本的编码声明行(第一行,若有shebang则为第二行)必须是ASCII兼容的文本,且要符合
# -*- coding: <encoding name> -*-的格式——而UTF-16/32编码下,所有字符都是双字节/四字节存储,BOM本身就是非ASCII字节,后续的编码声明行也会被转换成非ASCII的字节流,Python的tokenize.detect_encoding函数是用ASCII正则来匹配编码注释的,根本无法识别这些字节,自然会抛出编码错误。
2. 无BOM的UTF-16/32的死胡同
- 如果你省略BOM,编辑器(vim/PyCharm/GitLab)无法自动识别编码,必须手动修改编码设置才能正常显示,这直接违反了「开箱即用」的要求。
- 即使你手动让编辑器显示正确,Python依然无法识别编码:没有BOM,也没有能被ASCII正则匹配的编码注释行,Python会默认用UTF-8解析,结果就是字节乱码,要么抛出语法错误,要么解析出无意义的内容(比如你遇到的Python 3.10不报错但不执行
print的情况,本质是脚本内容被解析成了非预期的空白或无效语法)。
可行的折中方案
如果你只是为了测试特洛伊源攻击的检测工具,不妨换个思路:
方案1:优先使用UTF-8编码(推荐)
PEP 3120已经将UTF-8设为Python 3脚本的默认编码,完全避开了所有编码冲突问题:
- 编辑器能自动识别UTF-8,无需手动设置;
- Python能直接执行,不需要额外的编码声明;
- 特洛伊源攻击的核心(比如双向控制字符、同形异义字符)都能在UTF-8下复现,完全满足测试需求。
方案2:创建仅用于静态检测的UTF-16/32测试文件
如果必须测试UTF-16/32场景,只要求linter能检测,不要求Python执行:
- 创建带BOM的UTF-16/32文件,编辑器会通过BOM自动识别编码;
- 这类文件虽然Python无法执行,但可以作为linter的静态测试用例,验证工具对非UTF-8编码文件的特洛伊源检测能力。
方案3:用UTF-8脚本中转处理UTF-16/32内容
写一个UTF-8编码的Python脚本,读取UTF-16/32编码的测试文件内容,手动指定编码解析后,再传递给你的自定义linter检测——这样既保证了主脚本能被Python执行、编辑器正常识别,又能覆盖UTF-16/32的测试场景。
附录:测试文件生成代码
# create_examples.py from pathlib import Path base_path = Path(__file__).parent.absolute() def write_encoding(enc: str, strip_endian: bool = False): encoding = enc.lower() if encoding.endswith('be') or encoding.endswith('le'): # As defined in unicode, if the le or the be is given, a BOM is not written bom_used = False if strip_endian: enc = enc[:-3] else: bom_used = True suffix = enc if bom_used: suffix += "_bom" name = f"test_{encoding}_{suffix}.py" content = f"# vim: set fileencoding={enc} :\n" content += f"print(\"{name}\")\n" content += 's = "x" * 100 # "x" is assigned' content += "\n" path = base_path / name path.write_bytes(content.encode(encoding)) write_encoding('utf_8') write_encoding('utf_16') write_encoding('utf_16_be', False) write_encoding('utf_16_be', True) write_encoding('utf_16_le', False) write_encoding('utf_16_le', True) write_encoding('utf_32') write_encoding('utf_32_be', False) write_encoding('utf_32_be', True) write_encoding('utf_32_le', False) write_encoding('utf_32_le', True)
测试文件执行命令
for i in $(ls test_*); do python $i; done
内容的提问来源于stack exchange,提问作者Kound




