Unicode Python 字符串中的字节数
Posted
技术标签:
【中文标题】Unicode Python 字符串中的字节数【英文标题】:Bytes in a unicode Python string 【发布时间】:2012-04-08 09:43:22 【问题描述】:在 Python 2 中,Unicode 字符串可能同时包含 unicode 和字节:
a = u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba'
我知道这绝对是不应该在自己的代码中编写,但这是我必须处理的字符串。
上面字符串中的字节是 UTF-8 for ек
(Unicode \u0435\u043a
)。
我的目标是获得一个包含 Unicode 格式的所有内容的 unicode 字符串,即Русский ек
(\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \u0435\u043a
)。
将其编码为 UTF-8 产生
>>> a.encode('utf-8')
'\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xb8\xd0\xb9 \xc3\x90\xc2\xb5\xc3\x90\xc2\xba'
然后从 UTF-8 解码得到带有字节的初始字符串,这不好:
>>> a.encode('utf-8').decode('utf-8')
u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba'
我找到了一个解决问题的方法,但是:
>>> repr(a)
"u'\\u0420\\u0443\\u0441\\u0441\\u043a\\u0438\\u0439 \\xd0\\xb5\\xd0\\xba'"
>>> eval(repr(a)[1:])
'\\u0420\\u0443\\u0441\\u0441\\u043a\\u0438\\u0439 \xd0\xb5\xd0\xba'
>>> s = eval(repr(a)[1:]).decode('utf8')
>>> s
u'\\u0420\\u0443\\u0441\\u0441\\u043a\\u0438\\u0439 \u0435\u043a'
# Almost there, the bytes are proper now but the former real-unicode characters
# are now escaped with \u's; need to un-escape them.
>>> import re
>>> re.sub(u'\\\\u([a-f\\d]+)', lambda x : unichr(int(x.group(1), 16)), s)
u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \u0435\u043a' # Success!
这很好用,但由于使用了eval
、repr
,然后对 unicode 字符串表示进行了额外的正则表达式,因此看起来很 hacky。有没有更清洁的方法?
【问题讨论】:
没有可靠的方法来解决这个问题,因为输入数据首先没有包含足够的信息。 输入数据中的所有字节都是UTF-8编码的字符,所以我认为可以安全地假设初始字符串中的每个字节序列都可以安全地从UTF-8解码 @NiklasB。是正确的 - UTF-8 编码字节也是有效的 Unicode 代码点,所以没有办法知道什么是可靠的。 @EtiennePerot,如果您从 UTF-8 字节序列开始,请将其添加到问题中。您向我们展示的是一个不同的 Unicode 字符串! 顺便说一句,“Русский ек”似乎也无效,它可能应该是“Русский язык”(=俄语),所以我想还有更多的问题。 【参考方案1】:在 Python 2 中,Unicode 字符串可能同时包含 unicode 和字节:
不,他们可能不会。它们包含 Unicode 字符。
在原始字符串中,\xd0
不是 UTF-8 编码的字节。它是代码点为 208 的 Unicode 字符。u'\xd0'
== u'\u00d0'
。碰巧的是,Python 2 中 Unicode 字符串的 repr
更喜欢在可能的情况下使用 \x
转义来表示字符(即代码点
无法查看字符串并判断 \xd0
字节是否应该是某个 UTF-8 编码字符的一部分,或者它本身是否真的代表该 Unicode 字符。
但是,如果您假设您始终可以将这些值解释为编码值,您可以尝试编写依次分析每个字符的内容(使用 ord
转换为代码点整数),将小于 256 的字符解码为UTF-8,并按原样传递 >= 256 个字符。
【讨论】:
我想我必须做出这样的假设才能让 unicode 字符串正常运行。我知道如果假设失败并且字符串应该包含 您可以隔离高阶 ASCII 字符 (x80-xFF),然后尝试将它们从 utf8 转换。如果成功,这很可能是正确的,因为普通文本不太可能包含 utf8 序列(î
任何人?),否则保持原样。
@thg435 这正是我的简单 Perl 解决方案所做的;但是由于某种原因,在 Python 中你经历了更多的麻烦;请参阅@Kev 的答案和 cmets。我很惊讶接受的答案并没有准确地说明如何做到这一点。
@tchrist:我发布了 example 我的意思,它比你的 perl sn-p 更冗长,但仍然简洁。
> “碰巧的是,[Python] 中用于 Unicode 字符串的 repr
更喜欢在可能的情况下使用 \x
转义来表示字符” — 确实,而 CPython 源代码中的 this seems to be the relevant code (as of today) 决定了如何转义字符。或者您可以尝试类似:for n in range(300): print hex(n), repr(unichr(n))
或(Python 3)for n in range(900): print(hex(n), repr(chr(n)), ascii(chr(n)))
。【参考方案2】:
(响应上面的 cmets):这段代码转换了所有看起来像 utf8 的东西,并保持其他代码点不变:
a = u'\u0420\u0443\u0441 utf:\xd0\xb5\xd0\xba bytes:bl\xe4\xe4'
def convert(s):
try:
return s.group(0).encode('latin1').decode('utf8')
except:
return s.group(0)
import re
a = re.sub(r'[\x80-\xFF]+', convert, a)
print a.encode('utf8')
结果:
Рус utf:ек bytes:blää
【讨论】:
干得好,我正在寻找某种null
编码,就像 latin1
所做的那样,它只会返回未修改的 Unicode 代码点 chr(ord(c)) 解决方法强制重新解释要优雅得多,恕我直言。
非常好。介意我是否接受 Karl Knechtel 的回答?我认为任何偶然发现这个问题的人都应该被告知为什么首先拥有这些字符串是一个坏主意,以及为什么尝试以这种方式修复它们容易出错【参考方案3】:
问题在于您的字符串实际上并未以特定编码方式编码。您的示例字符串:
a = u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba'
将 python 的 unicode 字符串内部表示与utf-8
编码文本混合在一起。如果我们只考虑“特殊”字符:
>>> orig = u'\u0435\u043a'
>>> bytes = u'\xd0\xb5\xd0\xba'
>>> print orig
ек
>>> print bytes
ек
但你说,bytes
是utf-8
编码的:
>>> print bytes.encode('utf-8')
ек
>>> print bytes.encode('utf-8').decode('utf-8')
ек
错了!但是呢:
>>> bytes = '\xd0\xb5\xd0\xba'
>>> print bytes
ек
>>> print bytes.decode('utf-8')
ек
万岁。
所以。 这对我意味着什么?这意味着您(可能)解决了错误的问题。您应该问我们/试图弄清楚的是为什么您的字符串一开始就采用这种形式以及如何避免它/修复它在你把它们都弄混了。
【讨论】:
一切都是真的;我知道这是不对的,而且这些字符串根本不应该是这种形式。这些字符串来自其他人编写的 Python 模块(一个名为 wikitools 的 MediaWiki API 库)。我也许可以修复该模块,而不是尝试自己处理事情,但如果有一个简单的解决方案而无需编辑该模块,我宁愿选择简单的解决方案。 @Etienne:问题是有很多你没有想到的情况(特别是你无法判断某些东西是 UTF-16 还是 UTF-8 编码数据的情况)会破坏您拥有的“解决方案”(这实际上只是一个讨厌的解决方法)。您真的应该考虑接受这个或 Karl 的回答,它更详细地解释了问题。 @tchris:它并没有像你想象的那样“破碎”。\xB5
只是等效 \u00B5
的默认表示,所以实际上破坏的是启发式“字节
【参考方案4】:
您应该将unichr
s 转换为chr
s,然后对其进行解码。
u'\xd0' == u'\u00d0'
是True
$ python
>>> import re
>>> a = u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba'
>>> re.sub(r'[\000-\377]*', lambda m:''.join([chr(ord(i)) for i in m.group(0)]).decode('utf8'), a)
u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \u0435\u043a'
r'[\000-\377]*'
将匹配 unichrs u'[\u0000-\u00ff]*'
u'\xd0\xb5\xd0\xba' == u'\u00d0\u00b5\u00d0\u00ba'
您使用 utf8
编码字节作为 unicode 代码点(这是问题)
我通过将那些错误的 unichars 伪装成相应的字节来解决问题
我搜索了所有这些错误的 unichars,并将它们转换为 chars,然后对其进行解码。
如果我错了,请告诉我。
【讨论】:
仅仅因为一个字节在 0x80 到 0xff 范围内并不意味着它是 UTF-8 序列的一部分。这些字节中的每一个也是一个有效的 Unicode 代码点,如果您的字符串包含该范围内的实际字符,则此方法将失败。 @tchrist:在 Python 中也是如此。之所以有单独的chr
和unichr
函数,是因为前者产生一个“经典”的ASCII 字符串,而后者产生一个unicode 字符串。在 Python 3 中,没有这样的目标,所有字符串都是 unicode(因此,unichr
不再存在)。
@tchrist:我认为 OP 只是面临严重损坏的输入并试图修复症状,而不是原因。同意拥有两种不同类型的字符串是次优的,我认为这主要是历史原因在这里发挥作用。 Ruby 1.8 也遇到了同样的问题,我想知道为什么他们没有从一开始就将 Unicode 作为两种语言的默认设置......
@NiklasB 请接受我一开始如此密集的道歉。去年我做 Python 编程时,我只使用了 Python3,所以不确定 Python2 是如何工作的。我现在看到了实际发生的事情;我必须为 OP 将我的 Perl 解决方案翻译成 Python。但我非常同意他们真的应该追查对一段字符串进行双重编码的部分并修复它,而不是试图在事后消除损坏。
@tchrist:我认为这里发生的事情是 decode 旨在将二进制(非 Unicode)字符串转换为 Unicode。如果使用 Unicode 字符串作为参数调用,它首先使用默认编码(通常为 ascii)将其转换为二进制字符串,这对于代码点 > 127 显然会失败。s.encode('latin1').decode('utf-8')
似乎是一个很好的解决方案。【参考方案5】:
您已经得到了答案,但这里有一种解读 UTF-8-like Unicode 序列的方法,这种方法不太可能错误地解码 latin-1 Unicode 序列。 re.sub
函数:
-
匹配类似于有效 UTF-8 序列的 RFC 3629)。
将 Unicode 序列编码为其等效的 latin-1 字节序列。
使用 UTF-8 将序列解码回 Unicode。
用匹配的 Unicode 字符替换原始的类似 UTF-8 的序列。
请注意,如果只是正确的字符彼此相邻,这仍然可以匹配 Unicode 序列,但这种可能性要小得多。
import re
# your example
a = u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba'
# printable Unicode characters < 256.
a += ''.join(chr(n) for n in range(32,256)).decode('latin1')
# a few UTF-8 characters decoded as latin1.
a += ''.join(unichr(n) for n in [2**7-1,2**7,2**11-1,2**11]).encode('utf8').decode('latin1')
# Some non-BMP characters
a += u'\U00010000\U0010FFFF'.encode('utf8').decode('latin1')
print repr(a)
# Unicode codepoint sequences that resemble UTF-8 sequences.
p = re.compile(ur'''(?x)
\xF0[\x90-\xBF][\x80-\xBF]2 | # Valid 4-byte sequences
[\xF1-\xF3][\x80-\xBF]3 |
\xF4[\x80-\x8F][\x80-\xBF]2 |
\xE0[\xA0-\xBF][\x80-\xBF] | # Valid 3-byte sequences
[\xE1-\xEC][\x80-\xBF]2 |
\xED[\x80-\x9F][\x80-\xBF] |
[\xEE-\xEF][\x80-\xBF]2 |
[\xC2-\xDF][\x80-\xBF] # Valid 2-byte sequences
''')
def replace(m):
return m.group(0).encode('latin1').decode('utf8')
print
print repr(p.sub(replace,a))
###输出
你'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \xd0\xb5\xd0\xba !"#$%&'()*+,-./0123456789:;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz|~\x7f\x80\x81\x82\x83\x84\x85\ x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\ x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\ xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\ xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\ xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x7f\xc2 \x80\xdf\xbf\xe0\xa0\x80\xf0\x90\x80\x80\xf4\x8f\xbf\xbf'
你'\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \u0435\u043a !"#$%&'()*+,-./0123456789:;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz|~\x7f\x80\x81\x82\x83\x84\x85\ x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\ x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\ xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\ xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\ xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x7f\x80 \u07ff\u0800\U00010000\U0010ffff'
【讨论】:
【参考方案6】:我解决了
unicodeText.encode("utf-8").decode("unicode-escape").encode("latin1")
【讨论】:
以上是关于Unicode Python 字符串中的字节数的主要内容,如果未能解决你的问题,请参考以下文章
笨办法学python3代码练习ex23.py 字符串字节串字符编码