Python请求中的Unicode相关问题

5
首先请注意,u'\xc3\xa8' 是 Python 2 中具有 2 个代码点的 Unicode 字符串,即 è。接下来请注意,'\xc3\xa8' 是 Python 2 中的字节字符串,表示字符 è 的 UTF8 编码。因此,尽管 u'\xc3\xa8''\xc3\xa8' 看起来非常相似,但它们是两种非常不同的东西。
现在,如果我们在浏览器中尝试访问 https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl,一切都应该顺利。
如果我在 ipython 会话中定义:
unicode_url = u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'

然后我可以打印它并查看与浏览器URL栏中输入的内容相同,太好了。现在让我们试着使用Python请求获取它。
首先,我天真地尝试将Unicode URL直接传递给requests:requests.get(unicode_url)。不行,404错误,没问题,URL应该进行编码,所以我尝试了requests.get(unicode_url.encode('utf8'))。还是404错误。没关系,也许我需要对URL进行编码,所以我尝试了requests.get(urllib.quote(unicode_url.encode('utf8')))... 它完全不喜欢那样做。
但是,回想一下我在开头提到的Unicode和字节字符串对象之间的相似之处,我也尝试过:
  requests.get('http://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl')

令我惊讶的是它有效并返回成功的200。

requests在这里做了什么?

编辑:作为另一个实验(这次是在Scrapy shell中)

   from scrapy.http import Request
   unicode_url = u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'
   fetch(Request(unicode_url))

绝对没有问题!那么为什么Scrapy和浏览器可以毫无问题地处理它,但是python-requests不能呢?为什么替代url在python-requests中可以工作,但在浏览器或Scrapy中却不行。

Latin1与UTF8

还有一个事实是

print unicode_url.encode('utf8').decode('latin1')
u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'

一般来说,我认为对于仅限于拉丁字符的Unicode字符而言,如果您有一个Unicode字符串,例如u'\xe8',那么您可以通过将其编码为Latin1将其转换为相同格式的字节字符串,即u'è'=u'\xe8'u'\xe8'.encode('latin1') = '\xe8'(右侧的对象是使用与表示è的Unicode代码点相同的形式在Latin1中进行编码的字节字符串)。
In [95]: print u'è'.encode('utf8').decode('latin1')
è

并且同样地,
In [94]: print u'è'.encode('latin1').decode('utf8')
è

我在想罪犯会不会是

def prepare_url(self, url, params):
    """Prepares the given HTTP URL."""
    #: Accept objects that have string representations.
    #: We're unable to blindly call unicode/str functions
    #: as this will include the bytestring indicator (b'')
    #: on python 3.x.
    #: https://github.com/kennethreitz/requests/pull/2238
    if isinstance(url, bytes):
        url = url.decode('utf8')
    else:
        url = unicode(url) if is_py2 else str(url)

来源于requests/models.py文件。


你的文件前两行有 # coding: utf-8 这一行吗? - tdelaney
这是 Python 2.7 吗?你能添加 python-2.7(或类似的)标签吗?然后迅速转移到 Python 3,因为那里已经修复了 Unicode 支持问题?(微笑) - tdelaney
@tdelaney 这是在一个 IPython shell 中进行的,所以我假设 'utf-8' 是默认编码。我也添加了标签。 - fpghost
我不使用ipython shell,所以无法详细说明确切的机制,但看起来您已经将utf-8字节作为单独的字符,并且在shell中它看起来正确,因为它也是utf-8编码的。进行一个较小的测试,len(u"premièr")是7还是8? - tdelaney
@tdelaney 这是7。 - fpghost
你正在毁掉我最好的理论! - tdelaney
2个回答

0
问题在于该网站上的URL实际上使用latin1编码来表示“è”字符 - 出于某种原因,Python 2的requests库在调用前尝试“自动清理URL”时会将“è”字符编码为utf-8 - 这就是导致404错误的原因。
在调用requests.get之前尝试对unicode_url进行latin1编码也无济于事 - 它在“清理操作”之前尝试将其解码为Unicode,并在使用latin-1(即“ \xe8”字符)时出现无效utf-8序列的错误。
值得注意的是,与Python 3一起使用的requests也没有任何问题 - 因为该语言自动处理文本,所以requests需要更少地在文本编码之间来回转换 - 在我第一次尝试使用Python 3时,我得到了以下结果:
In [13]: requests.get(unicode_url)
Out[13]: <Response [200]>

现在,使用Python 2.7和requests的解决方法似乎很难找到 - 除非对requests中的某些特定代码进行猴子补丁以使其正常工作。然而,即使在Python2中,手动将您的unicode_url编码为latin-1,并使用urllib.open而不是requests也可以工作 - 如果您真的需要Python 2,那么这可能是最好的方法:

In [28]: a  = urllib.urlopen(unicode_url.encode("latin1"))

In [29]: a.code
Out[29]: 200

(如果这只是您为某个特定工具编写的一些脚本,我建议您切换到Python 3.6 - 处理数据时会更容易)


我在Python 2.7中进一步尝试了一下对requests进行猴子补丁,然后再发出请求,但似乎是徒劳的 - 实际上,在Python 2中只需使用urllib.urlopen,或者切换到Python 3。 - jsbueno
谢谢。你是怎么发现这个网站使用的是latin1的呢?(之前我查看了相关头部信息,它显示是utf8编码)。所以这是否是requests库中应该报告的错误,请问能指出清理代码相关部分吗? - fpghost
另外,为什么我的 OP 中的替代 URL 可以与请求一起工作? - fpghost
另外:In [6]: a.readlines() Out[6]: ['<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 4373853497355116658</body></html>']....在这个项目中无法更改为Python3。 - fpghost
是的,那显然是个打字错误。我真的不能在这个项目中改用Python 3。这是一个庞大的Django/Scrapy项目。 - fpghost
显示剩余5条评论

0
我观察到一些奇怪的东西:
In [1]: import requests

In [2]: s = requests.Session()

In [3]: unicode_url = u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'

In [4]: s.get(unicode_url)
Out[4]: <Response [404]>

In [5]: s.get(unicode_url)
Out[5]: <Response [200]>

第二次会话似乎可以正常工作!

可能可以说这实际上与cookies有关。第一次请求没有cookies,因此Web服务器返回404错误,同时仍然设置了一些会话cookies。下一个请求发送cookies并且Web服务器返回200。

但是,请注意第二个请求现在不需要重定向;您可以将第二个请求替换为s.get(unicode_url, allow_redirects=False),仍然可以获得200而不是302。而第一个请求通过重定向链。因此,现在它能够正常工作的唯一原因是使用cookies绕过了重定向。这表明编码问题发生在重定向链的某个地方。

注意:这与Chrome的干净会话中清除cookies完全相同。如果清除cookies然后转到URL,它将返回404。如果重新输入并再次尝试,则会遇到没有问题的200(cookies由第一个请求设置,并避免了导致404的麻烦重定向)

还有一件奇怪的事情:

In [11]:   requests.get(u'http://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl')
Out[11]: <Response [200]>

尽管没有cookies / sessions,我仍然很难理解。在这种情况下,重定向的位置标头如下:

 'Location':  'http://www.sainsburys.co.uk/webapp/wcs/stores/servlet/gb/groceries/chablis/chablis-premi\xc3\xa8r-cru-brocard-75cl?langId=44&storeId=10151&krypto=dZB7Mt97QsHQQ%2BGMpb1iMZwdVfmbg%2BbRUdkh%2FciAItm7%2F4VSUi8NRUiszN3mSofKSCyAv%2F0QRKSsjhHzoo1x7in7Ctd4vzPIDIW5CcjiksLKE48%2BFU9nLNGkVzGj92PknAgP%2FmIFz63xpKhvPkxbJrtUmwi%2FUpbXNW9XIygHyTA%3D&ddkey=http%3Agb%2Fgroceries%2Fchablis%2Fchablis-premi%C3%83%C2%A8r-cru-brocard-75cl'

即我们有 utf8 编码的 u'è' 而不是 latin1 编码。

概要

考虑到在我尝试的每个平台上(包括Chrome、Scrapy、python-requests),第一个请求(实际上没有 cookie 且依赖于重定向)都失败了,因此我认为这可能是主机服务器本身的错误。它在重定向中使用 latin1 编码其位置标头,但希望使用 utf8 编码的 URL,并在实际请求该重定向位置 URL 时出现404错误,因为服务器实际上正在等待一个 utf8 编码的 URL。它应该真正地对其重定向响应的位置标头进行 utf8 编码,以与其正在使用的 URL 编码保持一致。

这就是为什么当你作弊使用u'http://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'时,实际上会得到正确的utf8编码的重定向位置头,因为u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'.encode('latin1')'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premi\xc3\xa8r-cru-brocard-75cl',它恰好是u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl'的正确utf8编码字节串,所以当浏览器重定向时它能工作。

如果您已经设置了从访问URL或者网站其他地方获取的cookie,则可以避免重定向,并避免破碎的重定向过程。

请参考https://github.com/kennethreitz/requests/blob/eae38b8d131e8b51c3daf3583e69879d1c02f9a4/requests/sessions.py#L101-L114,了解在Python3中的请求处理方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接