如何在Python中获得可靠的Unicode字符计数?

8 投票
2 回答
2137 浏览
提问于 2025-04-16 22:46

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 个回答

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
4

我知道我可以先把它编码成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
)

当然,这样做并不简单……我也怀疑这样做是否真的更有效!

撰写回答