Sunday, December 27, 2009

python默认对待unicode的方式

我不是故意去找问题的。 但是我很不喜欢问题不请自来。谁会愿意把青春花在这个鸟问题上面.

不过话也说回来, 我又不玩游戏, 不下棋. 解决这个问题就当是一场“抓迷藏游戏”吧。

这里, 主要涉及到两个编码问题:
1. 文件系统使用编码方式. 这个值由 sys.getfilesystemencoding() 取得
2. python的unicode函数使用的默认解码方式. 这个值由 sys.getdefaultencoding() 取得.

世界的编码是非常之烦的一类事, 一看到locale -m的输出結果我就没有胃口了. 还是集中解决UTF-8, unicode, ascii之间的问题就够用了

locale的重要性

可以说, locale对程序的行为影响是很大的. linux下的libc提供了机制方便处理这种问题. 举个例子:
jessinio@jessinio-laptop:/$ export LC_ALL='POSIX'
jessinio@jessinio-laptop:/$ locale
LANG=
LC_CTYPE="POSIX"
LC_NUMERIC="POSIX"
LC_TIME="POSIX"
LC_COLLATE="POSIX"
LC_MONETARY="POSIX"
LC_MESSAGES="POSIX"
LC_PAPER="POSIX"
LC_NAME="POSIX"
LC_ADDRESS="POSIX"
LC_TELEPHONE="POSIX"
LC_MEASUREMENT="POSIX"
LC_IDENTIFICATION="POSIX"
LC_ALL=POSIX
jessinio@jessinio-laptop:/$ bash

这时的bash环境是无法使用中文的. 就算你copy过去它也不买单, locale影响到程序对字节流的处理方式(这水深, 主要是于C函数上, 这里先不钻进去).

事件的源由


前段时间把时区的问题搞清楚了。 今夜也跑来一个i18n问题。 只能发挥宅男的本色。碰个杀一个。

这是一句很平常的語句: os.path.exists( path ), 在哪里使用都很正常。但是在mod_wsgi中使用就狗日的有问题:

File "/home/jessinio/data/workspace/project/home/views.py" in index
  32.     os.path.exists(path)
File "/usr/lib/python2.6/genericpath.py" in exists
  18.         st = os.stat(path)

Exception Type: UnicodeEncodeError at /
Exception Value: ('ascii', u'/tmp/\u6881\u5e86\u559c', 5, 8, 'ordinal not in range(128)')

os.stat出问题。为什么在一些地方python解释器可以解码, 但是在mod_wsgi中又无法解码?

开始关注于C语言的i18n的处理方式。环境变量则是问题的入手点. 下面看一个证据:
python文件内容:
jessinio@jessinio-laptop:~$ cat /tmp/en.py
# coding: utf-8
import os

s = u'/tmp/梁庆喜'
os.path.exists(s)

# 下面证明了LANG环境变量的作用:
jessinio@jessinio-laptop:~$ env|grep LANG
LANG=en_US.UTF-8
GDM_LANG=en_US.UTF-8
jessinio@jessinio-laptop:~$ python /tmp/en.py

jessinio@jessinio-laptop:~$ export LANG=zh_CN.GBK
jessinio@jessinio-laptop:~$ python /tmp/en.py
Traceback (most recent call last):
  File "/tmp/en.py", line 6, in <module>
    os.path.exists(s)
  File "/usr/lib/python2.6/genericpath.py", line 18, in exists
    st = os.stat(path)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 5-7: ordinal not in range(128)

先看看os.stat到底做了什么, 在Modules/posixmodule.c文件里的posix_do_stat函数这样写:

    if (!PyArg_ParseTuple(args, format,
                          Py_FileSystemDefaultEncoding, &path))
        return NULL;
    pathfree = path;

    Py_BEGIN_ALLOW_THREADS
    res = (*statfunc)(path, &st);
    Py_END_ALLOW_THREADS


那么Py_FileSystemDefaultEncoding哪里来? 下面的内容来自Python/pythonrun.c. 这个文件是python启动时使用的. 这里有设置了Py_FileSystemDefaultEncoding

#if defined(Py_USING_UNICODE) && defined(HAVE_LANGINFO_H) && defined(CODESET)
    /* On Unix, set the file system encoding according to the
       user's preference, if the CODESET names a well-known
       Python codec, and Py_FileSystemDefaultEncoding isn't
       initialized by other means. Also set the encoding of
       stdin and stdout if these are terminals, unless overridden.  */

    if (!overridden || !Py_FileSystemDefaultEncoding) {
        saved_locale = strdup(setlocale(LC_CTYPE, NULL));
        setlocale(LC_CTYPE, "");
        loc_codeset = nl_langinfo(CODESET);
        if (loc_codeset && *loc_codeset) {
            PyObject *enc = PyCodec_Encoder(loc_codeset);
            if (enc) {
                loc_codeset = strdup(loc_codeset);
                Py_DECREF(enc);
            } else {
                loc_codeset = NULL;
                PyErr_Clear();
            }
        } else
            loc_codeset = NULL;
        setlocale(LC_CTYPE, saved_locale);
        free(saved_locale);

        if (!overridden) {
            codeset = icodeset = loc_codeset;
            free_codeset = 1;
        }

        /* Initialize Py_FileSystemDefaultEncoding from
           locale even if PYTHONIOENCODING is set. */
        if (!Py_FileSystemDefaultEncoding) {
            Py_FileSystemDefaultEncoding = loc_codeset;
            if (!overridden)
                free_codeset = 0;
        }
    }

Py_FileSystemDefaultEncoding 的值在python环境下也是可以取得的: sys.getfilesystemencoding()

不过, 没有set函数, python里也没有C代码提供了修改的方法. 也就是说: python启动后这个值是被固定.( 有点郁闷~~~ )

python提供了一个叫locale的module, 类似C的locale处理函数(其实就是C的locale函数封装), 但是:
* 这个库无法修改Py_FileSystemDefaultEncoding. 不要希望在在启动python后通过这个库的函数修改Py_FileSystemDefaultEncoding
* 也就是说, locale无法修改python对待file system encoding的处理方法.

BTW:: 本人试图在python启动后修改这个值做了N个努力, 我日~~~~

file system encoding的作用

文件系统里存在的是文字的交换码. 比如一个文件的路径在文件系统内是utf-8方式存放的. 如:
In [34]: os.listdir('/tmp')
Out[34]:
['\xe6\xa2\x81\xe5\xba\x86\xe5\x96\x9c',]

当试图在python里使用一个unicode的字符串对象去对应文件系统里的资源时, python就会使用file system encoding的方式去编码, 如:
In [41]: a = unicode('/tmp/梁庆喜', 'utf-8')
In [42]: a
Out[42]: u'/tmp/\u6881\u5e86\u559c'
In [43]: os.path.exists(a)
Out[43]: True

python对待unicode的方法和下面的方式一致:
In [36]: a = '/tmp/梁庆喜'
In [37]: a
Out[37]: '/tmp/\xe6\xa2\x81\xe5\xba\x86\xe5\x96\x9c'
In [38]: os.path.exists(a)
Out[38]: True

如果os.path.exist的参数是unicode的话, 它将会使用file system encoding的方式去对unicode编码. 然后使用系统的API.

default encoding的作用

下面使用一个例子就可以看到这个问题:
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> a = '梁庆喜'
>>> unicode(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

unicode函数在不提供第二个参数时, 就要使用default encoding的解码方式.

平时启动python的方式python会把setdefaultencoding方法从sys中删除.

我们可以重载这个编码方式: 使用python的-S参数启动python:
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> sys.setdefaultencoding('utf-8')
>>> sys.getdefaultencoding()
'utf-8'
>>> a = '梁庆喜'
>>> unicode(a)
u'\u6881\u5e86\u559c'

如果不想启动时使用-S参数, 也可以修改/usr/lib/python2.6/site.py里的setencoding函数

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.