如何在Python中将代理对转换为普通字符串?

53
这是对如何将包含Unicode代理对的JSON编码数据转换为字符串?的跟进。在那个问题中,提问者有一个由json.dumps()编码的文件,其中一个表情符号被表示为代理对- \ud83d\ude4f。 他们在读取文件并正确翻译表情符号方面遇到了问题,正确的 答案 是从文件中使用json.loads()加载每一行,然后json模块会处理从代理对到(我假设为UTF8编码的)表情符号的转换。
所以这是我的情况:假设我有一个普通的Python 3 Unicode字符串,其中包含一个代理对:
emoji = "This is \ud83d\ude4f, an emoji."

我该如何处理这个字符串以获得emoji的表示形式? 我想要获得类似于以下内容的东西:
"This is , an emoji."
# or
"This is \U0001f64f, an emoji."

我已经尝试过:

print(emoji)
print(emoji.encode("utf-8")) # also tried "ascii", "utf-16", and "utf-16-le"
json.loads(emoji) # and `.encode()` with various codecs

通常我会遇到类似于 UnicodeEncodeError: XXX编解码器无法在位置8处对字符'\ud83d'进行编码:代理不允许 的错误。
我正在Linux上运行Python 3.5.1,$LANG设置为en_US.UTF-8。我在命令行上的Python解释器和在Sublime Text中运行的IPython内部都运行了这些示例 - 看起来没有任何区别。

2
tweepy(以及一般的Twitter)似乎正在做这件事。在这里提到它,希望更多搜索此问题的人能找到这个答案。 - tripleee
在反向方向上(从单个字符到代理对):Python:从非BMP Unicode字符查找等效代理对 - Stack Overflow - user202729
python - Current idiom for removing 'surrogateescape' characters fron a decoded string - Stack Overflow中提到了ftfy.fixes.fix_surrogates(text)(第三方库)用于删除解码字符串中的'surrogateescape'字符的当前惯用语。 - user202729
关于我之前的评论,实际上,任何使用JSON存储Unicode字符串的东西都被迫使用代理对,因为JSON字符串表示法本身不支持非BMP码点。 - undefined
2个回答

65

你在磁盘上的json文件中混合了一个文字字符串\ud83d(六个字符:\ u d 8 3 d)和一个单字符u'\ud83d'(在Python源代码中使用字符串文字进行指定)。这是Python 3中len(r'\ud83d') == 6len('\ud83d') == 1之间的区别。

如果你看到'\ud83d\ude4f'这样的Python字符串(2个字符),那么就存在一个顶部错误。通常情况下,你不应该得到这样的字符串。如果你得到了这个字符串并且无法修复生成它的顶层程序,你可以使用surrogatepass错误处理程序来修复它:

>>> "\ud83d\ude4f".encode('utf-16', 'surrogatepass').decode('utf-16')
''

Python 2 更加宽容.

注意:即使您的json文件包含文字\ud83d\ude4f(12个字符); 您也不应该得到代理对:

>>> print(ascii(json.loads(r'"\ud83d\ude4f"')))
'\U0001f64f'

注意:结果是1个字符('\U0001f64f'),而不是代理对('\ud83d\ude4f')。


20
因为这是一个经常出现的问题,而且错误消息有点模糊,所以这里提供更详细的解释。
代理是一种表达大于U+FFFF的Unicode代码点的方法。
请记住,Unicode最初被规定包含65,536个字符,但很快发现这不足以容纳世界上所有字形。
作为(否则是固定宽度的)UTF-16编码的扩展机制,设立了一个保留区域,用于包含表示基本多文种平面之外代码点的机制:任何在此特殊区域中的代码点都必须后跟来自同一区域的另一个字符代码,它们一起将表示一个数字大于旧限制的代码点。
(严格来说,代理区域分为两半;一对中的第一个代理必须来自高代理半部分,而第二个代理必须来自低代理。令人困惑的是,高代理U+D800-U+DBFF具有比低代理U+DC00-U+DFFF更低的代码点编号。)
这是一种遗留机制,专门支持UTF-16编码,并且不应在其他编码中使用;它们不需要它,并且适用的标准明确说明禁止使用。
换句话说,虽然 U+12345 可以用代理对 U+D808 U+DF45 表示,但除非你专门使用 UTF-16,否则应直接表示它。更详细地说,以下是如何将其作为单个字符用 UTF-8 表示的:
0xF0 0x92 0x8D 0x85

这里是相应的代理序列:

0xED 0xA0 0x88
0xED 0xBD 0x85

正如已经在被接受的答案中建议的那样,您可以使用以下内容来往返

>>> "\ud808\udf45".encode('utf-16', 'surrogatepass').decode('utf-16').encode('utf-8')
b'\xf0\x92\x8d\x85'

也许还可以参考http://www.russellcottrell.com/greek/utilities/surrogatepaircalculator.htm


Related:https://dev59.com/questions/LJDea4cB1Zd3GeqPi_eG - tripleee
这是误导性的。在Python中,"\U0001f64f"是一个单一字符(1个Unicode代码点),您不需要代理对来表示它。现在,各种字符编码可能需要多个代码单元来表示它(utf-8使用8位(1字节)代码单元,utf-16使用16位(2字节)代码单元。两者都是可变宽度编码:单个Unicode 代码点可能需要多个代码单元。JSON允许在\u转义中使用UTF-16代理对。但同样,它也不需要它们。原则上,JSON允许将代理对保留为两个代码点,但Python不支持它。 - jfs
JSON除替代项外没有其他机制来表达BMP之外的码位。例如,在json字符串中,可以直接放置任何Unicode字符(除了双引号"和反斜杠\以及控制字符),比如""就是一个符合标准的json字符串(虽然可以通过替代转义来表示,但不必要这样做)。参考链接:https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf - jfs
这个问题是关于为什么Python拒绝加载代理对以及你可以怎么做。当然,它只是一个代码点。我不明白你怎么会把它解释成其他意思。json.loads可以很好地处理它,但如果你正在读取例如CSV文件,则会失败。我的答案解释了原因并提出了一些可能的解决方案。 - tripleee
我发布第二个答案的原因是许多人试图导入例如使用代理对的JSON编码的Twitter CSV数据,而这并不明显为什么这是他们问题的重复。 - tripleee
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接