讨论一个纠结的Django编码问题

2009-12-22 21:54

简介

本文简单记录了我在项目中遇到的一个Django编码问题. 通过Django源码分析, 我们找到了问题的根源, 并提出了解决问题的方法.

问题总结: 用WSGI方式使用Django, 请求的URL是GBK编码, 而且在视图函数中使用了request.REQUEST字典的话, 可能会有编码问题.

问题背景

问题的背景是这样的, 在我接手的报警项目中, 报警服务器提交到服务器的报警信息内容是GBK编码的, 例如下面的URL是一个典型的报警请求:

http://192.168.24.133/msggw.php?from_ip=1.2.3.4&project_name=blah&msg=blah+427%28%D0%A1%C3%B7%C9%B3%29+1.2.3.4+has+new+debug+log%2Ccheck%21%21%21%21%0ATue+Dec+22+08%3A01%3A18+2009%2822172096%29%28539088508%29%09shutdownOS%280%29

为了处理GBK编码, 我们要在视图函数里面设置request的编码, 例如:

def receiving_message(request):
request.encoding = "gbk"

这样, 在这个语句之后, 我们理论上应该不会再遇到编码问题. 但是不幸的是, 我还是发现有编码问题, 问题似乎来自我在视图函数的后半部分使用了request.REQUEST这个字典.

代码背后的故事

今天, 我在确定这个问题来自Django处理request后, 仔细读了下相关的源码. 在Django产生一个request对象到在视图函数里面使用request.GET/request.POST的具体细节包括:

由WSGI脚本生成了一个request对象.

相关代码在django/core/handlers/wsgi.py文件中, 生成的request对象是WSGIRequest的一个实例. 这个实例的初始化基本只是设置了request.META这个字典的内容. 接下来这个对象就被传给视图函数了.(不清楚中间具体还发生了什么事情, 哪天有机会再去仔细研究下...)

在视图函数里面使用GET/POST字典

这个过程看起来很简单, 实际上却比较复杂. 首先, 在WSGIRequest这个对象定义的结尾处, Django设置了使用GET/POST/REQUEST这三个字典的方法. 我们使用GET/POST字典实际上是通过执行这些内置的方法来实现的. 例如, 我们需要知道request.GET['msg']的内容, 此时, Django是通过_get_get方法来拿到GET字典的内容的:

def _get_get(self):
if not hasattr(self, '_get'):
# The WSGI spec says 'QUERY_STRING' may be absent.
self._get = http.QueryDict(self.environ.get('QUERY_STRING', ''), encoding=self._encoding)
return self._get

Django文档中说, Django在处理GET/POST时是很懒惰的, 如果没有明确要GET/POST中的内容, Django是不会浪费时间生成这两个字典的. 上面这个函数里面的if就是一个明确的证据. 如果没有_get属性, 则实例化一个QueryDict(代码在django/http/__init__.py中), 并将其作为_get的内容, 也即是GET的内容. 在实例化的过程中给出了两个参数, 一个是QUERY_STRING, 一个是_encoding. 如果你还没有在视图函数里面设置编码, 那么此时_encoding是默认的值(utf-8). 在QueryDict的__init__方法里面, 我们看到这样的语句:

self.encoding = encoding
for key, value in parse_qsl((query_string or ''), True): # keep_blank_values=True
self.appendlist(force_unicode(key, encoding, errors='replace'),
force_unicode(value, encoding, errors='replace'))

这一段代码负责生成GET字典的内容. 其中那个parse_qsl函数来自python默认的cgi模块, 作用是将QUERY_STRING分解为一个字典. 对于字典里面的每个值, Django使用force_unicode方法(代码在django/utils/encoding.py文件中)将其编码解码. 顺便说一句, 这个force_unicode函数会做一些诡异的事情, 例如将一个GBK字符串转码不成功且不论, 它会生成类似u'u68a6u5e7b:u0421xf7u0273'的字符串, 很乱很杂交.

上面说了这么多, 总结成一句话: Django处理视图函数时, 直到你明确要求GET字典中的键值时, 这个字典才会被生成, 生成字典时按照编码的配置做了编码转码等工作.

设置编码

我们在文章的一开头就给出了设置Django处理QUERY_STRING的编码的方法, 即直接指定request的encoding. 这句话看起来简单, 实际上却在背后做了一些额外的工作, 这是因为在Django对HttpRequest对象(代码在django/http/__init__.py中)的定义中有这么一句:

encoding = property(_get_encoding, _set_encoding)

即, 要取得编码的值, 实际上是调用了_get_encoding方法, 要设置编码的值, 实际上调用了_set_encoding方法. 我们关注的是后一种方法, 它的定义为:

def _set_encoding(self, val):
"""
        Sets the encoding used for GET/POST accesses. If the GET or POST
        dictionary has already been created, it is removed and recreated on the
        next access (so that it is decoded correctly).
        """
self._encoding = val
if hasattr(self, '_get'):
del self._get
if hasattr(self, '_post'):
del self._post

很聪明很合乎逻辑的做法, 如果用户设置了编码, 那么我们把_get和_post都删除掉, 这样也就对应地删除了GET/POST这两个字典. 下次用户再想要拿这两个字典里面的内容时, 再用新的编码再生成一次GET/POST就是了.

request.REQUEST字典

这个字典是模仿php中的对应快捷方式而建立的, 实际上只是将GET和POST合并起来, 代码在django/core/handlers/wsgi.py的WSGIRequest定义中:

def _get_request(self):
if not hasattr(self, '_request'):
self._request = datastructures.MergeDict(self.POST, self.GET)
return self._request

哪段代码有问题?

上面洋洋洒洒一大篇描述了Django处理GET/POST字典的原理, 我接下来要说的是这种处理过程中有哪些问题. 我们看看下面这段视图函数中的代码:

def receiving_message(request):
msg1 = request.REQUEST['msg']
request.encoding = "gbk"
msg2 = request.REQUEST['msg']

首先, 第一次拿到的msg1显然是乱码, 我们还没设置处理URL的编码, 因此URL没有被正确地解码. 但是不幸地是, 第二次拿到的msg2仍然是乱码. 这是因为设置编码的过程中仅仅删除了可能存在的GET和POST字典, 而没有删除REQUEST字典. 因此第二次我们从REQUEST字典里取东西时, 所拿到的结果仍然是按照不正确的编码解出来的结果.

这个问题出在哪儿? 虽然看起来应该认为上面的_set_encoding方法是有问题的, 应该额外加两行, 删除self._request才对. 不过问题不是那么简单的, HttpRequest对象里面根本没有request.REQUEST这个东东. 这个东东是在WSGIRequest中定义的. 我们应该在WSGIRequest的定义中override掉HttpRequest的默认_set_encoding方法才对.

再顺口提一句, modpython里面也没有override这个方法, 因此理论上也存在同样的问题.

问题回顾

上面说了很多, 但是为什么只有在很偶然的情况下才会出现编码问题呢? 在大多数情况下, request.REQUEST字典都没有生成. 因此在视图函数中, 设置编码后从request.REQUEST字典中拿东西出来是没有问题的. 但是在一定几率下(我只能这么说了...), 后面的视图函数代码中从request.REQUEST字典里拿到的数据可能是有编码问题的.

我在前面曾经推测这个问题的出现可能和request.REQUEST这个字典相关, 不过我在测试中发现, 即使这个字典没有出现, 也可能会出现编码问题. 哎哎, 无论如何, 这个是一个诡异的问题...

关于这个一定几率, 根据我的经验, 不是指某一段时间可以, 某一段时间不行; 而是某一次重启后可以, 某一次重启后不行... 也许这个问题在更深层次上和会话相关, 不过暂时还是不要这么钻牛角尖的好, 因为我们有一个比较放心的解决方案.

解决方案

比较放心也比较累赘的解决方案是在设置编码后, 将GET/POST/REQUEST都删掉:

def receiving_message(request):
request.encoding = "gbk"
if hasattr(request, '_request'):
del request._request
if hasattr(request, '_get'):
del request._get
if hasattr(request, '_post'):
del request._post

这样我想, 应该是不会再有问题了.