Tips 25
HTTP WEB
http web服务是指以编程的方式直接使用HTTP操作从远程服务器发送和接收数据。如果从服务器获取数据使用HTTP GET;如果向服务器上传数据使用HTTP POST.一些高级的HTTP Web服务 API允许使用HTTP put 和HTTP delete创建、修改和删除数据。数据格式同常是xml或json,可以实现创建好并静态的存储下来,或则由服务器端脚本动态生成,并且所有主要原因都包含http库用于下载数据。调试也比较方便,http web服务中每一个资源都有一个唯一的地址(以url的形式存在),可以在浏览器中加载并立即看到原始的数据。
python3的htpp.client是实现http协议的底层库 urllib.request建立在http.client之上的抽象层。它为访问http和ftp服务器提供了标准的API,可以自动跟随http重定向,并且处理了一些常见形式的http认证。
建议使用httplib2,一个第三方的开源库,它比http.client更完整的实现了http协议,又比urllb.request提供了更好的抽象
HTTP
首先得了解http的特点。
缓存
网络访问代价还是比较大,即使在最快的宽带连接上,延迟(从发送请求到开始响应获得数据所花费的时间)仍然很高。路由器的异常,被丢包,中间代理商被工具攻击等等,这些你无法改变。因此,http在设计时就考虑到了缓存。有这么一类设备( 缓存代理服务器),它们能起作用,因为缓存是内建在http协议中的。
当你下载一个图片是,服务器返回包含了以下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头中 public关键字说明这一点)。当你再次请求下载这个图片时,如果其他的缓存服务器有这张图片,会截取你的请求,返回给你这张图片,如果都没有,就会发出网络请求从远程服务器下载这张图片。当每个角色都按协议来做时,http缓存才能发挥作用。 python标准库中的http库不支持缓存,但httplib2支持
最后修改时间
web上的数据可能从不改变,或许总是在变,或则两则之间。http对于这个问题也有对应的解决方案。当你第一次请求数据时,服务器返回一个last-modified头。 如果再次请求同一资源时,可以在请求发送If-Modified-Since头,其值为你上一次返回的时间。如果数据发送过变化,服务器会忽略If-Modified-Since并返回新数据和200状态码。否则会服务器会发回HTTP304状态码,它的意思你请求的时间没变化,不用发给你啦。 python的http库不支持最好修改时间, 但httplib2支持
ETAGS
ETag是另一个和最后修改时间达到同样目的的方法。使用ETag时,服务器返回数据的同在ETag头里面返回一个哈希码(如何生成取决于服务器,要求是数据变时哈希码跟着变)。当你再次请求相同数据时,你在If-None-Match放入ETag值,如果服务器比对一下,发现一样,就会返回304状态码。 同样python的http库不支持ETag,httplib2支持
压缩
http_web服务一般都是在来回运输文本数据。可能是XML、JSON或则就是纯文本,不管是啥压缩之后体积就会小,传输就会快。http支持多种压缩算法,最常见的就是gzip和deflate。当你请求资源时,可以包含Accept-encoding头,里面列出你支持的压缩算法。如果服务器也只支持这种算法,就会返回对应格式的压缩数据。 python http不支持压缩,httplib2支持
重定向
好的url不会变化,但是有很多url会变,比如网站调整了,扩展了等等。每一次向服务器请求时,服务器都会在响应中包含一个状态码。200:就是一切正常 404:就是找不到页面 300系列:就是某种形式的重定向。最常见的就是302和301. 302:临时重定向 资源暂时不在;301:永久重定向,但都会在Location头里给出新的地址。urllib.request模块从服务器获得重定向码时会自动重定向,但他不会反馈,所以每次都需要重定向。而httplib2会永久重定向,它会在本地记录这些重定向,下次发送请求时会自动重写为重定向后的url.
避免通过http重复获取数据
如果你想通过http下载一个资源。
>>> a_url = 'http://diveintopython3.org/examples/feed.xml'
>>> data = urllib.request.urlopen(a_url).read() # python通过http下载非常简单。urllib.request模块有一个方便的函数urlopen(),它接收获取页面的地址,然后返回一个类文件对象。然后调用其read()方法就可以获得网页全部内容。
>>> type(data)
<class 'bytes'> # urlopen().read()方法返回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/'/>
…
如果只是一次下载,这样确实没有问题,但如果定期访问web服务的时候(比如每隔1小时请求一下这个供稿),这样做就显得简单粗暴了。 为啥说简单粗暴呢,我们打开python的http库的调试功能,看看什么东西被发送到线路上。
>>> from http.client import HTTPConnection # 从http.client导入HTTPConnection类
>>> HTTPConnection.debuglevel = 1 # 打开调试
然后再次执行向服务器请求数据,你会看到urlopen()请求了未压缩的数据,所以比较低效。
HTTPLIB2
>>> import httplib2
>>> h = httplib2.Http('.cache') # httplib2的主要接口是Http对象。创建时需要传入一个目录名。
>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')#调用http对象的request方法就可以获取数据。
>>> response.status # request()方法返回两个值,一个是httplib2.Response对象,包含了服务器返回的所有http头,另一个是status
200
>>> content[:52] # content变量包含了返回的数据
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
>>> len(content)
3070
httplib2返回是字节而不是字符。可以根据需要进行转换。
处理缓存
前面我们说应该在创建httplib2.Http对象时提供一个目录名,就是为了缓存。 当你再次发出相同的请求时,就会发现比之前快了很多。让我们打开调试功能看一下
>>> import httplib2
>>> httplib2.debuglevel = 1 # 打开调试功能
>>> h = httplib2.Http('.cache')
>>> response2, content2 = h.request('http://www.vimlinux.com/feeds/atom.xml') # 线路上啥都没返回
>>> response2.status
200
>>> response2.fromcache # 显示是cache获取的数据
True
如果想要打开httplib2的调试开关,需要设置一个模块级的常量(httplib2.debuglevel),然后再创建httplib2.Http对象。关闭是同样要设置。 现在数据缓存着,但你想跳过缓存重新获取。应该使用http的特性来保证你的请求能够到达远程服务器,因为这其中还有多级缓存要跳过。
>>> response2, content2 = h.request('http://www.vimlinux.com/feeds/atom.xml', headers={'cache-control':'no-cache'}) # httplib2允许添加任意的http头部,此次添加no-cache为了跳过缓存
connect: (www.vimlinux.com, 80) ************
send: 'GET /feeds/atom.xml HTTP/1.1\r\nHost: www.vimlinux.com\r\nuser-agent: Python-httplib2/0.10.3 (gzip)\r\naccept-encoding: gzip, deflate\r\ncache-control: no-cache\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Server: GitHub.com
header: Content-Type: application/xml
header: Last-Modified: Thu, 09 Mar 2017 07:40:35 GMT
header: Access-Control-Allow-Origin: *
header: Expires: Thu, 09 Mar 2017 08:44:14 GMT
header: Cache-Control: max-age=600
header: Content-Encoding: gzip
header: X-GitHub-Request-Id: C15C:2A686:36787E9:4761B30:58C11385
header: Content-Length: 2422597
header: Accept-Ranges: bytes
header: Date: Thu, 09 Mar 2017 09:29:38 GMT
header: Via: 1.1 varnish
header: Age: 0
header: Connection: keep-alive
header: X-Served-By: cache-nrt6131-NRT
header: X-Cache: MISS
header: X-Cache-Hits: 0
header: X-Timer: S1489051777.946283,VS0,VE181
header: Vary: Accept-Encoding
header: X-Fastly-Request-ID: 62146dd2576ae50f0f43f9f9117e4b618c0cdb54
# 初始化一个网络请求,并且加入了一个no-cache的头
>>> response2.status
200 # 请求成功。服务器返回了一个新的数据,并更新了本地缓存
>>> response2.fromcache
False
>>> print(dict(response2.items())
... )
{'content-length': '22385455', 'via': '1.1 varnish', 'vary': 'Accept-Encoding', 'x-cache-hits': '0', 'cache-control': 'max-age=600', 'status': '200', 'x-served-by': 'cache-nrt6131-NRT', 'x-cache': 'MISS', 'x-github-request-id': 'C15C:2A686:36787E9:4761B30:58C11385', 'accept-ranges': 'bytes', 'expires': 'Thu, 09 Mar 2017 08:44:14 GMT', 'last-modified': 'Thu, 09 Mar 2017 07:40:35 GMT', '-content-encoding': 'gzip', 'date': 'Thu, 09 Mar 2017 09:29:38 GMT', 'access-control-allow-origin': '*', 'content-location': 'http://www.vimlinux.com/feeds/atom.xml', 'age': '0', 'x-timer': 'S1489051777.946283,VS0,VE181', 'server': 'GitHub.com', 'connection': 'keep-alive', 'x-fastly-request-id': '62146dd2576ae50f0f43f9f9117e4b618c0cdb54', 'content-type': 'application/xml'}
>>>
http缓存设计为尽量最大化的缓存命中率和最小化的网络访问。
HTTPLIB2如何处理LAST-MODIFFIED和ETAG头
Cache-Control和Expires缓存头被称为新鲜度指标。它们通过只缓存,除非缓存过期,不然不会产生任何的网络活动(除非显示的要求跳过缓存)
如果数据可能已改变,但缓存不知道呢?Http因此定义了Last-Modified和Etag头。如果本地缓存不再是最新的数据,客户端请求时可以发送Last-Modified和Etag头进行验证数据是否改变。如果没有改变,服务器会返回304状态码,但不返回数据。
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> response, content = h.request('http://diveintopython3.org/') # 第一次请求该页面,httplib2在请求时发出较少的头,没啥要处理的
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())) # 响应包含多个http头,但没有缓存信息,但包含了ETag和Last-Modified头
{'-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
>>> 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" # httplib2将Etag validator通过if-none-match头发送回服务器
if-modified-since: Tue, 02 Jun 2009 02:51:48 GMT # httplib2将Last-Modified validator通过if-modfied-since发送回服务器
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 304 Not Modified' # 服务器查看这些验证器,查看请求的页面,然后判断出页面在上次请求之后没有变化,返回304状态码
>>> response.fromcache # 客户端httplib2检查到304,所以从缓存加载页面内容
True
>>> response.status # response.status 返回缓存的状态码,没有返回304
200
>>> response.dict['status'] # 服务器返回的状态码(304)可以从response.dict里找到
'304'
>>> len(content) # 数据仍然保存在content变量
6657
HTTP2LIB处理压缩
http2lib支持deflate或gzip格式。
>>> response, content = h.request('http://diveintopython3.org/')
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip # 每次httplib2发送请求,都包含了accept-encoding头来告诉服务器它能处理的deflate或gzip压缩
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'
>>> print(dict(response.items()))
{'-content-encoding': 'gzip', # 服务器返回了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处理重定向
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> response, content = h.request('http://diveintopython3.org/examples/feed-302.xml') # 这个url没有feed,但设置让服务器发出一个正确的重定向
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' # 响应包含了一个Location头给出实际的URL
send: b'GET /examples/feed.xml HTTP/1.1 # httplib2立即跟随重定向,发出一个新的Location(包含正确的地址)
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'
>>> response # 调用request()方法返回的respnse是最终URL的响应
{'status': '200',
'content-length': '3070',
'content-location': 'http://diveintopython3.org/examples/feed.xml', # httplib2会将最终的URL以content-localton加入到response字典
'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'}
你得到的response给了你最终URL的相关信息,如果希望那些最后重定向到最终URL中间的URL的信息呢?
>>> response.previous # 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) # response和response.previous都是httplib2.Response对象
<class 'httplib2.Response'>
>>> type(response.previous)
<class 'httplib2.Response'>
>>> response.previous.previous # 如果重定向多次,该方法可以追溯
>>>
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-302.xml') # 同一个URL,同一个httplib2.Http对象
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-302.xml HTTP/1.1 # 302影响没有缓存,所以httplib2对同一个URL再一次发送请求
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 302 Found' # 再一次,返回302.但是没有继续进行。一旦httplib2收到302Found状态码,再发送新的请求前检查都缓存数据仍然可用
>>> content2 == content
True
永久重定向
>>> response, content = h.request('http://diveintopython3.org/examples/feed-301.xml') # 请求一个不存在的URL,设置服务器执行一个永久重定向到正确URL
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' # 状态码301,但是再次注意什么没发生,因为本地已缓存
>>> response.fromcache # httplib2跟随重定向到缓存里
True
# continued from the previous example
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml') # 这是临时和永久重定向的区别:一旦httplib2跟随了一个永久的重定向,后续的都会到定向的URL
>>> response2.fromcache # 数据从本地缓存获取
True
>>> content2 == content
True
HTTP GET之外
HTTP web服务不限于GET请求。当你要修改服务器上的数据时,就需要POST请求。再发布前,需要进行身份的验证,httplib2支持SSL和HTTP Basic Authentication. POST请求和GET请求不同,因为它包含payload,是你要发送到服务器的数据。这个API方法必须的参数是status,并且它应该是URL编码的。这是一种简单的序列化格式,将一组键值对转换为一个字符串。
>>> from urllib.parse import urlencode # Python带有一个工具函数用于URL编码一个字典:urllib.parse.urlencode()
>>> data = {'status': 'Test update from Python 3'} # 这是Identi.ca API所期望的字典。包含一个键,status, 对应值是状态更新的文本
>>> urlencode(data) # URL编码之后的字符串的样子。这就是会通过线路发送到Identi.ca API服务器的HTTP POST请求的负荷
'status=Test+update+from+Python+3'
>>> 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') # httplib2处理认证的方法,add_credentials()方法记录你的用户名和密码。当httplib2试图执行请求的时候,服务会返回一个401 Unauthorized状态码,并且列出所有它支持的认证方法(WWW-Authenticate头).httplib2会自动构造Authorization头并且重新请求该URL
>>> resp, content = h.request('https://identi.ca/api/statuses/update.xml',
... 'POST', # 请求的类型POST
... urlencode(data), # 发送到服务器的负荷,包含状态消息的URL编码过的字典
... headers={'Content-Type': 'application/x-www-form-urlencoded'}) # 最后告诉服务器负荷是URL编码过的数据
add_credentials()方法的第三个参数是该证书有效的域名。你应该总是指定这个参数,如果忽略了,并且之后重用这个httplib2.Http对象访问另一个需要认证的站点,可能会导致httplib2将一个站点的用户名密码泄露给其他站点。
发送到线路上的数据:
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' # 第一个请求,服务器以401 Unauthorized状态码返回。httplib2从不主动发送认证头,触发服务器明确的要求。
send: b'POST /api/statuses/update.xml HTTP/1.1 # httplib2马上转个身,发出第二次相同的URL
Host: identi.ca
Accept-Encoding: identity
Content-Length: 32
content-type: application/x-www-form-urlencoded
authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2 # 包含了你通过add_credentials()方法加入的用户名和密码
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文档。
HTTP并不限于GET和POST,还能处理一些其他的web事务
>>> from xml.etree import ElementTree as etree
>>> tree = etree.fromstring(content) # 服务器返回的是XML
>>> status_id = tree.findtext('id') # findtext()方法找到对应表达式的第一个实例并抽取它的文本内容
>>> status_id
'5131472'
>>> url = 'https://identi.ca/api/statuses/destroy/{0}.xml'.format(status_id) # 基于id元素的文本内容,我们构造出一个url用于删除刚发布的状态消息
>>> resp, deleted_content = h.request(url, 'DELETE') # 要删除一条消息,只需对该URL执行一个HTTP DELETE请求即可
python的网络功能还是比较强大的,以后再分享python网络爬虫。