TOC

Python UCS 相关问题

老项目中有时可以看到报错:unichr() arg not in range(0x10000) (narrow Python build)

Traceback (most recent call last):
  File "/tmp/parser.py", line 107, in _inner
    payload = HTML.unescape(payload)
  File "/usr/local/lib/python2.7/HTMLParser.py", line 390, in unescape
    return re.sub(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));", replaceEntities, s)
  File "/usr/local/lib/python2.7/re.py", line 151, in sub
    return _compile(pattern, flags).sub(repl, string, count)
  File "/usr/local/lib/python2.7/HTMLParser.py", line 376, in replaceEntities
    return unichr(c)
ValueError: unichr() arg not in range(0x10000) (narrow Python build)

经过一番调研之后,发现是和 Python 内部 Unicode 编码有关。

问题环境上(Python 2.7.15):

>>> import sys
>>> sys.maxunicode
65535

另一套环境(2.7.18):

>>> import sys
>>> sys.maxunicode
1114111

先理一理 Unicode 与 UCS 的关系

UCS 是 ISO 的国际标准(ISO/IEC 10646),全名 Universal Character Set,即通用字符集。
Unicode 是 IT 企业组成的 Unicode 联盟制定的标准。
两套标准目的都是为了统一各种字符编码,虽然独立发布,但是相互协同合作,保持了一致。
只是 Unicode 名头更响一些。

从 Unicode 2.0 开始,Unicode 采用了与 ISO 10646-1 相同的字库和字码;ISO 也承诺,ISO 10646 将不会替超出 U+10FFFF 的 UCS-4 编码赋值,以使得两者保持一致。

我的理解:

  1. ISO 和 Unicode 联盟共同维护了一套字符集,每个字对应到一个码位(Code Point),比如 “” 字的码位为 6211
  2. ISO 这边叫做 UCS,Unicode 联盟这边叫做 Unicode
  3. 编码方案:
    1. UCS 对应 UCS-2 和 UCS-4 两套字符编码
    2. Unicode 对应 UTF 系列编码方案,常见的有 UTF-8、UTF-16、UTF-32 三种。
      最流行的是 UTF-8,主要是对 ASCII 兼容的缘故(美国在 IT 技术领先地位的体现)。
  4. 不止字符集相同,在编码方面:
    1. UTF-16 完全覆盖 UCS-2,并采用变长的方式可以覆盖所有 Unicode 字符。
      换句话说,UCS-2 就是 UTF-16 中两字节部分字符。
    2. UTF-32 和 UCS-4 都是固定长度编码,两者完全相同。

再说回来

  65535 ->   0xFFFF
1114111 -> 0x10FFFF
  • 65535 对应的就是两字节编码方案
  • 1114111 对应的就是 4 字节编码方案
    Unicode 规范使用的最大码位为 0x10FFFF,按他们的话来说就是一共使用 17 个平面(Plane),每个平面可容纳 2^16 个码位(Code Point)。

Python 2.x 编译时,可以通过参数 --enable-unicode=ucs2|ucs4 指定使用 UCS-2 还是 UCS-4,默认是 UCS-2。
Python3 不支持这个参数,直接使用的 UCS-4。

PS:下载 CentOS Python 2 源码确认了,CentOS 官方提供的 Python 编译时带上了 ucs4,不会有这个问题。

解决办法

$ ./configure --help | grep -i ucs
  --enable-unicode[=ucs[24]]

重新编译 Python,./configure 参数加上 --enable-unicode=ucs4 就好了。

参考资料与拓展阅读