您当前位置: 首页 ‣ 深入 Python 3 ‣
难度级别: ♦♦♦♦♢
❝ 心绪不宁,枕头难安。 ❞
— 夏洛蒂·勃朗特
从哲学角度来说,我可以用 12 个词来描述 HTTP 网络服务:使用 HTTP 操作与远程服务器交换数据。如果您想从服务器获取数据,请使用 HTTP GET
。如果您想将新数据发送到服务器,请使用 HTTP POST
。一些更高级的 HTTP 网络服务 API 还允许使用 HTTP PUT
和 HTTP DELETE
来创建、修改和删除数据。就这样。没有注册表、没有信封、没有包装器、没有隧道。HTTP 协议中内置的“动词”(GET
、POST
、PUT
和 DELETE
)直接映射到用于检索、创建、修改和删除数据的应用程序级操作。
这种方法的主要优点是简单,而且它的简单性已被证明很受欢迎。数据(通常是 XML 或 JSON)可以静态构建和存储,也可以由服务器端脚本动态生成,所有主要的编程语言(包括 Python,当然!)都包含一个 HTTP 库用于下载它。调试也更容易;因为 HTTP 网络服务中的每个资源都有一个唯一的地址(以 URL 的形式),您可以将其加载到您的网络浏览器中,并立即查看原始数据。
HTTP 网络服务的示例
Python 3 带有两个用于与 HTTP 网络服务交互的不同库
http.client
是一个低级库,它实现了 RFC 2616,即 HTTP 协议。urllib.request
是构建在 http.client
之上的抽象层。它提供了一个标准 API 用于访问 HTTP 和 FTP 服务器,自动遵循 HTTP 重定向,并处理一些常见的 HTTP 身份验证形式。那么您应该使用哪个呢?两个都不。相反,您应该使用 httplib2
,这是一个开源的第三方库,它比 http.client
更全面地实现了 HTTP,但提供了比 urllib.request
更好的抽象。
要理解为什么 httplib2
是正确的选择,您首先需要了解 HTTP。
⁂
所有 HTTP 客户端都应支持五个重要特性。
关于任何类型的网络服务,最重要的是要了解网络访问非常昂贵。我的意思是“金钱”昂贵(尽管带宽并不免费)。我的意思是打开连接、发送请求和从远程服务器检索响应需要很长时间。即使在最快的宽带连接上,延迟(发送请求并开始检索响应数据所需的时间)也可能比您预期的要高。路由器出现故障、数据包丢失、中间代理受到攻击等等,公共互联网上从不平静,而且您可能对此无能为力。
HTTP 的设计考虑到了缓存。有一整类设备(称为“缓存代理”)其唯一作用就是坐在您和世界其他地方之间,并最大程度地减少网络访问。即使您不知道,您的公司或 ISP 也几乎肯定维护着缓存代理。它们之所以有效,是因为缓存是内置在 HTTP 协议中的。
以下是一个关于缓存如何工作的具体示例。您在浏览器中访问 diveintomark.org
。该页面包含一个背景图像,wearehugh.com/m.jpg
。当您的浏览器下载该图像时,服务器会包含以下 HTTP 标头
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
Cache-Control
和 Expires
标头告诉您的浏览器(以及您和服务器之间的任何缓存代理)可以将此图像缓存长达一年。一年!并且如果在下一年,您访问另一个包含指向此图像链接的页面,您的浏览器将从其缓存中加载图像,不会生成任何网络活动。
但是等等,事情变得更好了。假设您的浏览器出于某种原因从您的本地缓存中清除图像。也许它没有足够的磁盘空间;也许您手动清除了缓存。无论如何。但是 HTTP 标头说这些数据可以被公共缓存代理缓存。(从技术上讲,重要的是标头没有说的是什么;Cache-Control
标头没有 private
关键字,所以默认情况下这些数据是可以缓存的。)缓存代理被设计为拥有大量的存储空间,可能远超过您的本地浏览器分配的空间。
如果您的公司或 ISP 维护着缓存代理,那么代理可能仍然缓存了图像。当您再次访问 diveintomark.org
时,您的浏览器将在其本地缓存中查找图像,但不会找到,因此它将发出网络请求以尝试从远程服务器下载它。但是,如果缓存代理仍然有一个图像副本,它将拦截该请求并从其缓存中提供图像。这意味着您的请求永远不会到达远程服务器;事实上,它永远不会离开您公司的网络。这将加快下载速度(更少的网络跃点)并为您的公司节省资金(更少从外部世界下载的数据)。
HTTP 缓存只有在每个人都尽职尽责的情况下才能发挥作用。一方面,服务器需要在其响应中发送正确的标头。另一方面,客户端需要在两次请求相同数据之前了解并尊重这些标头。中间的代理不是万能药;它们只能像服务器和客户端允许它们那样聪明。
Python 的 HTTP 库不支持缓存,但 httplib2
支持。
有些数据从不改变,而其他数据一直在改变。在这两者之间,有一个巨大的数据领域,这些数据可能已经改变,但还没有改变。CNN.com 的提要每隔几分钟就会更新,但我的博客提要可能几天或几周都不会改变。在后一种情况下,我不希望告诉客户端将我的提要缓存几周,因为当我真正发布一些东西时,人们可能需要几周才能阅读(因为他们尊重我的缓存标头,标头说“不要打扰检查这个提要几周”)。另一方面,如果我的提要没有改变,我不想让客户端每小时下载我的整个提要一次!
HTTP 也为此提供了一个解决方案。当您第一次请求数据时,服务器可以发送回一个 Last-Modified
标头。这正是它听起来的意思:数据更改的日期。从 diveintomark.org
引用的背景图像包含一个 Last-Modified
标头。
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
当您第二次(或第三次或第四次)请求相同数据时,您可以使用 If-Modified-Since
标头发送请求,并带上您上次从服务器获得的日期。如果数据自那时起发生了改变,那么服务器将使用 200
状态代码为您提供新数据。但是,如果数据自那时起没有改变,服务器将发送回一个特殊的 HTTP 304
状态代码,这意味着“此数据自上次请求以来没有改变”。您可以在命令行中使用 curl 测试它
you@localhost:~$ curl -I -H "If-Modified-Since: Fri, 22 Aug 2008 04:28:16 GMT" http://wearehugh.com/m.jpg HTTP/1.1 304 Not Modified Date: Sun, 31 May 2009 18:04:39 GMT Server: Apache Connection: close ETag: "3075-ddc8d800" Expires: Mon, 31 May 2010 18:04:39 GMT Cache-Control: max-age=31536000, public
为什么这是一个改进?因为当服务器发送 304
时,它不会重新发送数据。您只获得状态代码。即使您缓存的副本已过期,上次修改检查也能确保您不会在数据没有改变的情况下两次下载相同的数据。(作为额外的好处,此 304
响应还包含缓存标头。代理将保留数据的副本,即使它正式“过期”也是如此,希望数据没有真正改变,并且下一个请求将使用 304
状态代码和更新的缓存信息进行响应。)
Python 的 HTTP 库不支持上次修改日期检查,但 httplib2
支持。
ETag 是实现与上次修改检查相同功能的另一种方式。使用 Etag,服务器会将一个哈希代码放在 ETag
标头中,与您请求的数据一起发送。(此哈希是如何确定的完全取决于服务器。唯一的要求是当数据改变时它也会改变。)从 diveintomark.org
引用的背景图像有一个 ETag
标头。
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
您第二次请求相同数据时,将在请求的 If-None-Match
标头中包含 ETag 哈希。如果数据没有改变,服务器将发送回一个 304
状态代码。与上次修改日期检查一样,服务器只发送回 304
状态代码;它不会第二次发送相同的数据。通过在您的第二次请求中包含 ETag 哈希,您告诉服务器,如果数据仍然匹配此哈希,则无需重新发送相同的数据,因为您仍然拥有上次的数据。
再次使用 curl
you@localhost:~$ curl -I -H "If-None-Match: \"3075-ddc8d800\"" http://wearehugh.com/m.jpg ①
HTTP/1.1 304 Not Modified
Date: Sun, 31 May 2009 18:04:39 GMT
Server: Apache
Connection: close
ETag: "3075-ddc8d800"
Expires: Mon, 31 May 2010 18:04:39 GMT
Cache-Control: max-age=31536000, public
If-None-Match
标头中。Python 的 HTTP 库不支持 ETag,但 httplib2
支持。
当您谈论 HTTP 网络服务时,您几乎总是在谈论通过网络来回移动基于文本的数据。也许是 XML,也许是 JSON,也许只是纯文本。无论格式如何,文本都压缩得很好。XML 章节中的示例提要未压缩时为 3070 字节,但经过 gzip 压缩后为 941 字节。这仅仅是原始大小的 30% 而已!
HTTP 支持几种压缩算法。最常见的两种类型是 gzip 和 deflate。当您通过 HTTP 请求资源时,您可以要求服务器以压缩格式发送它。您在请求中包含一个 Accept-encoding
标头,其中列出了您支持哪些压缩算法。如果服务器支持任何相同的算法,它将发送回压缩数据(以及一个 Content-encoding
标头,告诉您它使用了哪种算法)。然后由您来解压缩数据。
☞服务器端开发人员的重要提示:确保资源的压缩版本与未压缩版本具有不同的Etag。否则,缓存代理会感到困惑,并可能将压缩版本提供给无法处理它的客户端。阅读 Apache bug 39727 的讨论以了解更多关于此细微问题的详细信息。
Python 的 HTTP 库不支持压缩,但 httplib2
支持。
酷炫的 URI 不会改变,但很多 URI 真的很不酷。网站会进行重组,页面会迁移到新的地址。即使是网络服务也会进行重组。http://example.com/index.xml
上的联合提要可能会迁移到 http://example.com/xml/atom.xml
。或者整个域可能会迁移,因为组织会扩展和重组;http://www.example.com/index.xml
变成 http://server-farm-1.example.com/index.xml
。
每次您向 HTTP 服务器请求任何类型的资源时,服务器都会在其响应中包含一个状态码。状态码 200
表示“一切正常,这是您所请求的页面”。状态码 404
表示“页面未找到”。(您可能在浏览网页时看到过 404 错误。)300 多位的状态码表示某种形式的重定向。
HTTP 有几种不同的方法来表明资源已移动。两种最常见的技术是状态码 302
和 301
。状态码 302
是一个临时重定向;它表示“哦,那个被临时移到了这里”(然后在 Location
标头中给出临时地址)。状态码 301
是一个永久重定向;它表示“哦,那个被永久移到了这里”(然后在 Location
标头中给出新的地址)。如果您收到一个 302
状态码和一个新的地址,HTTP 规范规定您应该使用新的地址来获取您所请求的内容,但下次您想访问同一个资源时,应该重试旧的地址。但如果您收到一个 301
状态码和一个新的地址,您应该从那时起使用新的地址。
当 urllib.request
模块从 HTTP 服务器接收到适当的状态码时,它会自动“跟随”重定向,但它不会告诉您它已经这样做了。您最终会得到您所请求的数据,但您永远不会知道底层库“帮助”您进行了重定向。因此,您将继续使用旧地址,每次您都会被重定向到新地址,每次 urllib.request
模块都会“帮助”您进行重定向。换句话说,它将永久重定向与临时重定向视为相同。这意味着两次往返而不是一次,这对服务器和您都不利。
httplib2
会为您处理永久重定向。它不仅会告诉您永久重定向发生了,还会在本地跟踪它们,并在请求重定向的 URL 之前自动重写它们。
⁂
假设您想通过 HTTP 下载资源,例如 Atom 订阅。作为一个订阅,您不仅仅要下载一次;您要一遍又一遍地下载它。(大多数订阅阅读器会每小时检查一次是否有更改。)让我们先用快速而脏的方法来做,然后再看看如何做得更好。
>>> import urllib.request >>> a_url = 'http://diveintopython3.org/examples/feed.xml' >>> data = urllib.request.urlopen(a_url).read() ① >>> type(data) ② <class 'bytes'> >>> print(data) <?xml version='1.0' encoding='utf-8'?> <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title>dive into mark</title> <subtitle>currently between addictions</subtitle> <id>tag:diveintomark.org,2001-07-29:/</id> <updated>2009-03-27T21:56:07Z</updated> <link rel='alternate' type='text/html' href='http://diveintomark.org/'/> …
urllib.request
模块有一个方便的 urlopen()
函数,它接受您想要的页面的地址,并返回一个类似文件的对象,您可以从中 read()
来获取页面的全部内容。它不可能再容易了。urlopen().read()
方法总是返回 一个 bytes
对象,而不是一个字符串。记住,字节就是字节;字符是一个抽象概念。HTTP 服务器不处理抽象概念。如果您请求资源,您将获得字节。如果您希望它是一个字符串,您需要确定字符编码并显式地将其转换为字符串。那么这有什么问题呢?对于测试或开发期间的快速一次性操作,它没有什么问题。我一直这样做。我想要订阅的内容,我得到了订阅的内容。同样的技术适用于任何网页。但是,一旦您开始考虑一个您想定期访问的网络服务(例如 每小时请求一次此订阅),那么您就是在效率低下,并且您也是在无礼。
⁂
为了看看为什么这样做效率低下且无礼,让我们打开 Python 的 HTTP 库的调试功能,看看“电线上”(即 通过网络)发送了什么。
>>> from http.client import HTTPConnection >>> HTTPConnection.debuglevel = 1 ① >>> from urllib.request import urlopen >>> response = urlopen('http://diveintopython3.org/examples/feed.xml') ② send: b'GET /examples/feed.xml HTTP/1.1 ③ Host: diveintopython3.org ④ Accept-Encoding: identity ⑤ User-Agent: Python-urllib/3.1' ⑥ Connection: close reply: 'HTTP/1.1 200 OK' …further debugging information omitted…
urllib.request
依赖于另一个标准 Python 库 http.client
。通常情况下,您不需要直接接触 http.client
。(urllib.request
模块会自动导入它。)但是,我们在这里导入它,以便我们可以切换 HTTPConnection
类(urllib.request
使用它来连接到 HTTP 服务器)的调试标志。urllib.request
模块会向服务器发送五行内容。urllib.request
默认情况下不支持压缩。Python-urllib
加上版本号。urllib.request
和 httplib2
都支持更改用户代理,只需在请求中添加一个 User-Agent
标头(这将覆盖默认值)。现在让我们看看服务器在响应中发回了什么。
# continued from previous example >>> print(response.headers.as_string()) ① Date: Sun, 31 May 2009 19:23:06 GMT ② Server: Apache Last-Modified: Sun, 31 May 2009 06:39:55 GMT ③ ETag: "bfe-93d9c4c0" ④ Accept-Ranges: bytes Content-Length: 3070 ⑤ Cache-Control: max-age=86400 ⑥ Expires: Mon, 01 Jun 2009 19:23:06 GMT Vary: Accept-Encoding Connection: close Content-Type: application/xml >>> data = response.read() ⑦ >>> len(data) 3070
urllib.request.urlopen()
函数返回的 response
包含服务器发回的所有 HTTP 标头。它还包含用于下载实际数据的函数;我们将在稍后讨论它。Last-Modified
标头。ETag
标头。Content-encoding
标头。您的请求表明您只接受未压缩数据 (Accept-encoding: identity
),事实证明,此响应包含未压缩数据。response.read()
来下载实际数据。正如您从 len()
函数中看到的,这总共获取了 3070 字节。如您所见,此代码效率低下:它请求(并接收)了未压缩的数据。我确切地知道此服务器支持 gzip 压缩,但是 HTTP 压缩是可选的。我们没有请求它,所以我们没有得到它。这意味着我们获取了 3070 字节,而我们本可以获取 941 字节。坏狗,没有饼干。
但是等等,事情更糟!为了看看这段代码效率有多低,让我们再次请求同一个订阅。
# continued from the previous example >>> response2 = urlopen('http://diveintopython3.org/examples/feed.xml') send: b'GET /examples/feed.xml HTTP/1.1 Host: diveintopython3.org Accept-Encoding: identity User-Agent: Python-urllib/3.1' Connection: close reply: 'HTTP/1.1 200 OK' …further debugging information omitted…
您注意到此请求有什么特别之处吗?它没有改变!它与第一个请求完全一样。没有 If-Modified-Since
标头 的迹象。没有 If-None-Match
标头 的迹象。不尊重缓存标头。仍然没有压缩。
当您两次做同样的事情时,会发生什么?您将获得相同的响应。两次。
# continued from the previous example >>> print(response2.headers.as_string()) ① Date: Mon, 01 Jun 2009 03:58:00 GMT Server: Apache Last-Modified: Sun, 31 May 2009 22:51:11 GMT ETag: "bfe-255ef5c0" Accept-Ranges: bytes Content-Length: 3070 Cache-Control: max-age=86400 Expires: Tue, 02 Jun 2009 03:58:00 GMT Vary: Accept-Encoding Connection: close Content-Type: application/xml >>> data2 = response2.read() >>> len(data2) ② 3070 >>> data2 == data ③ True
Cache-Control
和 Expires
以允许缓存,Last-Modified
和 ETag
以启用“未修改”跟踪。甚至 Vary: Accept-Encoding
标头也暗示服务器将支持压缩,只要您提出要求即可。但您没有。HTTP 的设计优于此。urllib
就像我讲西班牙语一样说 HTTP——足以应付困境,但不足以进行对话。HTTP 是一种对话。是时候升级到一个能够流利地说 HTTP 的库了。
⁂
httplib2
在使用 httplib2
之前,您需要安装它。访问 code.google.com/p/httplib2/
并下载最新版本。httplib2
可用于 Python 2.x 和 Python 3.x;确保您获得 Python 3 版本,名称类似于 httplib2-python3-0.5.0.zip
。
解压缩存档,打开一个终端窗口,然后转到新创建的 httplib2
目录。在 Windows 上,打开 开始
菜单,选择 运行...
,键入 cmd.exe 然后按 ENTER。
c:\Users\pilgrim\Downloads> dir Volume in drive C has no label. Volume Serial Number is DED5-B4F8 Directory of c:\Users\pilgrim\Downloads 07/28/2009 12:36 PM <DIR> . 07/28/2009 12:36 PM <DIR> .. 07/28/2009 12:36 PM <DIR> httplib2-python3-0.5.0 07/28/2009 12:33 PM 18,997 httplib2-python3-0.5.0.zip 1 File(s) 18,997 bytes 3 Dir(s) 61,496,684,544 bytes free c:\Users\pilgrim\Downloads> cd httplib2-python3-0.5.0 c:\Users\pilgrim\Downloads\httplib2-python3-0.5.0> c:\python31\python.exe setup.py install running install running build running build_py running install_lib creating c:\python31\Lib\site-packages\httplib2 copying build\lib\httplib2\iri2uri.py -> c:\python31\Lib\site-packages\httplib2 copying build\lib\httplib2\__init__.py -> c:\python31\Lib\site-packages\httplib2 byte-compiling c:\python31\Lib\site-packages\httplib2\iri2uri.py to iri2uri.pyc byte-compiling c:\python31\Lib\site-packages\httplib2\__init__.py to __init__.pyc running install_egg_info Writing c:\python31\Lib\site-packages\httplib2-python3_0.5.0-py3.1.egg-info
在 Mac OS X 上,运行 /Applications/Utilities/
文件夹中的 Terminal.app
应用程序。在 Linux 上,运行 Terminal
应用程序,它通常位于您的 应用程序
菜单中的 附件
或 系统
下。
you@localhost:~/Desktop$ unzip httplib2-python3-0.5.0.zip Archive: httplib2-python3-0.5.0.zip inflating: httplib2-python3-0.5.0/README inflating: httplib2-python3-0.5.0/setup.py inflating: httplib2-python3-0.5.0/PKG-INFO inflating: httplib2-python3-0.5.0/httplib2/__init__.py inflating: httplib2-python3-0.5.0/httplib2/iri2uri.py you@localhost:~/Desktop$ cd httplib2-python3-0.5.0/ you@localhost:~/Desktop/httplib2-python3-0.5.0$ sudo python3 setup.py install running install running build running build_py creating build creating build/lib.linux-x86_64-3.1 creating build/lib.linux-x86_64-3.1/httplib2 copying httplib2/iri2uri.py -> build/lib.linux-x86_64-3.1/httplib2 copying httplib2/__init__.py -> build/lib.linux-x86_64-3.1/httplib2 running install_lib creating /usr/local/lib/python3.1/dist-packages/httplib2 copying build/lib.linux-x86_64-3.1/httplib2/iri2uri.py -> /usr/local/lib/python3.1/dist-packages/httplib2 copying build/lib.linux-x86_64-3.1/httplib2/__init__.py -> /usr/local/lib/python3.1/dist-packages/httplib2 byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/iri2uri.py to iri2uri.pyc byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/__init__.py to __init__.pyc running install_egg_info Writing /usr/local/lib/python3.1/dist-packages/httplib2-python3_0.5.0.egg-info
要使用 httplib2
,请创建 httplib2.Http
类的实例。
>>> import httplib2 >>> h = httplib2.Http('.cache') ① >>> response, content = h.request('http://diveintopython3.org/examples/feed.xml') ② >>> response.status ③ 200 >>> content[:52] ④ b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns=" >>> len(content) 3070
httplib2
的主要接口是 Http
对象。出于将在下一节中看到的原因,您应该始终在创建 Http
对象时传递一个目录名。该目录不必存在;httplib2
将在必要时创建它。Http
对象后,检索数据就像使用您想要的数据的地址调用 request()
方法一样简单。这将对该 URL 发出 HTTP GET
请求。(在本章的后面,您将看到如何发出其他 HTTP 请求,例如 POST
。)request()
方法返回两个值。第一个是 httplib2.Response
对象,它包含服务器返回的所有 HTTP 标头。例如,status
代码为 200
表示请求成功。content
变量包含 HTTP 服务器返回的实际数据。数据以 bytes
对象,而不是字符串 的形式返回。如果您希望它是一个字符串,您需要确定字符编码并自行转换它。☞您可能只需要一个
httplib2.Http
对象。创建多个对象有充分的理由,但您应该只在知道为什么需要它们的情况下才这样做。“我需要从两个不同的 URL 请求数据”不是一个正当理由。重新使用Http
对象,只需调用request()
方法两次即可。
httplib2
返回字节而不是字符串的简短说明字节。字符串。真让人头疼。为什么 httplib2
不能“仅仅”为您完成转换呢?嗯,这很复杂,因为确定字符编码的规则取决于您正在请求的资源类型。httplib2
如何知道您正在请求什么类型的资源?它通常在 Content-Type
HTTP 标头中列出,但这是 HTTP 的可选功能,并非所有 HTTP 服务器都包含它。如果该标头未包含在 HTTP 响应中,则由客户端自行猜测。(这通常被称为“内容嗅探”,而且它从不完美。)
如果您知道您正在期待哪种类型的资源(在本例中是 XML 文档),那么您也许可以“仅仅”将返回的 bytes
对象传递给 xml.etree.ElementTree.parse()
函数。只要 XML 文档包含有关其自身字符编码的信息(此文档包含此信息),这就会起作用,但这是一种可选功能,并非所有 XML 文档都这样做。如果 XML 文档不包含编码信息,则客户端应该查看封闭的传输——即 Content-Type
HTTP 标头,它可以包含 charset
参数。
但这还不止。现在字符编码信息可以存在两个地方:XML 文档本身以及 Content-Type
HTTP 头部。如果信息在这两个地方都存在,哪一个会获胜?根据 RFC 3023(我发誓这不是我编造的),如果 Content-Type
HTTP 头部中给出的媒体类型为 application/xml
、application/xml-dtd
、application/xml-external-parsed-entity
或者 application/xml
的任何子类型,比如 application/atom+xml
、application/rss+xml
甚至 application/rdf+xml
,那么编码将是
Content-Type
HTTP 头部中 charset
参数给出的编码,或者encoding
属性给出的编码,或者另一方面,如果 Content-Type
HTTP 头部中给出的媒体类型为 text/xml
、text/xml-external-parsed-entity
或者像 text/AnythingAtAll+xml
这样的子类型,那么文档中 XML 声明的 encoding
属性将被完全忽略,编码将是
Content-Type
HTTP 头部中 charset
参数给出的编码,或者us-ascii
而这仅仅针对 XML 文档。对于 HTML 文档,网页浏览器已经构建了如此复杂的 内容嗅探规则 [PDF],我们仍在努力弄清楚它们。
“欢迎贡献补丁。”
httplib2
如何处理缓存还记得上一节中我说过,你应该始终使用目录名称创建一个 httplib2.Http
对象吗?这就是缓存的原因。
# continued from the previous example >>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml') ① >>> response2.status ② 200 >>> content2[:52] ③ b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns=" >>> len(content2) 3070
status
再次为 200
,与上次相同。所以……谁在乎呢?退出 Python 交互式 shell 并使用新会话重新启动它,我将向你展示。
# NOT continued from previous example! # Please exit out of the interactive shell # and launch a new one. >>> import httplib2 >>> httplib2.debuglevel = 1 ① >>> h = httplib2.Http('.cache') ② >>> response, content = h.request('http://diveintopython3.org/examples/feed.xml') ③ >>> len(content) ④ 3070 >>> response.status ⑤ 200 >>> response.fromcache ⑥ True
httplib2
中开启 http.client
中调试功能的等效操作。httplib2
将打印发送到服务器的所有数据以及发送回来的某些关键信息。httplib2.Http
对象。httplib2
的本地缓存生成的。你在创建 httplib2.Http
对象时传入的目录名——这个目录保存了 httplib2
执行的所有操作的缓存。☞如果你想开启
httplib2
调试,你需要设置一个模块级常量(httplib2.debuglevel
),然后创建一个新的httplib2.Http
对象。如果你想关闭调试,你需要更改相同的模块级常量,然后创建一个新的httplib2.Http
对象。
你之前请求了此 URL 的数据。该请求成功(status: 200
)。该响应不仅包含提要数据,还包含一组 缓存头,它告诉任何监听者他们可以缓存此资源长达 24 小时(Cache-Control: max-age=86400
,即以秒为单位的 24 小时)。httplib2
理解并尊重这些缓存头,并将之前的响应存储在 .cache
目录中(你在创建 Http
对象时传入)。该缓存尚未过期,因此你第二次请求此 URL 的数据时,httplib2
只是返回缓存的结果,而无需访问网络。
我说“仅仅”,但显然这种简单背后隐藏着很多复杂性。httplib2
自动且默认处理 HTTP 缓存。如果由于某种原因你需要知道响应是否来自缓存,你可以检查 response.fromcache
。否则,它会自动运行。
现在,假设你缓存了一些数据,但你想绕过缓存并从远程服务器重新请求它。浏览器有时会这样做,如果用户明确要求。例如,按下 F5 会刷新当前页面,但按下 Ctrl+F5 会绕过缓存并从远程服务器重新请求当前页面。你可能会想,“哦,我只需要从我的本地缓存中删除数据,然后再次请求它。”你可以这样做,但请记住,除了你和远程服务器之外,可能还有其他方参与。那些中间代理服务器呢?它们完全不受你的控制,它们可能仍然缓存了这些数据,并且会很乐意将它们返回给你,因为(就它们而言)它们的缓存仍然有效。
与其操作你的本地缓存并寄希望于最好,不如使用 HTTP 的功能来确保你的请求确实到达了远程服务器。
# continued from the previous example >>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml', ... headers={'cache-control':'no-cache'}) ① connect: (diveintopython3.org, 80) ② send: b'GET /examples/feed.xml HTTP/1.1 Host: diveintopython3.org user-agent: Python-httplib2/$Rev: 259 $ accept-encoding: deflate, gzip cache-control: no-cache' reply: 'HTTP/1.1 200 OK' …further debugging information omitted… >>> response2.status 200 >>> response2.fromcache ③ False >>> print(dict(response2.items())) ④ {'status': '200', 'content-length': '3070', 'content-location': 'http://diveintopython3.org/examples/feed.xml', 'accept-ranges': 'bytes', 'expires': 'Wed, 03 Jun 2009 00:40:26 GMT', 'vary': 'Accept-Encoding', 'server': 'Apache', 'last-modified': 'Sun, 31 May 2009 22:51:11 GMT', 'connection': 'close', '-content-encoding': 'gzip', 'etag': '"bfe-255ef5c0"', 'cache-control': 'max-age=86400', 'date': 'Tue, 02 Jun 2009 00:40:26 GMT', 'content-type': 'application/xml'}
httplib2
允许你向任何传出的请求添加任意 HTTP 头。为了绕过所有缓存(不仅仅是你的本地磁盘缓存,还有你与远程服务器之间的任何缓存代理),在 headers 字典中添加一个 no-cache
头。httplib2
发起了一个网络请求。httplib2
双向理解并尊重缓存头——作为传入响应的一部分以及作为传出请求的一部分。它注意到你添加了 no-cache
头,因此它完全绕过了它的本地缓存,然后别无选择,只能访问网络来请求数据。httplib2
使用这些头更新其本地缓存,以期在下次你请求此提要时避免网络访问。关于 HTTP 缓存的一切都旨在最大程度地提高缓存命中率并减少网络访问。即使你这次绕过了缓存,远程服务器也真的很感谢你下次能够缓存结果。httplib2
如何处理 Last-Modified
和 ETag
头Cache-Control
和 Expires
缓存头 被称为新鲜度指示器。它们明确告诉缓存,在缓存过期之前,你可以完全避免所有网络访问。这正是你在 上一节中看到的行为:给定一个新鲜度指示器,httplib2
不会生成任何网络活动来提供缓存的数据(当然,除非你明确地 绕过缓存)。
但对于数据可能已更改但实际上没有更改的情况怎么办?HTTP 为此定义了 Last-Modified
和 Etag
头。这些头被称为验证器。如果本地缓存不再新鲜,客户端可以在下次请求中发送验证器,以查看数据是否确实发生了更改。如果数据没有更改,服务器会发送一个 304
状态码以及无数据。因此仍然需要进行网络往返,但你最终下载的字节数更少。
>>> import httplib2 >>> httplib2.debuglevel = 1 >>> h = httplib2.Http('.cache') >>> response, content = h.request('http://diveintopython3.org/') ① connect: (diveintopython3.org, 80) send: b'GET / HTTP/1.1 Host: diveintopython3.org accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 200 OK' >>> print(dict(response.items())) ② {'-content-encoding': 'gzip', 'accept-ranges': 'bytes', 'connection': 'close', 'content-length': '6657', 'content-location': 'http://diveintopython3.org/', 'content-type': 'text/html', 'date': 'Tue, 02 Jun 2009 03:26:54 GMT', 'etag': '"7f806d-1a01-9fb97900"', 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT', 'server': 'Apache', 'status': '200', 'vary': 'Accept-Encoding,User-Agent'} >>> len(content) ③ 6657
httplib2
可以利用的东西很少,它只发送最少的请求头。ETag
和 Last-Modified
头。# continued from the previous example >>> response, content = h.request('http://diveintopython3.org/') ① connect: (diveintopython3.org, 80) send: b'GET / HTTP/1.1 Host: diveintopython3.org if-none-match: "7f806d-1a01-9fb97900" ② if-modified-since: Tue, 02 Jun 2009 02:51:48 GMT ③ accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 304 Not Modified' ④ >>> response.fromcache ⑤ True >>> response.status ⑥ 200 >>> response.dict['status'] ⑦ '304' >>> len(content) ⑧ 6657
Http
对象(以及同一个本地缓存)。httplib2
将 ETag
验证器发送回服务器,位于 If-None-Match
头中。httplib2
还将 Last-Modified
验证器发送回服务器,位于 If-Modified-Since
头中。304
状态码以及无数据。httplib2
注意到 304
状态码,并从其缓存中加载页面内容。304
(这次从服务器返回,导致 httplib2
查看其缓存)和 200
(上次从服务器返回,并与页面数据一起存储在 httplib2
的缓存中)。response.status
返回来自缓存的状态。response.dict
来获得它,response.dict
是从服务器返回的实际头的字典。httplib2
足够聪明,让你可以装傻。)在 request()
方法返回给调用者之前,httplib2
已经更新了其缓存并将数据返回给你。http2lib
如何处理压缩HTTP 支持 多种类型的压缩;两种最常见的类型是 gzip 和 deflate。httplib2
支持这两种类型。
>>> response, content = h.request('http://diveintopython3.org/') connect: (diveintopython3.org, 80) send: b'GET / HTTP/1.1 Host: diveintopython3.org accept-encoding: deflate, gzip ① user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 200 OK' >>> print(dict(response.items())) {'-content-encoding': 'gzip', ② 'accept-ranges': 'bytes', 'connection': 'close', 'content-length': '6657', 'content-location': 'http://diveintopython3.org/', 'content-type': 'text/html', 'date': 'Tue, 02 Jun 2009 03:26:54 GMT', 'etag': '"7f806d-1a01-9fb97900"', 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT', 'server': 'Apache', 'status': '304', 'vary': 'Accept-Encoding,User-Agent'}
httplib2
发送请求时,它都会包含一个 Accept-Encoding
头,告诉服务器它可以处理 deflate
或 gzip
压缩。request()
方法返回之前,httplib2
已经解压缩了响应的正文并将其放入 content 变量中。如果你想知道响应是否被压缩,你可以检查 response['-content-encoding'];否则,不用担心。httplib2
如何处理重定向HTTP 定义了 两种类型的重定向:临时重定向和永久重定向。除了遵循临时重定向之外,没有其他特殊操作需要执行,httplib2
会自动执行此操作。
>>> import httplib2 >>> httplib2.debuglevel = 1 >>> h = httplib2.Http('.cache') >>> response, content = h.request('http://diveintopython3.org/examples/feed-302.xml') ① connect: (diveintopython3.org, 80) send: b'GET /examples/feed-302.xml HTTP/1.1 ② Host: diveintopython3.org accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 302 Found' ③ send: b'GET /examples/feed.xml HTTP/1.1 ④ Host: diveintopython3.org accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 200 OK'
302 Found
。此处未显示,此响应还包含一个指向实际 URL 的 Location
头:http://diveintopython3.org/examples/feed.xml
httplib2
立即转身并“遵循”重定向,通过发出另一个针对 Location
头中给出的 URL 的请求:http://diveintopython3.org/examples/feed.xml
“跟随”重定向只不过是这个例子所展示的那样。httplib2
发送对您所请求的 URL 的请求。服务器返回一个响应,说“不不,看看那边。”httplib2
就会发送另一个对新 URL 的请求。
# continued from the previous example >>> response ① {'status': '200', 'content-length': '3070', 'content-location': 'http://diveintopython3.org/examples/feed.xml', ② 'accept-ranges': 'bytes', 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT', 'vary': 'Accept-Encoding', 'server': 'Apache', 'last-modified': 'Wed, 03 Jun 2009 02:20:15 GMT', 'connection': 'close', '-content-encoding': 'gzip', ③ 'etag': '"bfe-4cbbf5c0"', 'cache-control': 'max-age=86400', ④ 'date': 'Wed, 03 Jun 2009 02:21:41 GMT', 'content-type': 'application/xml'}
request()
方法的单个调用中获取的 响应 是来自最终 URL 的响应。httplib2
将最终 URL 添加到 响应 字典中,作为 content-location
。这不是来自服务器的标头;它是特定于 httplib2
的。您获取的 响应 提供有关最终 URL 的信息。如果您想要有关中间 URL(最终重定向到最终 URL 的那些 URL)的更多信息怎么办?httplib2
也允许您这样做。
# continued from the previous example >>> response.previous ① {'status': '302', 'content-length': '228', 'content-location': 'http://diveintopython3.org/examples/feed-302.xml', 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT', 'server': 'Apache', 'connection': 'close', 'location': 'http://diveintopython3.org/examples/feed.xml', 'cache-control': 'max-age=86400', 'date': 'Wed, 03 Jun 2009 02:21:41 GMT', 'content-type': 'text/html; charset=iso-8859-1'} >>> type(response) ② <class 'httplib2.Response'> >>> type(response.previous) <class 'httplib2.Response'> >>> response.previous.previous ③ >>>
response.previous
属性保存对 httplib2
为获取当前响应对象而跟随的先前响应对象的引用。response
和 response.previous
都是 httplib2.Response
对象。response.previous.previous
以进一步向后跟踪重定向链。(场景:一个 URL 重定向到第二个 URL,第二个 URL 重定向到第三个 URL。这可能会发生!)在本例中,我们已经到达重定向链的起点,因此该属性为 None
。如果您再次请求相同的 URL 会发生什么?
# continued from the previous example >>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-302.xml') ① connect: (diveintopython3.org, 80) send: b'GET /examples/feed-302.xml HTTP/1.1 ② Host: diveintopython3.org accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 302 Found' ③ >>> content2 == content ④ True
httplib2.Http
对象(因此具有相同的缓存)。302
响应未被缓存,因此 httplib2
发送对相同 URL 的另一个请求。302
响应。但请注意没有发生什么:从未对最终 URL http://diveintopython3.org/examples/feed.xml
发出第二个请求。该响应已缓存(请记住您在前面的示例中看到的 Cache-Control
标头)。当 httplib2
收到 302 Found
代码后,它在发出另一个请求之前检查了它的缓存。缓存中包含 http://diveintopython3.org/examples/feed.xml
的最新副本,因此无需重新请求它。request()
方法返回时,它已从缓存中读取提要数据并将其返回。当然,它与您上次收到的数据相同。换句话说,您无需对临时重定向执行任何特殊操作。httplib2
将自动跟踪它们,并且一个 URL 重定向到另一个 URL 的事实不会影响 httplib2
对压缩、缓存、ETags
或任何其他 HTTP 功能的支持。
永久重定向也同样简单。
# continued from the previous example >>> response, content = h.request('http://diveintopython3.org/examples/feed-301.xml') ① connect: (diveintopython3.org, 80) send: b'GET /examples/feed-301.xml HTTP/1.1 Host: diveintopython3.org accept-encoding: deflate, gzip user-agent: Python-httplib2/$Rev: 259 $' reply: 'HTTP/1.1 301 Moved Permanently' ② >>> response.fromcache ③ True
http://diveintopython3.org/examples/feed.xml
发出永久重定向。301
。但再次注意没有发生什么:没有对重定向 URL 发出请求。为什么?因为它已经在本地缓存。httplib2
“跟随”了重定向,直接进入它的缓存。但是等等!还有更多!
# continued from the previous example >>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml') ① >>> response2.fromcache ② True >>> content2 == content ③ True
httplib2
跟随了永久重定向,所有对该 URL 的进一步请求都将透明地重写到目标 URL,而不会触网获取原始 URL。请记住,调试仍然处于打开状态,但是根本没有网络活动的输出。HTTP。它有效。
⁂
HTTP 网络服务不仅限于 GET
请求。如果您想创建新内容怎么办?无论何时您在讨论论坛上发表评论,更新您的网络日志,在 Twitter 或 Identi.ca 等微博服务上发布您的状态,您可能已经在使用 HTTP POST
。
Twitter 和 Identi.ca 都提供了一个简单的基于 HTTP 的 API,用于以 140 个字符或更少的字符发布和更新您的状态。让我们看看 Identi.ca 的 API 文档,了解如何更新您的状态。
Identi.ca REST API 方法:statuses/update
更新验证用户的状态。需要指定以下status
参数。请求必须是POST
。
- URL
https://identi.ca/api/statuses/update.format
- 格式
xml
、json
、rss
、atom
- HTTP 方法
POST
- 需要身份验证
- true
- 参数
status
。必需。您的状态更新的文本。根据需要进行 URL 编码。
这如何运作?要发布一条新消息到 Identi.ca,您需要向 http://identi.ca/api/statuses/update.format
发出 HTTP POST
请求。(format
部分不是 URL 的一部分;您需要用您想要服务器在响应您的请求时返回的数据格式替换它。因此,如果您想要 XML 格式的响应,则应向 https://identi.ca/api/statuses/update.xml
发布请求。)请求需要包含一个名为 status
的参数,其中包含您的状态更新的文本。并且请求需要进行身份验证。
身份验证?当然。要更新您在 Identi.ca 上的状态,您需要证明您的身份。Identi.ca 不是维基百科;只有您才能更新您自己的状态。Identi.ca 使用 HTTP 基本身份验证(也称为 RFC 2617)通过 SSL 提供安全且易于使用的身份验证。httplib2
支持 SSL 和 HTTP 基本身份验证,因此这部分很容易。
POST
请求与 GET
请求不同,因为它包含一个有效负载。有效负载是要发送到服务器的数据。此 API 方法需要的唯一数据是 status
,它应该是URL 编码的。这是一个非常简单的序列化格式,它采用一组键值对(即 字典)并将其转换为字符串。
>>> from urllib.parse import urlencode ① >>> data = {'status': 'Test update from Python 3'} ② >>> urlencode(data) ③ 'status=Test+update+from+Python+3'
urllib.parse.urlencode()
。status
,其值是单个状态更新的文本。POST
请求中“通过网络”发送到 Identi.ca API 服务器的有效负载。>>> from urllib.parse import urlencode >>> import httplib2 >>> httplib2.debuglevel = 1 >>> h = httplib2.Http('.cache') >>> data = {'status': 'Test update from Python 3'} >>> h.add_credentials('diveintomark', 'MY_SECRET_PASSWORD', 'identi.ca') ① >>> resp, content = h.request('https://identi.ca/api/statuses/update.xml', ... 'POST', ② ... urlencode(data), ③ ... headers={'Content-Type': 'application/x-www-form-urlencoded'}) ④
httplib2
处理身份验证的方式。使用 add_credentials()
方法存储您的用户名和密码。当 httplib2
尝试发出请求时,服务器将以 401 Unauthorized
状态代码响应,并列出它支持的哪些身份验证方法(在 WWW-Authenticate
标头中)。httplib2
将自动构造一个 Authorization
标头并重新请求 URL。POST
。☞
add_credentials()
方法的第三个参数是凭据有效的域。您应该始终指定它!如果您省略了域,并在以后在另一个经过身份验证的网站上重新使用httplib2.Http
对象,httplib2
最终可能会将一个网站的用户名和密码泄露到另一个网站。
这就是通过网络传输的内容
# continued from the previous example send: b'POST /api/statuses/update.xml HTTP/1.1 Host: identi.ca Accept-Encoding: identity Content-Length: 32 content-type: application/x-www-form-urlencoded user-agent: Python-httplib2/$Rev: 259 $ status=Test+update+from+Python+3' reply: 'HTTP/1.1 401 Unauthorized' ① send: b'POST /api/statuses/update.xml HTTP/1.1 ② Host: identi.ca Accept-Encoding: identity Content-Length: 32 content-type: application/x-www-form-urlencoded authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2 ③ user-agent: Python-httplib2/$Rev: 259 $ status=Test+update+from+Python+3' reply: 'HTTP/1.1 200 OK' ④
401 Unauthorized
状态代码响应。httplib2
永远不会发送身份验证标头,除非服务器明确要求它们。这就是服务器要求它们的方式。httplib2
立即转过身来,第二次请求相同的 URL。add_credentials()
方法添加的用户名和密码。服务器在成功请求后会返回什么?这完全取决于网络服务 API。在某些协议(如 Atom Publishing Protocol)中,服务器会发送回 201 Created
状态代码和新创建资源的位置,该位置位于 Location
标头中。Identi.ca 返回 200 OK
和一个包含有关新创建资源的信息的 XML 文档。
# continued from the previous example >>> print(content.decode('utf-8')) ① <?xml version="1.0" encoding="UTF-8"?> <status> <text>Test update from Python 3</text> ② <truncated>false</truncated> <created_at>Wed Jun 10 03:53:46 +0000 2009</created_at> <in_reply_to_status_id></in_reply_to_status_id> <source>api</source> <id>5131472</id> ③ <in_reply_to_user_id></in_reply_to_user_id> <in_reply_to_screen_name></in_reply_to_screen_name> <favorited>false</favorited> <user> <id>3212</id> <name>Mark Pilgrim</name> <screen_name>diveintomark</screen_name> <location>27502, US</location> <description>tech writer, husband, father</description> <profile_image_url>http://avatar.identi.ca/3212-48-20081216000626.png</profile_image_url> <url>http://diveintomark.org/</url> <protected>false</protected> <followers_count>329</followers_count> <profile_background_color></profile_background_color> <profile_text_color></profile_text_color> <profile_link_color></profile_link_color> <profile_sidebar_fill_color></profile_sidebar_fill_color> <profile_sidebar_border_color></profile_sidebar_border_color> <friends_count>2</friends_count> <created_at>Wed Jul 02 22:03:58 +0000 2008</created_at> <favourites_count>30768</favourites_count> <utc_offset>0</utc_offset> <time_zone>UTC</time_zone> <profile_background_image_url></profile_background_image_url> <profile_background_tile>false</profile_background_tile> <statuses_count>122</statuses_count> <following>false</following> <notifications>false</notifications> </user> </status>
httplib2
返回的数据始终是 字节,而不是字符串。要将其转换为字符串,您需要使用正确的字符编码对其进行解码。Identi.ca 的 API 始终以 UTF-8 返回结果,因此这部分很容易。结果出现了
⁂
HTTP 不仅限于 GET
和 POST
。它们无疑是最常见的请求类型,尤其是在网络浏览器中。但是网络服务 API 可以超越 GET
和 POST
,而 httplib2
已做好准备。
# continued from the previous example >>> from xml.etree import ElementTree as etree >>> tree = etree.fromstring(content) ① >>> status_id = tree.findtext('id') ② >>> status_id '5131472' >>> url = 'https://identi.ca/api/statuses/destroy/{0}.xml'.format(status_id) ③ >>> resp, deleted_content = h.request(url, 'DELETE') ④
findtext()
方法查找给定表达式的第一个实例并提取其文本内容。在本例中,我们只是在查找一个 <id>
元素。<id>
元素的文本内容,我们可以构建一个 URL 来删除我们刚刚发布的状态消息。DELETE
请求。这就是通过网络传输的内容
send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ① Host: identi.ca Accept-Encoding: identity user-agent: Python-httplib2/$Rev: 259 $ ' reply: 'HTTP/1.1 401 Unauthorized' ② send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ③ Host: identi.ca Accept-Encoding: identity authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2 ④ user-agent: Python-httplib2/$Rev: 259 $ ' reply: 'HTTP/1.1 200 OK' ⑤ >>> resp.status 200
就这样,它消失了。
⁂
httplib2
:
httplib2
项目页面httplib2
代码示例httplib2
httplib2
:HTTP 持久性和身份验证HTTP 缓存
RFC
© 2001–11 Mark Pilgrim