如何在Python中获得可靠的Unicode字符计数?
Google App Engine使用的是Python 2.5.2,似乎启用了UCS4。不过,GAE的数据存储内部使用的是UTF-8格式。所以,如果你把u'\ud834\udd0c'(长度为2)存储到数据存储中,当你取出来的时候,你会得到'\U0001d10c'(长度为1)。我想计算字符串中unicode字符的数量,确保在存储前后得到的结果是一样的。因此,我想在接收到字符串后,尽快把它规范化(从u'\ud834\udd0c'变成'\U0001d10c'),然后再计算它的长度并存入数据存储。我知道我可以先把它编码成UTF-8再解码回来,但有没有更简单或更高效的方法呢?
2 个回答
不幸的是,CPython解释器在3.3版本之前的行为取决于它是使用“窄”还是“宽”的Unicode支持来构建的。因此,同样的代码,比如调用len
,在不同版本的标准解释器中可能会得到不同的结果。想了解更多例子,可以查看这个问题。
所谓“窄”和“宽”的区别在于,“窄”解释器内部使用16位的代码单元(UCS-2),而“宽”解释器则使用32位的代码单元(UCS-4)。对于代码点U+10000及以上(超出基本多语言平面),在“窄”解释器中,len
的值是2,因为需要两个UCS-2代码单元来表示它们(使用代理),而len
就是用来测量这个的。在“宽”版本中,对于非BMP代码点,只需要一个UCS-4代码单元,所以在这些版本中,len
的值是1。
我已经确认下面的代码可以处理所有unicode
字符串,无论它们是否包含代理对,并且在CPython 2.7的窄和宽版本中都能正常工作。(可以说,在宽解释器中指定像u'\ud83d\udc4d'
这样的字符串,实际上是希望完整表示一个代理代码点,而不是部分字符代码单元,因此这并不一定是需要纠正的错误,但我在这里不考虑这个。这是一个边缘案例,通常不是期望的使用情况。)
下面使用的@invoke
技巧是一种避免重复计算的方法,而不会向模块的__dict__
中添加任何内容。
invoke = lambda f: f() # trick taken from AJAX frameworks
@invoke
def codepoint_count():
testlength = len(u'\U00010000') # pre-compute once
assert (testlength == 1) or (testlength == 2)
if testlength == 1:
def closure(data): # count function for "wide" interpreter
u'returns the number of Unicode code points in a unicode string'
return len(data.encode('UTF-16BE').decode('UTF-16BE'))
else:
def is_surrogate(c):
ordc = ord(c)
return (ordc >= 55296) and (ordc < 56320)
def closure(data): # count function for "narrow" interpreter
u'returns the number of Unicode code points in a unicode string'
return len(data) - len(filter(is_surrogate, data))
return closure
assert codepoint_count(u'hello \U0001f44d') == 7
assert codepoint_count(u'hello \ud83d\udc4d') == 7
我知道我可以先把它编码成UTF-8,然后再解码回来。
没错,这通常是解决“在UCS-4字符串中有UTF-16代理”的问题的办法。不过,正如机械蜗牛所说,这种输入其实是有问题的,最好还是去修复产生它的那个地方。
有没有更简单或更有效的方法呢?
嗯……你可以用正则表达式手动处理,比如:
re.sub(
u'([\uD800-\uDBFF])([\uDC00-\uDFFF])',
lambda m: unichr((ord(m.group(1))-0xD800<<10)+ord(m.group(2))-0xDC00+0x10000),
s
)
当然,这样做并不简单……我也怀疑这样做是否真的更有效!