Python之美[从菜鸟到高手]--urllib源码分析

时间:2022-03-05 04:03:21

    urllib提供了较好的封装,可以很方便的读取http,ftp,file等协议数据,本篇只关注http。urllib的底层还是使用httplib模块,相比于httplib,urllib接口更加好用,功能更加强大。支持http代理,可从环境变量中获取代理信息,支持http basic auth,可自动处理302等。但也有不足,如不支持gzip等压缩编码,不支持摘要认证,NTML认证等。

urllib快速使用

    urllib的urlopen方法很好用,代理使用如下:

import urllib
opener=urllib.urlopen('http://www.baidu.com/',proxies={'http':'http://root:root2@192.168.1.101:808'})
print opener.code
urlopen返回一个类文件对象,具有一些列方法,read(),readline(),readlines(),close()等,其中info()方法和readhers属性都是返回http.HTTPMessage实例。HTTPMessage是继承自mimetools.Message,具有读取状态码或邮件头这样格式的内容。Python之美[从菜鸟到高手]--urllib源码分析

注意:http://www.baidu.com/需要在域名后加"/",不然会报"IOError: ('http protocol error', 0, 'got a bad status line', None)”,且代理站点需要添加http前缀。

urllib还有一个比较好用的下载方法,urlretrieve(url, filename=None, reporthook=None, data=None) 该函数可指定回调函数reporthook(blocknum, bs, size),默认下载1024*8字节回调一次,也就是bs大小,blocknum是块数量,其实就是回调的次数,size是下载文件总大小,通过"Content-Length"获取。data是需要post的值,filename是下载文件名。返回一个(本地filename,HTTPMessage)元组
import urllibdef reporthook(bk,bs,total):    print bk*bs,'b'filename,message=urllib.urlretrieve("http://ww.baidu.com/",None,reporthook)print message.getheader('Content-Length')
返回结果:
8192 b
16384 b
11394
你会看到大小不太一样,难免的,因为最后一次可能没有bs大小可读了,但回调还是调用了。
urlretrieve是基于URLopener.retrieve的,在看当作也遇到了一点疑惑,在获取“Content-Length"头信息时,代码如下:
fp = self.open(url, data)headers = fp.info()if "content-length" in headers:    size = int(headers["Content-Length"])
看的我疑惑了,这headers到底是什么类型啊,怎么那么像字典,还能自动处理大小写?
原来这是rfc822.Message实例,定义了__contains__和__getitem__,果然源码之下,没有秘密可言。
class Message:    def __contains__(self, name):        """Determine whether a message contains the named header."""        return name.lower() in self.dict            def __getitem__(self, name):        """Get a specific header, as from a dictionary."""        return self.dict[name.lower()]

urllib源码分析


1.总体流程

  urlopen其实是urllib.FancyURLopener的实例,且可全局缓存,避免了多次创建。FancyURLopener是继承自urlib.URLopener的。FancyURLopener更好的处理了一些特殊状态情况,如302,401,407等,如支持最大10次302跳转。而URLopener出现302等会直接报IOError状态码异常,如果为了获取真实返回状态码需要用URLopener,URLopener通样支持代理等基本特性,如:
Python之美[从菜鸟到高手]--urllib源码分析

当出现状态码异常时,URLopener将调用http_error方法,如果存在“ 'http_error_%d' % errcode”方法的话,http_error将调用。FancyURLopener定义了较多的特殊状态码处理函数,如http_error_302方法。所以说,如果想定制自己的urllib可以通用继承于URLopener或FancyURLopener,并定义自己的状态码处理函数。

2.http basic auth(基本认证)

   这是很简单的一种认证方式,每次访问通过base64加密将"用户名:密码”作为“Authorization”头发送,并没有作为cookie。下面是我简化的请求-响应过程信息
GET / HTTP/1.1Host: 127.0.0.1HTTP/1.1 401 Authorization RequiredWWW-Authenticate: Basic realm="my site"----------------------------------------------------------GET / HTTP/1.1Host: 127.0.0.1Authorization: Basic cm9vdDpyb290HTTP/1.1 200 OK
我们可以简单配置一下apache,修改httpd.conf,在需要认证的的directory下,添加AllowOverride authconfig,
<Directory "c:/wamp/www/">    AllowOverride authconfig    Order Deny,Allow    Deny from all    Allow from 127.0.0.1</Directory>
然后在c:/wamp/www/目录下新建.htaccess,我的如下
authname "my site"authtype basicauthuserfile c:/wamp/www/user.txtrequire valid-user
其中authname也就是提示信息,会出现在“WWW-Authenticate”头的“realm"(以前一直很奇怪,realm到底是什么?)。authuserfile最好绝对路径。
我们知道了认证方式,那么编码也就简单了。
user_passwd, realhost = splituser(realhost) #realhost类似root:root@www.baiud.comif user_passwd:    import base64    auth = base64.b64encode(user_passwd).strip()    else:        auth = None    h = httplib.HTTP(host)    if auth: h.putheader('Authorization', 'Basic %s' % auth)
当url中没有提供用户名:密码时,但站点的确需要认证时,将返回401状态码,这时将调用http_error_401,如果出现"www-authenticate"响应头,将判断是否是basic认证。如果是将调用retry_http_basic_auth,这个函数从缓存中寻找有无该站点的用户名:密码,如果没有将调用prompt_user_passwd,提示用户输入(如果在GUI环境下需要重写该函数),并将用户名:密码缓存,如下

Python之美[从菜鸟到高手]--urllib源码分析

第二次请求是不需要用户名和密码的,用户名密码缓存在auth_cache字典中。(PS:写代码时缓存思想很重要)

3.代理

    代理其实和http basic认证一样,都是通过发送请求头来实现的,下面是我使用工具CCProxy设置本地代理后截取的请求--响应,如下:
GET / HTTP/1.1Host: www.baidu.comHTTP/1.0 407 UnauthorizedServer: ProxyProxy-Authenticate: Basic realm="CCProxy Authorization"GET / HTTP/1.1Host: www.baidu.comProxy-Authorization: Basic cm9vdDpyb290Mg==HTTP/1.1 200 OK
第一次访问百度,由于设置了代理,所以返回了407状态码,”其中Basic realm“就是网页认证对话标题。如果代理需要”用户名:密码“认证,和http基本认证一样,每次都将”用户名:密码“base64加密作为”Proxy-Authorization“。
URLopener构造函数如下:
def __init__(self, proxies=None, **x509):        if proxies is None:            proxies = getproxies()        assert hasattr(proxies, 'has_key'), "proxies must be a mapping"        self.proxies = proxies
我们看到,如果没有设置代理,将调用getproxies()函数获取本地环境变量中的代理。
def getproxies_environment():    proxies = {}    for name, value in os.environ.items():        name = name.lower()        if value and name[-6:] == '_proxy':            proxies[name[:-6]] = value    return proxies
本地环境变量中代理一般设置如下,如在linux中.bashrc设置
export http_proxy="16.111.53.167:808"
export ftp_proxy="16.111.53.167:328"
如果设置代理,将向代理主机发送请求,其余和HTTP Basic认证相同。

urllib提供的有用函数

1. quote(s, safe = '/')   url编码,将保留字符编码为%20格式,主要用来编码path前面信息

    其实这个函数主要用来给path,query编码用的,如果将整个url编码肯定有问题的, 因为默认只有数字+字母+'_.-/'不编码,如果需要传递整个url,可将第二个参数设置为"%/:=&?~#+!$,;'@()*[]|",其实urllib会主动编码的,代码如下:
fullurl = quote(fullurl, safe="%/:=&?~#+!$,;'@()*[]|")

增补:从python2.6版本才会主动编码,2.5以及低版本不会

2. unquote(s):   quote的逆过程。

3. quote_plus(s, safe = ''):  query字符串编码,与quote不同点就是将“空格”用“+"表示,主要用于urlencode函数中

4. unquote_plus(s):   quote_plus的逆过程。

5.urlencode(query,doseq=0) :   将2元素的序列或字典编码成查询字符串,doseq允许第二元素也可以为序列,query字符串都是用quote_plus编码。

Python之美[从菜鸟到高手]--urllib源码分析

注意:这里有必要对quote,quote_plus和urlencode做个说明

quote用于url的编码,而quote_plus用于post的data编码,所以而urlencode内部是用quote_plus实现的,所以urlencode一般只用做post data使用,get时不用。

官方文档说明:

urllib.unquote(string)
Replace %xx escapes by their single-character equivalent.

Example: unquote('/%7Econnolly/') yields '/~connolly/'.

urllib.unquote_plus(string)
Like unquote(), but also replaces plus signs by spaces, as required for unquoting HTML form values.

urllib.urlencode(query[, doseq])
Convert a mapping object or a sequence of two-element tuples to a “percent-encoded” string, suitable to pass to urlopen() above as the optional data argument. This is useful to pass a dictionary of form fields to a POST request. The resulting string is a series of key=value pairs separated by '&' characters, where both key and value are quoted using quote_plus() above. When a sequence of two-element tuples is used as the query argument, the first element of each tuple is a key and the second is a value. The value element in itself can be a sequence and in that case, if the optional parameter doseq is evaluates to True, individual key=value pairs separated by '&' are generated for each element of the value sequence for the key. The order of parameters in the encoded string will match the order of parameter tuples in the sequence. The urlparse module provides the functions parse_qs() and parse_qsl() which are used to parse query strings into Python data structures.


6 .url2pathname:   将file协议url转换为本地路径

    以windows来说,file://host/dirs...,如何是本地可省略host,如      Python之美[从菜鸟到高手]--urllib源码分析      看几个简单结果:      Python之美[从菜鸟到高手]--urllib源码分析   
7. urllib提供了一些列解析url各部分的函数,和urlparse不同的是基本上都是用正则匹配的。 如获取协议的splittype函数,注意_typeprog正则只在第一次的编译,如果有类似情况可以仿写。不过,话说回来,如果用的比较多,并不在意导入该模块时花费一点时间 编译正则对象,完全可以在全局编译。
_typeprog = Nonedef splittype(url):    """splittype('type:opaquestring') --> 'type', 'opaquestring'."""    global _typeprog    if _typeprog is None:        import re        _typeprog = re.compile('^([^/:]+):')    match = _typeprog.match(url)    if match:        scheme = match.group(1)        return scheme.lower(), url[len(scheme) + 1:]    return None, url