让GAE支持中文URL处理

标签:Google App Engine, Python

目前互联网上的网址大多都是英文的,对中国人并不友好,但实际上浏览器和服务器都能处理中文,所以下面的网址是完全OK的:
http://tieba.baidu.com/智代

下面就来看看GAE中应该如何实现中文URL。嗯,以下全部基于Python,Java似乎不行(其实是我的系统装不了JDK,没法测试)。

GAE中处理URL有2种方式,一是在app.yaml里指定路径对应的文件(夹),二是在Python脚本里处理。由于前者不支持中文路径,所以直接看后者。

首先定义app.yaml,由于路径包含中文,所以必须将中文部分用通配符来表示。简单起见,这里假设只有一个脚本:
- url: .*
  script: main.py
OK,接着就开始写main.py了。

这里仍然有2种方式,一是直接在URL映射中处理,二是在RequestHandler中处理。
由于URL是经过转义的,直接在URL映射中处理会降低可读性,而且不得不为每个网址都写上一条映射,所以选择第二种。

于是这样一个简单的例子就出来了:
# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from urllib import unquote # 实际上urllib2里也有这个函数,并且urllib.unquote is urllib2.unquote为True

class UrlHandler(webapp.RequestHandler):
  def get(self, path):
    self.response.out.write(unquote(path)) # 注意要进行转义,否则输出的都是%开头的乱码

# 简单起见,我只写了一条URL映射,注意(.*)会被传递到UrlHandler.get的path参数中
application = webapp.WSGIApplication([('/(.*)', UrlHandler),])

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()
现在这个例子在绝大多数的浏览器上都能正常使用了。
如果你在GAE的本地开发服务器上测试的话,访问http://localhost:8080/智代就会显示智代了。

不过很可惜,IE有个很无语的设置:工具-Internet选项-高级-总是以UTF-8发送URL。如果这个勾没打上的话,你访问上面的网址,应该会看到一个问号,原因是解码错误。
于是继续来研究一下出错的原因。

实际上浏览器在访问http://localhost:8080/智代这个网址时,由于其中包含了非英文字符,所以会将其进行转义。在大多数浏览器中采用的是UTF-8编码,于是实际上请求的就是http://localhost:8080/%E6%99%BA%E4%BB%A3这个网址了。
于是上例中的path就被赋值为'%E6%99%BA%E4%BB%A3',而unquote(path)则将其转换为UTF-8编码的'智代',再由于我设置了# -*- coding: utf-8 -*-,于是可以正常输出UTF-8编码的字符串。

但如果在IE中取消了“总是以UTF-8发送URL”这个选项,那么IE就会以本地所设置的语言来进行编码。在Windows XP简体中文版下,默认的编码为CP936,(在Python中)它实际上是gbk的别名。所以此时发送的网址就是http://localhost:8080/%D6%C7%B4%FA
'%D6%C7%B4%FA'是无法以UTF-8来解码的,于是就输出了问号。

知道这点就简单了,我们只需要将其转换为正确的编码即可,一个简单的实现如下(由于其余部分与上面相同,所以我只列出get方法):
def get(self, path):
  unquotedPath = unquote(path)
  try:
    path = unicode(unquotedPath, 'utf8')
  except:
    try:
      path = unicode(unquotedPath, 'gbk')
    except:
      try:
        path = unicode(unquotedPath, 'big5')
      except:
        try:
          path = unicode(unquotedPath, 'shiftjis')
        except:
          try:
            path = unicode(unquotedPath, 'korean')
          except:
            pass
  self.response.out.write(path)
如此一来,GAE会尝试使用几种编码来进行解码,当然你也可以不支持日韩…
于是中国大陆应该都能正确访问了,不信你可以试试。

不过这个代码仍然有缺陷,因为有些字符代码可能在多种编码中都可以正常解码,而我却优先使用了gbk编码,于是仍可能会变成乱码。
于是我想到了一个强大的第三方库:chardet。目前最新版本为1.0.1。

它的使用很简单,把chardet文件夹解压出来,就能当成包来用了:
>>> a = '智代'
>>> b = u'智代'.encode('utf8')
>>> c = u'智代'.encode('shiftjis')
>>> d = u'智代'.encode('big5')
>>> e = u'智代'.encode('korean')
>>> import chardet
>>> chardet.detect(a)
{'confidence': 0.98999999999999999, 'encoding': 'GB2312'}
>>> chardet.detect(b)
{'confidence': 0.75249999999999995, 'encoding': 'utf-8'}
>>> chardet.detect(c)
{'confidence': 0.5, 'encoding': 'windows-1252'}
>>> chardet.detect(d)
{'confidence': 0.98999999999999999, 'encoding': 'Big5'}
>>> chardet.detect(e)
{'confidence': 0.98999999999999999, 'encoding': 'EUC-TW'}
>>> f = u'智代アフター'.encode('shiftjis')
>>> chardet.detect(f)
{'confidence': 0.98999999999999999, 'encoding': 'SHIFT_JIS'}
注意日文的识别有错误,不过可信度也只给了0.5,所以很容易知道有问题。实际上文档中有说到windows-1252的问题,实际上它将无法确信的编码都设为这个了。
如果在其中包含假名的话,就很准确地识别为日文编码了。
但韩文的识别则非常囧,居然识别为台湾了,而且给出很高的置信度…

鉴于chardet对日文、韩文编码的识别存在问题(不过也不能怪它,因为我没用到日文和韩文字符),所以还是使用最初的实现算了。可是每次要支持中文URL时都得写这一大串代码也太累了,所以我继续用Decorator将它封装一下。

于是main.py就变成这样了:
# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from urllib import unquote

def handleMultibyteUrl(func):
  def handler(self, *paths):
    if not paths: # 没有路径就不用转义了
      return func(self)

    newPath = []

    for path in paths: # 如果有多个参数的话,将其全部转义
      unquotedPath = unquote(path)
      try:
        path = unicode(unquotedPath, 'utf8')
      except:
        try:
          path = unicode(unquotedPath, 'gbk')
        except:
          try:
            path = unicode(unquotedPath, 'big5')
          except:
            try:
              path = unicode(unquotedPath, 'shiftjis')
            except:
              try:
                path = unicode(unquotedPath, 'korean')
              except:
                pass
      newPath.append(path)
    return func(self, *newPath)

  return handler


class UrlHandler(webapp.RequestHandler):
  @handleMultibyteUrl  #注意这里,我们的方法只需要加上这一句就能处理中文了
  def get(self, path):
    self.response.out.write(path)


class UrlHandler2(webapp.RequestHandler):
  @handleMultibyteUrl
  def get(self, path1, path2):
    self.response.out.write(path1 + path2)

application = webapp.WSGIApplication([('/(.*)/(.*)', UrlHandler2),
                                      ('/(.*)', UrlHandler)])

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()
现在这个handleMultibyteUrl便能神奇地帮助你完成解析任务了,你可以试试http://localhost:8080/智代最高!http://localhost:8080/智代/最高!这2个URL,它们都会显示智代最高!

如果看不懂这个handleMultibyteUrl究竟完成了什么,你可以看看《在Python中实现Decorator模式》这篇帖,实际上我也刚接触它而已,不过用起来就觉得很优雅。

最后给个提示:最好是把handleMultibyteUrl单独放在一个py文件里,需要时就import进来。

更新一下:
在以前的版本,GAE服务器会一律以UTF-8来解析,所以在处理前就已经变成\ufffd了,没法还原;并且self.request.path取到的值也是如此。
但1.2.5以后已不再做这种变化,并可以通过os.environ['PATH_INFO']和os.environ['QUERY_STRING']获取。但需要正确的编码格式才能正确显示,否则变为乱码。

5条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?