Python 2.7.6 将单个 "high"unicode 代码点一分为二

标签 python python-2.7 unicode

作为 MySQL 在遇到“高”(序号 >= 2^16)代码点时截断 unicode 字符串的变通方法,我一直在使用一个小的 Python 方法逐步遍历字符串(记住,字符串是序列),确实ord() 在字符上,并通过替换其他内容或完全删除代码点来抢占截断。这在许多使用 Python 2.7.3 的机器上都按预期工作(Ubuntu 12.04 LTS,一些 Centos 6,混合 32 位和 64 位 CPU,到目前为止没有关系)。

我注意到在 Python 2.7.6 安装中,这会中断。 Ascii 字符和“低”代码点(序号 < 2^16)的行为与以前一样。但是高代码点 (>= 2^16) 的行为非常奇怪。 Python2.7.6 似乎将它们分别视为两个 代码点。这是一个归结为基础知识的测试用例:

### "good" machine, Python2.7.3
$ uname -a && echo $LANG
Linux *** 3.2.0-60-virtual #91-Ubuntu SMP Wed Feb 19 04:13:28 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
en_US.UTF-8
$ python2.7
Python 2.7.3 (default, Feb 27 2014, 19:58:35) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> utest = u'a\u0395\U0001f30e'    # three chars: ascii, "low" codepoint, "high" codepoint
>>> utest.__class__
<type 'unicode'>
>>> len(utest), hash(utest)
(3, 1453079728409075183)
>>> list(utest)        # split into list of single chars
[u'a', u'\u0395', u'\U0001f30e']
>>> utest[2]   # trying to extract third char (high codepoint)
u'\U0001f30e'
>>> len(utest[2])
1
>>> "%x" % ord(utest[2])
'1f30e'

这是预期的行为。我用三个字符初始化一个 unicode 字符串。 Python 说它是三个字符,它可以很好地“寻址”第三个字符,返回单个预期的高代码点。如果我得到该代码点的序数,我会得到与原始转义序列中相同的数字。

现在是 Python 2.7.6

### "bad" machine, Python 2.7.6
$ uname -a && echo $LANG
Linux *** 2.6.32-431.5.1.el6.x86_64 #1 SMP Wed Feb 12 00:41:43 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
en_US.UTF-8
$ python2.7
Python 2.7.6 (default, Jan 29 2014, 20:05:36)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> utest = u'a\u0395\U0001f30e'
>>> utest.__class__
<type 'unicode'>
>>> len(utest), hash(utest)    # !!!
(4, -2836525916470507760)

第一个差异:Python 2.7.6 说 utest 的长度为 4。哈希值也不同。下一个惊喜:

>>> list(utest)                # !!!
[u'a', u'\u0395', u'\ud83c', u'\udf0e']

不仅长度表现得很奇怪,拆分成单个字符甚至更奇怪,因为高代码点的两个“一半”变成了两个没有明显数字关系的低代码点——至少对我来说——原始代码点。

通过序列索引寻址该代码点会出现相同的破损:

>>> utest[2]
u'\ud83c'

要获得原始的高代码点,我现在必须使用两个字符的切片:

>>> utest[2:4]
u'\U0001f30e'

但是,如果不是很明显的话,Python2.7.6 仍然在内部将其视为两个 代码点。我无法从中获得单个序数。

>>> len(utest[2:4])
2
>>> "%x" % ord(utest[2:4])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 2 found

那么,怎么办?我拥有的代码取决于 unicode 字符串中代码点的序数。如果一个代码点有时真的是两个代码点,我的序数就变得毫无意义,我的代码也无法执行它的功能。

这种行为有理由吗?是有意改动吗? 是否有一些配置旋钮可以让我在 Python 内部或系统级别恢复旧行为?猴子补丁?我不知道去哪里看。

不幸的是,我什至无法将其缩小到确切的次要版本。我们安装了很多 2.7.3、一些 2.7.1 和几个 2.7.6。没有 2.7.4/2.7.5。我只能说我在任何 2.7.3 安装中都从未遇到过这个问题。

额外信息:将字符串编码为 utf8 会在两个 Python 版本中产生完全相同的响应(相同的字符、相同的长度、相同的散列)。再次解码编码为 utf8 的代码仍然会立即返回方 block 1(即,这不是解决方法,行为在 unicode 空间中仍然存在差异)。

最佳答案

您正在体验所谓的“代理对”。这些只发生在 narrow builds python,其中代码点在内部存储为 UTF-16。您可以通过检查 sys.maxunicode(它将是 2**16 - 1)来确认您拥有哪个版本。

其他一些好的读物是 PEP 393 , 不幸的是,对于 python 3.3+,这使它停止了。

编辑:用谷歌搜索解决方法。 Full credit to @dan04 .

def code_points(text):
    import struct
    utf32 = text.encode('UTF-32LE')
    return struct.unpack('<{}I'.format(len(utf32) // 4), utf32)

>>> len(utest)
4
>>> len(code_points(utest))
3

如果你关心长度你可以做 len(utest.encode('UTF-32LE'))//4,但看起来你想要做更多,所以也许上面的功能是有帮助的。

关于Python 2.7.6 将单个 "high"unicode 代码点一分为二,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22665667/

相关文章:

python - 在Linux上通过USB连接时如何从HID识别键盘

python - 测试 Python 中获取输入的不同方法的性能

python - NLTK 中的 "ImportError: cannot import name StanfordNERTagger"

python - Python 装饰器做什么,它的代码在哪里?

python - 在同一台 PC 上管理两个版本的 Python

python - 当我们点击PyQt中的QToolButton时如何打开另一个表单?

sql - 如何查看 MySQL 数据库/表/列的字符集是什么?

python - 如何在 Python 中创建具有不同名称的多个文件

c++ - 在 Windows Mobile 上使用 WideCharToMultiByte

c++ - 如何在 Linux gcc 下确定 vswprintf 的缓冲区大小