我正在使用Python 3.6.0b2。
我正在解析很多电子邮件。这一个特定的电子邮件是一个问题,因为我无法打印电子邮件地址的显示名称。试图打印电子邮件地址显示名称给出:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcc2' in position 30: surrogates not allowed
这是一段测试用例,展示了如何重现问题:
(venv3.6) mailripper@ip-10-0-0-112:/opt/mailripper$ cat test.py
from email import policy
from email.headerregistry import Address
from email.parser import BytesHeaderParser, BytesParser
email_bytes = b'From: =?utf-8?Q?John_Smith=2C_Prince2=C2=AE=2CPMP=C2=AE=2C_CSM=C2?=\r\n =?utf-8?Q?=AE=2C_ITIL=C2=AE=2C_ISTQB=C2=AE?= <jon.smith@example.org>\r\n'
msg = BytesHeaderParser(policy=policy.default).parsebytes(email_bytes)
print(msg['from'])
print(msg['from'].addresses[0].display_name)
以下是由上述代码生成的错误:
(venv3.6) mailripper@ip-10-0-0-112:/opt/mailripper$ python test.py
"John Smith, Prince2®,PMP®, CSM� �, ITIL®, ISTQB®" <jon.smith@example.org>
Traceback (most recent call last):
File "test.py", line 8, in <module>
print(msg['from'].addresses[0].display_name)
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcc2' in position 30: surrogates not allowed
这是显示名称,如OSX电子邮件客户端所示,它似乎能够解析它(这是截图,裁剪得很小):
我的目标是能够处理任何没有unicode错误的电子邮件,并且无需编写自定义unicode错误处理代码 - 这可能吗?
有人可以建议我在显示电子邮件地址显示名称时能避免出现Unicode错误吗?
答案 0 :(得分:3)
这里有一个棘手的问题。你的直接例子并不强硬:根据RFC 2047的规则,它是无效的。 email.parser
模块拒绝它是合理的。但是,电子邮件中包含的内容根据规则无效。电子邮件工具通常很难通过无效内容来挽救某些内容。你希望你的工具对无效内容做什么?
以下是您的示例无效的内容。我把它缩短了一点。它的相关部分是,
b'From: =?utf-8?Q?John=2C_PMP=C2=AE=2C_CSM=C2?=\r\n =?utf-8?Q?=AE=2C?= <jon@eg.org>\r\n'
这可能最初是字符串:From: John, PMP®, CSM®, <jon@eg.org>
。
这是一个Python字节字符串,包含From:
标题为 encoded-words 。此规范是 RFC 2047, MIME Part Three: Message Header Extensions for Non-ASCII Text 。
在示例中,您会看到=?utf-8?Q?
和?=
各有两个序列。 RFC 2047, Section 2, "Syntax of encoded-words"告诉我们,这些标记了两个编码字的开头和结尾,并且它们使用UTF-8字符集和Quoted-Printable编码。在“PMP”之后,存在序列=C2=AE
。这编码了2个八位字节的UTF-8序列0xC2 0xAE
,它是字符'®'。序列=2C
编码1-octet UTF-8(和ASCII)序列0x2C,即字符','。
第一个?=
和第二个=?utf-8?Q?
之间的部分读取\r\n
。这是字面的,不是根据RFC 2047编码的。它是通过插入行结尾和前导空白来延长长标题行。这也很合法。
现在照看“CSM”。请注意,有一个序列=C2
,然后是第一个结束第一个编码字的?=
。在第二个=?utf-8?Q?
开始第二个编码字之后,会有一个序列=AE
。这是相同的2个八位字节UTF-8序列0xC2 0xAE
,再次代表字符'®'。但是,UTF-8字符的两个八位字节在相邻的编码字中分开。
这违反了RFC 2047, Section 5, "Use of encoded-words in message headers" *的规则。它说:
每个'编码字'必须代表整数个字符 多个八位字节字符可能不会在相邻的“编码字”之间拆分。
输入的这两个渲染中的任何一个都是有效的:
b'From: =?utf-8?Q?John=2C_PMP=C2=AE=2C_CSM=C2=AE?=\r\n =?utf-8?Q?=2C?= <jon@eg.org>\r\n'
b'From: =?utf-8?Q?John=2C_PMP=C2=AE=2C_CSM?=\r\n =?utf-8?Q?=C2=AE=2C?= <jon@eg.org>\r\n'
(这是我阅读规范。我没有运行代码来检查。)
现在,您提出两个问题:
我的目标是能够处理任何没有unicode错误的电子邮件,并且 没有编写自定义unicode错误处理代码 - 可能吗?
我的建议是“不”。如果您要处理任何电子邮件,则需要准备好处理错误形成的电子邮件。您将需要编写自定义错误处理代码 - 不仅仅是针对Unicode问题,还要处理所有问题 - 以应对毫无疑问需要清洗的奇怪内容。
任何人都可以建议我可以做些什么来避免出现Unicode错误 显示电子邮件地址显示名称?
对于这个例子,我可以看到三种方法:
看看class email.policy.EmailPolicy(**kw)
,看看你是否可以弄清楚如何扩展它来处理这种错误编码的内容。您正在policy
中将此类的亲戚传递为BytesHeaderParser(policy=policy.default).parsebytes(email_bytes)
。
预处理所有标题行,查看此问题的结尾字节和连续编码字的开头。使用您自己的代码修复它,然后将更正的标题提供给BytesHeaderParser()
。也许你可以写一个可以检测到问题的regular expression。
在异常处理程序中将您的调用传递给BytesHeaderParser()
,该异常处理程序将仅针对失败的行尝试#2中的修复。修好了这一行后,您可以再次尝试BytesHeaderParser()
。
还会有其他问题。考虑构建您的代码,以便能够为无效内容提供越来越多的修复,因为您发现需要它们。