您当前位置: 首页 深入 Python 3

难度级别: ♦♦♦♦♢

HTTP 网络服务

心绪不宁,枕头难安。
— 夏洛蒂·勃朗特

 

深入

从哲学角度来说,我可以用 12 个词来描述 HTTP 网络服务:使用 HTTP 操作与远程服务器交换数据。如果您想从服务器获取数据,请使用 HTTP GET。如果您想将新数据发送到服务器,请使用 HTTP POST。一些更高级的 HTTP 网络服务 API 还允许使用 HTTP PUT 和 HTTP DELETE 来创建、修改和删除数据。就这样。没有注册表、没有信封、没有包装器、没有隧道。HTTP 协议中内置的“动词”(GETPOSTPUTDELETE)直接映射到用于检索、创建、修改和删除数据的应用程序级操作。

这种方法的主要优点是简单,而且它的简单性已被证明很受欢迎。数据(通常是 XMLJSON)可以静态构建和存储,也可以由服务器端脚本动态生成,所有主要的编程语言(包括 Python,当然!)都包含一个 HTTP 库用于下载它。调试也更容易;因为 HTTP 网络服务中的每个资源都有一个唯一的地址(以 URL 的形式),您可以将其加载到您的网络浏览器中,并立即查看原始数据。

HTTP 网络服务的示例

Python 3 带有两个用于与 HTTP 网络服务交互的不同库

那么您应该使用哪个呢?两个都不。相反,您应该使用 httplib2,这是一个开源的第三方库,它比 http.client 更全面地实现了 HTTP,但提供了比 urllib.request 更好的抽象。

要理解为什么 httplib2 是正确的选择,您首先需要了解 HTTP。

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-ControlExpires 标头告诉您的浏览器(以及您和服务器之间的任何缓存代理)可以将此图像缓存长达一年。一年!并且如果在下一年,您访问另一个包含指向此图像链接的页面,您的浏览器将从其缓存中加载图像,不会生成任何网络活动

但是等等,事情变得更好了。假设您的浏览器出于某种原因从您的本地缓存中清除图像。也许它没有足够的磁盘空间;也许您手动清除了缓存。无论如何。但是 HTTP 标头说这些数据可以被公共缓存代理缓存。(从技术上讲,重要的是标头没有说的是什么;Cache-Control 标头没有 private 关键字,所以默认情况下这些数据是可以缓存的。)缓存代理被设计为拥有大量的存储空间,可能远超过您的本地浏览器分配的空间。

如果您的公司或 ISP 维护着缓存代理,那么代理可能仍然缓存了图像。当您再次访问 diveintomark.org 时,您的浏览器将在其本地缓存中查找图像,但不会找到,因此它将发出网络请求以尝试从远程服务器下载它。但是,如果缓存代理仍然有一个图像副本,它将拦截该请求并从缓存中提供图像。这意味着您的请求永远不会到达远程服务器;事实上,它永远不会离开您公司的网络。这将加快下载速度(更少的网络跃点)并为您的公司节省资金(更少从外部世界下载的数据)。

HTTP 缓存只有在每个人都尽职尽责的情况下才能发挥作用。一方面,服务器需要在其响应中发送正确的标头。另一方面,客户端需要在两次请求相同数据之前了解并尊重这些标头。中间的代理不是万能药;它们只能像服务器和客户端允许它们那样聪明。

Python 的 HTTP 库不支持缓存,但 httplib2 支持。

Last-Modified 检查

有些数据从不改变,而其他数据一直在改变。在这两者之间,有一个巨大的数据领域,这些数据可能已经改变,但还没有改变。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,服务器会将一个哈希代码放在 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
  1. ETag 通常包含在引号中,但引号是值的一部分。这意味着您需要将引号发送回服务器的 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 有几种不同的方法来表明资源已移动。两种最常见的技术是状态码 302301。状态码 302 是一个临时重定向;它表示“哦,那个被临时移到了这里”(然后在 Location 标头中给出临时地址)。状态码 301 是一个永久重定向;它表示“哦,那个被永久移到了这里”(然后在 Location 标头中给出新的地址)。如果您收到一个 302 状态码和一个新的地址,HTTP 规范规定您应该使用新的地址来获取您所请求的内容,但下次您想访问同一个资源时,应该重试旧的地址。但如果您收到一个 301 状态码和一个新的地址,您应该从那时起使用新的地址。

urllib.request 模块从 HTTP 服务器接收到适当的状态码时,它会自动“跟随”重定向,但它不会告诉您它已经这样做了。您最终会得到您所请求的数据,但您永远不会知道底层库“帮助”您进行了重定向。因此,您将继续使用旧地址,每次您都会被重定向到新地址,每次 urllib.request 模块都会“帮助”您进行重定向。换句话说,它将永久重定向与临时重定向视为相同。这意味着两次往返而不是一次,这对服务器和您都不利。

httplib2 会为您处理永久重定向。它不仅会告诉您永久重定向发生了,还会在本地跟踪它们,并在请求重定向的 URL 之前自动重写它们。

如何不通过 HTTP 获取数据

假设您想通过 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/'/>
  …
  1. 在 Python 中通过 HTTP 下载任何东西都非常容易;事实上,它只是一行代码。urllib.request 模块有一个方便的 urlopen() 函数,它接受您想要的页面的地址,并返回一个类似文件的对象,您可以从中 read() 来获取页面的全部内容。它不可能再容易了。
  2. 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…
  1. 正如我在本章开头提到的,urllib.request 依赖于另一个标准 Python 库 http.client。通常情况下,您不需要直接接触 http.client。(urllib.request 模块会自动导入它。)但是,我们在这里导入它,以便我们可以切换 HTTPConnection 类(urllib.request 使用它来连接到 HTTP 服务器)的调试标志。
  2. 现在调试标志已设置,有关 HTTP 请求和响应的信息将实时打印出来。如您所见,当您请求 Atom 订阅时,urllib.request 模块会向服务器发送五行内容。
  3. 第一行指定您正在使用的 HTTP 动词,以及资源的路径(减去域名)。
  4. 第二行指定我们从哪个域名请求此订阅。
  5. 第三行指定客户端支持的压缩算法。正如我之前提到的,urllib.request 默认情况下不支持压缩
  6. 第四行指定正在发出请求的库的名称。默认情况下,这是 Python-urllib 加上版本号。urllib.requesthttplib2 都支持更改用户代理,只需在请求中添加一个 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
  1. urllib.request.urlopen() 函数返回的 response 包含服务器发回的所有 HTTP 标头。它还包含用于下载实际数据的函数;我们将在稍后讨论它。
  2. 服务器会告诉您它何时处理您的请求。
  3. 此响应包含一个 Last-Modified 标头。
  4. 此响应包含一个 ETag 标头。
  5. 数据长度为 3070 字节。请注意这里没有:Content-encoding 标头。您的请求表明您只接受未压缩数据 (Accept-encoding: identity),事实证明,此响应包含未压缩数据。
  6. 此响应包含缓存标头,这些标头表明此订阅可以缓存最多 24 小时(86400 秒)。
  7. 最后,通过调用 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
  1. 服务器仍然发送相同的“智能”标头数组:Cache-ControlExpires 以允许缓存,Last-ModifiedETag 以启用“未修改”跟踪。甚至 Vary: Accept-Encoding 标头也暗示服务器将支持压缩,只要您提出要求即可。但您没有。
  2. 再一次,此请求获取了全部 3070 字节……
  3. ……与您上次得到的 3070 字节完全相同。

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
  1. httplib2 的主要接口是 Http 对象。出于将在下一节中看到的原因,您应该始终在创建 Http 对象时传递一个目录名。该目录不必存在;httplib2 将在必要时创建它。
  2. 拥有 Http 对象后,检索数据就像使用您想要的数据的地址调用 request() 方法一样简单。这将对该 URL 发出 HTTP GET 请求。(在本章的后面,您将看到如何发出其他 HTTP 请求,例如 POST。)
  3. request() 方法返回两个值。第一个是 httplib2.Response 对象,它包含服务器返回的所有 HTTP 标头。例如,status 代码为 200 表示请求成功。
  4. 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 参数。

[I support RFC 3023 t-shirt]

但这还不止。现在字符编码信息可以存在两个地方:XML 文档本身以及 Content-Type HTTP 头部。如果信息在这两个地方都存在,哪一个会获胜?根据 RFC 3023(我发誓这不是我编造的),如果 Content-Type HTTP 头部中给出的媒体类型为 application/xmlapplication/xml-dtdapplication/xml-external-parsed-entity 或者 application/xml 的任何子类型,比如 application/atom+xmlapplication/rss+xml 甚至 application/rdf+xml,那么编码将是

  1. Content-Type HTTP 头部中 charset 参数给出的编码,或者
  2. 文档中 XML 声明中 encoding 属性给出的编码,或者
  3. UTF-8

另一方面,如果 Content-Type HTTP 头部中给出的媒体类型为 text/xmltext/xml-external-parsed-entity 或者像 text/AnythingAtAll+xml 这样的子类型,那么文档中 XML 声明的 encoding 属性将被完全忽略,编码将是

  1. Content-Type HTTP 头部中 charset 参数给出的编码,或者
  2. 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
  1. 这并不令人惊讶。它与上次你做的相同,只是你将结果放入两个新变量中。
  2. HTTP status 再次为 200,与上次相同。
  3. 下载的内容与上次相同。

所以……谁在乎呢?退出 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
  1. 让我们开启调试并查看 网络传输的内容。这是 httplib2 中开启 http.client 中调试功能的等效操作。httplib2 将打印发送到服务器的所有数据以及发送回来的某些关键信息。
  2. 使用与之前相同的目录名称创建一个 httplib2.Http 对象。
  3. 请求与之前相同的 URL似乎什么都没有发生。更准确地说,没有任何数据被发送到服务器,也没有任何数据从服务器返回。没有任何网络活动。
  4. 然而,我们确实“接收”了一些数据——实际上,我们收到了所有数据。
  5. 我们还“接收”了指示“请求”成功的 HTTP 状态码。
  6. 关键在于:此“响应”是由 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'}
  1. httplib2 允许你向任何传出的请求添加任意 HTTP 头。为了绕过所有缓存(不仅仅是你的本地磁盘缓存,还有你与远程服务器之间的任何缓存代理),在 headers 字典中添加一个 no-cache 头。
  2. 现在你看到 httplib2 发起了一个网络请求。httplib2 双向理解并尊重缓存头——作为传入响应的一部分以及作为传出请求的一部分。它注意到你添加了 no-cache 头,因此它完全绕过了它的本地缓存,然后别无选择,只能访问网络来请求数据。
  3. 此响应不是从你的本地缓存生成的。你当然知道这一点,因为你看到了传出请求的调试信息。但能够以编程方式验证这一点很好。
  4. 请求成功;你再次从远程服务器下载了整个提要。当然,服务器还与提要数据一起发送了完整的 HTTP 头。这包括缓存头,httplib2 使用这些头更新其本地缓存,以期在下次你请求此提要时避免网络访问。关于 HTTP 缓存的一切都旨在最大程度地提高缓存命中率并减少网络访问。即使你这次绕过了缓存,远程服务器也真的很感谢你下次能够缓存结果。

httplib2 如何处理 Last-ModifiedETag

Cache-ControlExpires 缓存头 被称为新鲜度指示器。它们明确告诉缓存,在缓存过期之前,你可以完全避免所有网络访问。这正是你在 上一节中看到的行为:给定一个新鲜度指示器,httplib2 不会生成任何网络活动来提供缓存的数据(当然,除非你明确地 绕过缓存)。

但对于数据可能已更改但实际上没有更改的情况怎么办?HTTP 为此定义了 Last-ModifiedEtag 头。这些头被称为验证器。如果本地缓存不再新鲜,客户端可以在下次请求中发送验证器,以查看数据是否确实发生了更改。如果数据没有更改,服务器会发送一个 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
  1. 这次,我们将下载网站的主页,而不是提要,该主页是 HTML。由于这是你第一次请求此页面,因此 httplib2 可以利用的东西很少,它只发送最少的请求头。
  2. 响应包含许多 HTTP 头……但没有缓存信息。但是,它确实包含了 ETagLast-Modified 头。
  3. 在我创建此示例时,此页面的大小为 6657 字节。它可能已经更改了,但不用担心。
# 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
  1. 你再次请求同一个页面,使用同一个 Http 对象(以及同一个本地缓存)。
  2. httplib2ETag 验证器发送回服务器,位于 If-None-Match 头中。
  3. httplib2 还将 Last-Modified 验证器发送回服务器,位于 If-Modified-Since 头中。
  4. 服务器查看了这些验证器,查看了你请求的页面,并确定自你上次请求以来页面没有更改,因此它发送回一个 304 状态码以及无数据
  5. 回到客户端,httplib2 注意到 304 状态码,并从其缓存中加载页面内容。
  6. 这可能有点令人困惑。实际上有两个状态码——304(这次从服务器返回,导致 httplib2 查看其缓存)和 200(上次从服务器返回,并与页面数据一起存储在 httplib2 的缓存中)。response.status 返回来自缓存的状态。
  7. 如果你想获得从服务器返回的原始状态码,你可以通过查看 response.dict 来获得它,response.dict 是从服务器返回的实际头的字典。
  8. 但是,你仍然会在 content 变量中获得数据。通常,你不需要知道为什么响应是从缓存中提供的。(你可能甚至不关心它是否是从缓存中提供的,这也没关系。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'}
  1. 每次 httplib2 发送请求时,它都会包含一个 Accept-Encoding 头,告诉服务器它可以处理 deflategzip 压缩。
  2. 在本例中,服务器使用 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'
  1. URL 上没有提要。我已经设置了我的服务器,使其发出一个指向正确地址的临时重定向。
  2. 这是请求。
  3. 这是响应:302 Found。此处未显示,此响应还包含一个指向实际 URLLocation 头:http://diveintopython3.org/examples/feed.xml
  4. 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'}
  1. 您从对 request() 方法的单个调用中获取的 响应 是来自最终 URL 的响应。
  2. httplib2 将最终 URL 添加到 响应 字典中,作为 content-location。这不是来自服务器的标头;它是特定于 httplib2 的。
  3. 顺便说一下,此提要被 压缩
  4. 而且可缓存。(这很重要,您稍后会看到。)

您获取的 响应 提供有关最终 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 
>>>
  1. response.previous 属性保存对 httplib2 为获取当前响应对象而跟随的先前响应对象的引用。
  2. responseresponse.previous 都是 httplib2.Response 对象。
  3. 这意味着您可以检查 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
  1. 相同的 URL,相同的 httplib2.Http 对象(因此具有相同的缓存)。
  2. 302 响应未被缓存,因此 httplib2 发送对相同 URL 的另一个请求。
  3. 服务器再次以 302 响应。但请注意没有发生什么:从未对最终 URL http://diveintopython3.org/examples/feed.xml 发出第二个请求。该响应已缓存(请记住您在前面的示例中看到的 Cache-Control 标头)。当 httplib2 收到 302 Found 代码后,它在发出另一个请求之前检查了它的缓存。缓存中包含 http://diveintopython3.org/examples/feed.xml 的最新副本,因此无需重新请求它。
  4. 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
  1. 同样,此 URL 实际上并不存在。我已经将我的服务器设置为对 http://diveintopython3.org/examples/feed.xml 发出永久重定向。
  2. 结果出现了:状态代码 301。但再次注意没有发生什么:没有对重定向 URL 发出请求。为什么?因为它已经在本地缓存。
  3. httplib2 “跟随”了重定向,直接进入它的缓存。

但是等等!还有更多!

# continued from the previous example
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml') 
>>> response2.fromcache 
True
>>> content2 == content 
True
  1. 以下是临时重定向和永久重定向之间的区别:一旦 httplib2 跟随了永久重定向,所有对该 URL 的进一步请求都将透明地重写到目标 URL而不会触网获取原始 URL。请记住,调试仍然处于打开状态,但是根本没有网络活动的输出。
  2. 是的,此响应是从本地缓存中检索的。
  3. 是的,您获得了整个提要(来自缓存)。

HTTP。它有效。

超越 HTTP GET

HTTP 网络服务不仅限于 GET 请求。如果您想创建新内容怎么办?无论何时您在讨论论坛上发表评论,更新您的网络日志,在 Twitter 或 Identi.ca 等微博服务上发布您的状态,您可能已经在使用 HTTP POST

Twitter 和 Identi.ca 都提供了一个简单的基于 HTTPAPI,用于以 140 个字符或更少的字符发布和更新您的状态。让我们看看 Identi.ca 的 API 文档,了解如何更新您的状态。

Identi.ca REST API 方法:statuses/update
更新验证用户的状态。需要指定以下 status 参数。请求必须是 POST

URL
https://identi.ca/api/statuses/update.format
格式
xmljsonrssatom
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 支持 SSLHTTP 基本身份验证,因此这部分很容易。

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'
  1. Python 带有一个实用函数来 URL 编码字典:urllib.parse.urlencode()
  2. 这就是 Identi.ca API 正在寻找的字典类型。它包含一个键 status,其值是单个状态更新的文本。
  3. 这就是 URL 编码字符串的样子。这就是将在您的 HTTP 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'}) 
  1. 这就是 httplib2 处理身份验证的方式。使用 add_credentials() 方法存储您的用户名和密码。当 httplib2 尝试发出请求时,服务器将以 401 Unauthorized 状态代码响应,并列出它支持的哪些身份验证方法(在 WWW-Authenticate 标头中)。httplib2 将自动构造一个 Authorization 标头并重新请求 URL
  2. 第二个参数是 HTTP 请求的类型,在本例中为 POST
  3. 第三个参数是要发送到服务器的有效负载。我们正在发送一个包含状态消息的 URL 编码字典。
  4. 最后,我们需要告诉服务器有效负载是 URL 编码数据。

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'                                  
  1. 在第一次请求之后,服务器以 401 Unauthorized 状态代码响应。httplib2 永远不会发送身份验证标头,除非服务器明确要求它们。这就是服务器要求它们的方式。
  2. httplib2 立即转过身来,第二次请求相同的 URL
  3. 这一次,它包含了您使用 add_credentials() 方法添加的用户名和密码。
  4. 它成功了!

服务器在成功请求后会返回什么?这完全取决于网络服务 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>
  1. 请记住,httplib2 返回的数据始终是 字节,而不是字符串。要将其转换为字符串,您需要使用正确的字符编码对其进行解码。Identi.ca 的 API 始终以 UTF-8 返回结果,因此这部分很容易。
  2. 这就是我们刚刚发布的状态消息的文本。
  3. 这是新状态消息的唯一标识符。Identi.ca 使用它来构建一个 URL,用于在网络上查看该消息。

结果出现了

screenshot showing published status message on Identi.ca

超越 HTTP POST

HTTP 不仅限于 GETPOST。它们无疑是最常见的请求类型,尤其是在网络浏览器中。但是网络服务 API 可以超越 GETPOST,而 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') 
  1. 服务器返回了 XML,对吧?你知道 如何解析 XML
  2. findtext() 方法查找给定表达式的第一个实例并提取其文本内容。在本例中,我们只是在查找一个 <id> 元素。
  3. 根据 <id> 元素的文本内容,我们可以构建一个 URL 来删除我们刚刚发布的状态消息。
  4. 要删除一条消息,您只需向该 URL 发出 HTTP 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
  1. “删除此状态消息。”
  2. “对不起,戴夫,我恐怕做不到。”
  3. “未经授权 哼哼。请删除此状态消息…
  4. …这是我的用户名和密码。”
  5. “包您满意!”

就这样,它消失了。

screenshot showing deleted message on Identi.ca

进一步阅读

httplib2:

HTTP 缓存

RFC

© 2001–11 Mark Pilgrim