在Python 3中,可以使用一个第三方的开源库 httplib2 来处理 HTTP Web 服务。它比自带的模块 http.client 更完整的实现了http协议,同时比另一个自带的模块 urllib.request 提供了更好的抽象。例如:

  1. Python的http库不支持缓存,而httplib2支持。
  2. Python的http库不支持最后修改时间检查,而httplib2 支持。
  3. Python的http库不支持ETag,而httplib2支持。
  4. Python的http库不支持压缩,但httplib2支持。
  5. urllib.request模块在从http服务器收到对应的状态码的时候会自动“跟随”重定向, 但它不会告诉你它这么干了。你最后得到了你请求的数据,但是你永远也不会知道下层的库友好的帮助你跟随了重定向;httplib2 帮你处理了永久重定向。它不仅会告诉你发生了永久重定向,而且它会在本地记录这些重定向,并且在发送请求前自动重写为重定向后的url。

我们来举个例子,你想要通过http下载一个资源, 比如说一个Atom 供稿。作为一个供稿, 你不会只下载一次,你会一次又一次的下载它。 (大部分的供稿阅读器会美一小时检查一次更新。) 让我们先用最粗糙和最快的方法来实现它,接着再来看看怎样改进。

HTTP Get

简单粗暴的方法就是使用 urllib 模块:

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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/'/>

说明

  • 在Python中通过http下载东西是非常简单的; 实际上,只需要一行代码。urllib.request模块有一个方便的函数urlopen() ,它接受你所要获取的页面地址,然后返回一个类文件对象,您只要调用它的read()方法就可以获得网页的全部内容。没有比这更简单的了。
  • urlopen().read()方法总是返回bytes对象,而不是字符串。记住字节仅仅是字节,字符只是一种抽象。 http 服务器不关心抽象的东西。如果你请求一个资源,你得到字节。 如果你需要一个字符串,你需要确定字符编码,并显式的将其转化成字符串。

如果需要定期访问Web服务,上面的做法就显得很低效和粗暴了。充分利用HTTP的缓存、压缩技术将可以实习更高效的做法。于是 httplib2 闪亮登场。

要使用httplib2, 请创建一个 httplib2.Http 类的实例。

创建 httplib2.Http 实例

示例

1
2
3
4
5
6
7
8
9
>>> 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对象时总是应该传入一个目录名(例如本例中的.cache),具体原因你会在下一节看见。目录不需要事先存在,httplib2会在必要的时候创建它。
  • 一旦你有了Http对象, 获取数据非常简单,以你要的数据的地址作为参数调用request()方法就可以了。这会对该url执行一个http GET请求. (这一章下面你会看见怎样执行其他http 请求, 比如 POST。)
  • request() 方法返回两个值。第一个是一个httplib2.Response对象,其中包含了服务器返回的所有http头。比如, status为200 表示请求成功。
  • content 变量包含了http服务器返回的实际数据。数据以bytes对象返回,不是字符串。 如果你需要一个字符串,你需要确定字符编码并自己进行转换。

你很可能只需要一个httplib2.Http对象。当然存在足够的理由来创建多个,但是只有当你清楚创建多个的原因的时候才应该这样做。从不同的url获取数据不是一个充分的理由,重用Http对象并调用request()方法两次就可以了。

使用缓存

之后,退出你的Python交互 shell 然后打开一个新的会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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的本地缓存构造出来的。你创建httplib2.Http对象是传入的目录里面保存了所有httplib2执行过的操作的缓存。
  • 你刚刚请求过这个url的数据。那个请求是成功的(状态码: 200)。该响应不仅包含feed数据,也包含一系列缓存头,告诉那些关注着的人这个资源可以缓存长达24小时(Cache-Control: max-age=86400, 24小时所对应的秒数)。 httplib2 理解并尊重那些缓存头,并且它会在.cache目录(你在创建Http对象时提供的)保存之前的响应。缓存还没有过期,所以你第二次请求该url的数据时, httplib2不会去访问网络,直接返回缓存着的数据。

如果你想要打开httplib2的调试开关,你需要设置一个模块级的常量(httplib2.debuglevel), 然后再创建httplib2.Http对象。如果你希望关闭调试,你需要改变同一个模块级常量, 接着创建一个新的httplib2.Http对象。

跳过缓存

假设你有数据缓存着,但是你希望跳过缓存并且重新请求远程服务器。由于缓存可能还存留在不受你控制的中继代理服务器里,因此简单删除本地缓存并不足以解决问题。你应该使用http的特性来保证你的请求最终到达远程服务器,而不是修改本地缓存。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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头就可以了。

处理Last-Modified和ETag头

Cache-Control和Expires缓存头被称为新鲜度指标(freshness indicators)。他们毫不含糊告诉缓存,你可以完全避免所有网络访问,直到缓存过期。而这正是你在前一节所看到的: 给出一个新鲜度指标, httplib2 不会产生哪怕是一个字节的网络活动就可以提供缓存了的数据(当然除非你显式的要求跳过缓存).

那如果数据可能已经改变了, 但实际没有呢? http 为这种目的定义了Last-Modified和Etag头。 这些头被称为验证器(validators)。如果本地缓存已经不是新鲜的,客户端可以在下一个请求的时候发送验证器来检查数据实际上有没有改变。如果数据没有改变,服务器返回304状态码,但不返回数据。 所以虽然还会在网络上有一个来回,但是你最终可以少下载一点字节。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> 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

现在在此请求同一页面,使用同一个Http对象(以及同一个本地缓存)。httplib2 将ETag validator 通过If-None-Match头发送回服务器。httplib2 也将Last-Modified validator 通过If-Modified-Since头发送回服务器。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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

服务器查看这些验证器(validators), 查看你请求的页面,然后判读得出页面在上次请求之后没有改变过, 所以它发回了304 状态码不带数据。

处理压缩

http支持两种类型的压缩。httplib2都支持。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> 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压缩。
  • 这个例子中,服务器返回了gzip压缩过的负载,当request()方法返回的时候,httplib2就已经解压缩了响应的体(body)并将其放在 content变量里。如果你想知道响应是否压缩过, 你可以检查response['-content-encoding'];否则,不用担心了.

处理重定向

http 定义了 两种类型的重定向: 临时的和永久的。

临时重定向

对于临时重定向,除了跟随它们其他没有什么特别要做的, httplib2 会自动处理跟随。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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'

“跟随” 一个重定向就是这个例子展示的那么多。httplib2 发送一个请求到你要求的url。服务器返回一个响应说“不,不, 看那边.” httplib2 给新的url发送另一个请求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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()方法返回的response最终url的响应。httplib2 会将最终的 url(在这里是feed.xml而不是上面的fee2.xml的url) 以 content-location 加入到 response 字典中。这不是服务器返回的头,它特定于httplib2。

如果你希望那些最后重定向到最终url的中间url的信息呢?httplib2 也能帮你。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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跟随那个响应获得了当前的响应对象。responseresponse.previous 都是 httplib2.Response 对象。这意味着你可以通过response.previous.previous 来反向跟踪重定向链到更前的请求。

对于临时重定向你不需要做什么特别的处理。httplib2 会自动跟随它们,而一个url重定向到另一个这个事实上不会影响httplib2对压缩,缓存, ETags, 或者任何其他http特性的支持。

永久重定向

永久重定向同样也很简单。

1
2
3
4
5
6
7
8
9
10
# 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

  • 又一次,这个url实际上并不存在。服务器已将其永久重定向到http://diveintopython3.org/examples/feed.xml.
  • 这就是: 状态码 301。 但是再次注意什么没有发生: 没有发送到重定向后的url的请求。为什么没有? 因为它已经在本地缓存了。
  • httplib2 “跟随” 重定向到了它的缓存里面。

但是等等! 还有更多!

1
2
3
4
5
6
# 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 Post

当你在论坛上发表一个评论,更新你的博客,在 Twitter 或者 Identi.ca 这样的微博客上面发表状态消息的时候, 你很可能已经使用了http POST。

Twitter 和 Identi.ca 都提供一个基于http的简单的api来发布并更新你状态(不超过140个字符)。让我们来看看Identi.ca的关于更新状态的api文档 :

1
2
3
4
5
6
7
8
9
10
11
12
13
Identi.ca 的rest api 方法: statuses/update
更新已认证用户的状态。需要下面格式的status参数。请求必须是POST.
url
https://identi.ca/api/statuses/update.format
Formats
xml, json, rss, atom
http Method(s)
POST
Requires Authentication
true
Parameters
status. Required. The text of your status update. url-encode as necessary.

怎么操作呢?要在 Identi.ca 发布一条消息, 你需要提交一个http POST请求到http://identi.ca/api/statuses/update.format。(format字样不是url的一部分; 你应该将其替换为你希望服务器返回的请求的格式。所以如果需要一个xml格式的返回。你应该向https://identi.ca/api/statuses/update.xml发送请求。) 请求需要一个参数status, 包含了你的状态更新文本。并且请求必须是已授权的。Identi.ca 使用建立在ssl之上的HTTP Basic Authentication (也就是RFC 2617) 来提供安全但方便的认证。httplib2 支持 ssl 和 http Basic Authentication, 所以这部分很简单。

POST 请求同 GET 请求不同, 因为它包含负荷(payload)。负荷是你要发送到服务器的数据。这个api方法必须的参数是status, 并且它应该是url编码过的。 这是一种很简单的序列化格式,将一组键值对(比如字典)转化为一个字符串。Python 带有一个工具函数用于url编码一个字典: urllib.parse.urlencode()

示例

1
2
3
4
5
6
7
8
9
10
>>> 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'})

说明

  • 第5行是 Identi.ca api 所期望的字典。
  • 第6行是httplib2处理认证的方法。 add_credentials()方法记录你的用户名和密码。当 httplib2 试图执行请求的时候,服务器会返回一个401 Unauthorized状态码, 并且列出所有它支持的认证方法(在 WWW-Authenticate 头中). httplib2会自动构造Authorization头并且重新请求该url。
  • 第二个参数是http请求的类型。这里是POST。
  • 第三个参数是要发送到服务器的负荷 。我们发送包含状态消息的url编码过的字典。
  • 最后,我们得告诉服务器负荷是url编码过的数据。

add_credentials()方法的第三个参数是该证书有效的域名。你应该总是指定这个参数! 如果你省略了这个参数,并且之后重用这个httplib2.Http对象访问另一个需要认证的站点,可能会导致httplib2将一个站点的用户名密码泄漏给其他站点。

发送到线路上的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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'

服务器返回客户端的数据

请求成功后服务器返回什么?这个完全由web 服务 api决定。 在一些协议里面(就像 Atom Publishing Protocol ),服务器会返回201 Created状态码,并通过Location提供新创建的资源的地址。Identi.ca 返回200 OK和一个包含新创建资源信息的xml 文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 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返回的数据总是字节串(bytes), 不是字符串。另外,上面第10行是这条状态消息的唯一标识符。Identi.ca 用这个标识来构造在web上查看该消息的url。

HTTP DELETE

http 并不只限于 GET 和 POST。 它们当然是最常见的请求类型,特别是在web浏览器里面。 但是web服务api会使用GET和POST之外的东西, 对此httplib2也能处理。

示例

下面将示例如何执行 HTTP DELETE 删除一条状态消息。要删除一条状态消息,首先需要找到该状态消息的 id。

1
2
3
4
5
6
7
8
# 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')

之后对该url执行一个http DELETE请求就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

http 缓存

RFCS

Comments