如何在Python中解码已在JavaScript中编码的unicode字符串?
平台:App Engine
框架:webapp / CGI / WSGI
在我的客户端(JavaScript)上,我通过把一个网址和一个unicode字符串拼接在一起,构造了一个新的网址:
http://www.foo.com/地震
然后我调用了encodeURI来获取:
http://www.foo.com/%E5%9C%B0%E9%9C%87
接着我把这个结果放进了一个HTML表单的值里。
这个表单会提交到PayPal,而我在那里的设置是使用'utf-8'编码。
PayPal随后会通过IPN(即时支付通知)对这个网址发起一个POST请求。
在我的服务器端,WSGIApplication尝试用我定义的正则表达式提取这个unicode字符串:
(r'/paypal-listener/(.+?)', c.PayPalIPNListener)
我会尝试通过调用
query = unquote_plus(query).decode('utf-8')
(或者其他变体)来解码它,但我会遇到错误:
/paypal-listener/%E5%9C%B0%E9%9C%87
...(省略)...
'ascii' 编码无法编码位置 0-1 的字符:序号不在范围(128)内
(第一行是请求的网址)
当我检查query
的长度时,Python告诉我它的长度是18,这让我觉得'%E5%9C%B0%E9%9C%87'并没有被编码过。
6 个回答
urllib.unquote()
这个函数在处理unicode字符串时不太好用。在这种情况下,应该传入字节字符串,然后再解码成unicode。
这样做是有效的:
>>> u = u'http://www.foo.com/%E5%9C%B0%E9%9C%87'
>>> print urllib.unquote(u.encode('ascii'))
http://www.foo.com/地震
>>> print urllib.unquote(u.encode('ascii')).decode('utf-8')
http://www.foo.com/地震
但是这样做就不行(还可以查看 urllib.unquote 用 Latin-1 解码百分号转义字符):
>>> print urllib.unquote(u)
http://www.foo.com/å °é
已经是unicode的字符串再解码是没用的:
>>> print urllib.unquote(u).decode('utf-8')
Traceback (most recent call last):
File "<input>", line 1, in <module>
File ".../lib/python2.6/encodings/utf_8.py", line
16, in decode
return codecs.utf_8_decode(input, errors, True)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 19-24: o
rdinal not in range(128)
假设这个HTML页面是用utf-8编码的,如果框架能够处理URL中的百分号编码,那么只需要简单地用 path.decode('utf-8')
就可以了。
如果框架不能处理,你可以使用:
- 如果URL是
http://www.foo.com/地震
,可以用urllib.unquote(path).decode('utf-8')
- 如果你是在说通过AJAX或者HTML的
<form>
发送的参数,可以用urllib.unquote_plus(path).decode('utf-8')
(详细信息可以查看 这里)
编辑:如果你还有问题,请提供以下信息,这样我们可以更好地帮助你解决问题:
- 你在Google App Engine中使用的网络框架是什么,比如Django、WebOb、CGI等
- 你是如何在应用中获取URL的(如果可以,请附上简短的代码示例)
- 当你将
http://www.foo.com/地震
作为URL添加时的repr(url)
是什么 尝试将这个作为URL添加,并发布
repr(url)
,这样我们可以确保服务器没有将字符解码为 latin-1 或 Windows-1252:http://foo.com/¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
编辑 2:因为这是一个实际的URL(而不是查询部分,比如不是 http://www.foo.com/?param=%E5%9C%B0%E9%9C%87
),所以使用
query = unquote(query.encode('ascii')).decode('utf-8')
应该是安全的。不过如果你是在解码实际的URL,应该用 unquote
而不是 unquote_plus
。我不知道为什么Google会把URL作为 unicode
对象传递,但我怀疑传递给应用的实际URL不会用 windows-1252
等方式解码。我有点担心,因为我以为它在错误地解码 查询(也就是传递给 GET
或 POST
的参数),但看起来并不是这样。
原则上,这个方法应该是可行的:
>>> urllib.unquote_plus('http://www.foo.com/%E5%9C%B0%E9%9C%87').decode('utf-8')
u'http://www.foo.com/\u5730\u9707'
不过,有几点需要注意:
unquote_plus
是用来处理application/x-form-www-urlencoded
数据的,比如通过 POST 提交的表单和查询字符串参数。在 URL 的路径部分,+
表示的是一个字面上的加号,而不是空格,所以在这里你应该使用普通的unquote
。一般来说,不应该对整个 URL 进行解码。因为 URL 中某些部分的特殊字符会丢失。你应该把 URL 拆分成几个部分,获取你感兴趣的单一路径组件(比如
%E5%9C%B0%E9%9C%87
),然后再进行解码。
(如果你想把一个 URI 完全转换成 IRI,比如 http://www.foo.com/地震
,事情会复杂一些。只有 IRI 的路径/查询/片段部分是用 UTF-8 编码的;而域名则是通过一种叫做‘Punycode’的奇特 IDN 方案在 Unicode 和字节之间进行映射。)
这个在我的 Python 服务器端接收到了。
你的服务器端具体是什么?是服务器、网关还是框架?你是怎么获取 url
变量的?
你似乎遇到了一个 UnicodeEncodeError
,这意味着在传给 unquote
函数的 输入 中出现了意外的非 ASCII 字符,这根本不是解码的问题。所以我建议你检查一下,可能在某个地方已经把 URL 的路径部分解码成了某种 Unicode 字符串。让我们看看那个变量的 repr
!
不幸的是,很多网络服务器在处理 URL 路径部分的 Unicode 时存在严重问题,这不仅在 Python 中如此,普遍都是这样。
主要问题是 PATH_INFO
变量根据 CGI 规范(后来又被 WSGI 规范)定义为预解码的。这是个可怕的错误,部分原因是上面提到的第(1)点,这意味着你无法在路径部分获取 %2F
,更严重的是,解码 %
序列会引入一个 Unicode 解码步骤,而这个步骤是应用程序无法控制的。不同的服务器环境在处理 URL 中的非 ASCII %
转义时差异很大,通常很难重现浏览器传入的确切字节序列。
IIS 特别麻烦,因为它默认会尝试将 URL 路径解析为 UTF-8,如果路径不是有效的 UTF-8 序列,它会退回到极不可靠的系统默认代码页(例如,在西方 Windows 安装中是 cp1252),但 不会告诉你。这样的话,你在尝试从环境变量中读取 PATH_INFO
中的任何非 ASCII 字符时,可能会遇到相当严重的问题,因为 Windows 的环境变量是 Unicode,但在 Python 2 和许多其他语言中是以系统代码页的字节形式访问的。
Apache 通过提供一个额外的非标准环境变量 REQUEST_URI
来缓解这个问题,这个变量保存了浏览器提交的原始、完全未解码的 URL,这样处理起来就简单多了。不过,如果你使用了 URL 重写或错误文档,那么这个未映射的 URL 可能和你想象中的不一样。
一些框架试图修复这些问题,效果各不相同。WSGI 1.1 预计会尝试标准化这个问题,但在此之前,我们面临的实际情况是,Unicode 路径并不总是能正常工作,而在一个服务器上尝试修复的黑科技通常会在另一个服务器上失效。
你总是可以使用 URL 重写将 Unicode 路径转换为 Unicode 查询参数。因为 QUERY_STRING
环境变量在应用程序外部并没有被解码,所以处理起来要简单得多。