β

Python 3 的 surrogateescape

TimothyQiu's Blog 240 阅读

新项目中第一次开始 作死 使用 Python 3,在测试中遇到一件奇怪的事情。

系统中有一个邮件发送模块,直接在命令行中手动跑 Worker 的话邮件可以成功发送,而一旦用 supervisor 运行则无法发送邮件。日志中显示在邮件发送库中抛出了如下异常:

UnicodeEncodeError: 'utf-8' codec can't encode character '\udce6' in position 0: surrogates not allowed

Surrogate

所以,日志里这个 surrogate 是个啥?

Surrogate 是 Unicode 中位于 BMP 外的一组不会有对应字符的码位,Python 3 中使用这些码位来「代表」无法编码的字节。

如果你明白 「锟斤拷」是怎么来的 ,那么就一定能理解 Python 的 replace 编解码错误处理机制:无法解码的字节会被替换成 U+FFFD ,无法编码的码位会被替换成 ? 。这样做有一个明显的缺点就是不可逆。Python 3 中新增的 surrogateescape 则是一种可逆的错误处理机制,利用 Surrogate 码位保存无法解码的字节,编码时则将其还原为对应的原始字节。

例子中的报错正是因为 0xE6 无法使用 ascii 解码,而被解为了 U+DCE6

'\udce6'.encode('ascii', 'surrogateescape')
>>> b'\xe6'
b'\xe6'.decode('ascii', 'surrogateescape')
>>> '\udce6'

知道了原因,用这种方法解码异常信息中的完整字符串就找到了罪魁祸首:邮件发送者的配置。

Python 3 的环境变量与编码

邮件发送者是使用 显示名称 <邮箱地址> 的格式在环境变量中设置的,显示名称部分包含了汉字。

Python 3 中,使用 os.getenv 或者 os.environ 获得到的环境变量的值都是 str 类型。在类 Unix 系统中,解码过程是使用 sys.getfilesystemencoding() 'surrogateescape' 错误处理来实现的。而 sys.getfilesystemencoding() 的取值与 nl_langinfo(COODESET) 相关,即 LC_CTYPE 环境变量。

所以,导致这部分环境变量包含 Surrogate 的原因就是 LC_TYPE 指定的编码不正确。

当环境变量 LC_CTYPE LC_ALL LANG 均没有显式定义时,各个操作系统有各自的 LC_CTYPE 的缺省值(比如 C )。这种情况下,通过 sys.getfilesystemencoding() 通常会得到 ascii ,环境变量中包含非 ASCII 字符就会得到上面的 Surrogate。

真正的问题

到了这里,问题根源和解决方法基本上已经水落石出了。

前面说手动跑 Worker 可以但是 supervisor 跑 Worker 就不行,这是因为被 supervisor 管理的子进程并不会单独开 Shell 而是继承 supervisord 的 Shell,所以基本的环境变量也有不同。

解决方法便是在 supervisor 配置中修改 environment,加入 LC_CTYPE 或者 LC_ALL 或者 LANG 环境变量的配置。

附, LANG LC_ALL LC_TYPE 什么的之间到底是个啥关系:查找 LC_X 的值的过程是: LC_ALL 有值就用 LC_ALL 的值;否则 LC_X 有值就用 LC_X 的值;否则 LANG 有值就用 LANG 的值;否则就用操作系统的默认值。

以上。

参考文档

作者:TimothyQiu's Blog
keep it simple stupid
原文地址:Python 3 的 surrogateescape, 感谢原作者分享。