You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何解决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

火山引擎 最新活动