Unicode 和 python 请求发生了一些有趣的事情



首先请注意,u'xc3xa8'是具有 2 个代码点的 python2 unicode 字符串,è个。接下来请注意,'xc3xa8'是 python2 字节 str,它表示字符è的 utf8 编码。所以u'xc3xa8''xc3xa8',尽管看起来非常相似,但这是两种截然不同的野兽。

现在,如果我们尝试在浏览器中访问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.get(unicode_url).不,404,没问题,URL 应该编码,所以我尝试requests.get(unicode_url.encode('utf8')).不,又是404。没问题,也许我也需要进行 URL 编码,所以我尝试requests.get(urllib.quote(unicode_url.encode('utf8')))....它根本不喜欢这样。

但是,回想起我在开头提到的 unicode 和 byte str 对象之间的相似之处,我也尝试了:

requests.get('http://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl')

令我惊讶的是,它有效并给出了成功的 200。

请求是怎么回事?

编辑:就像另一个实验(这次在Scrapy外壳中)

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请求呢?以及为什么备用网址在 python-request 中有效,但在浏览器或 Scrapy 中不起作用。

拉丁语1 vs 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 字符,如果你有一个像u'xe8'这样的 unicode str,那么你可以通过编码为 latin1 将其转换为相同形式的字节 str,即u'è'=u'xe8'u'xe8'.encode('latin1') = 'xe8'(右边的对象是拉丁语1的字节str编码,其形式与表示è的Unicode码位相同)

所以

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.

问题是网站上的 URL 实际上使用 latin1 编码来表示"è"字符 - 出于某种原因,Python 2 请求库在尝试在调用之前"自动清理 URL",用 utf-8 编码"è"字符 - 这就是导致 404 错误的原因。

在调用 requests.get 之前尝试用 latin1 对unicode_url进行编码也无济于事 - 它试图在"清理"之前将其解码为 unicode,并且在无效的 utf=8 序列上出错,该序列是使用拉丁语 1 时"è"的代码("\xe8"字符)。

在这个 ppint 值得注意的是,使用 Python 3 的请求也完全没有问题 - 由于该语言自动化文本处理,请求需要更少的来回文本编码 - 在我第一次尝试 Python 3 时,我刚刚得到:

In [13]: requests.get(unicode_url)
Out[13]: <Response [200]>

现在,Python 2.7 和请求的解决方法很难获得 - 无需猴子修补请求中的一些特定代码,以便它做正确的事情。然而,即使在 Python2 中,使用手动编码为 latin-1 的unicode_url,并使用urllib.open而不是请求也可以 - 如果你真的需要 Python 2,也许这是最适合你的方法:

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

(真的 - 如果这只是您为某些特定工具执行的一些脚本,我建议切换到 Python 3.6 - 当您到达数据时,处理数据也会容易得多)

我观察到了一些奇怪的事情:

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]>

似乎第二次在会话中工作!

也许可以说这实际上是关于饼干的。第一个请求没有cookie,因此网络服务器404s仍然设置一些会话cookie。下一个请求发送cookie和网络服务器200s。

但是,请注意,第二个请求现在不需要重定向;您可以将第二个请求替换为s.get(unicode_url, allow_redirects=False),并且仍然得到 200,而不是 302。而第一个请求通过重定向链。因此,它现在工作的唯一原因是重定向被 cookie 回避了。这表明编码问题发生在重定向链中的某个位置。

注意:这与清除了 Cookie 的干净 Chrome 会话完全相同。如果您清除cookie,则转到网址,它将404。如果您重新输入并重试,它将毫无问题地为 200(cookie 是由您的第一个请求设置的,并且避免了导致 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]>

尽管没有饼干/会话。我正在努力理解这一点。在这种情况下,重定向的位置标头如下所示:

'Location':  'http://www.sainsburys.co.uk/webapp/wcs/stores/servlet/gb/groceries/chablis/chablis-premixc3xa8r-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 编码。

总结

鉴于第一个请求(实际上没有cookie并依赖于重定向的请求)在我尝试过的每个平台(Chrome,Scrapy,python请求)中都失败了,我会将其归结为主机服务器本身的错误。它在重定向中对其位置标头进行 latin1 编码,但当浏览器实际请求该重定向位置 URL 时,它需要 utf8 编码的 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-premixc3xa8r-cru-brocard-75cl',这恰好是正确的utf8编码字节str的u'https://www.sainsburys.co.uk/shop/gb/groceries/chablis/chablis-premièr-cru-brocard-75cl',所以当浏览器重定向时它可以工作。

如果您已经设置了 cookie 来访问 URL 或网站上的其他地方,则可以避免重定向的需要,并避免损坏的重定向过程。

另请参阅 https://github.com/kennethreitz/requests/blob/eae38b8d131e8b51c3daf3583e69879d1c02f9a4/requests/sessions.py#L101-L114,了解这在 python3 请求中的工作方式。

最新更新