Python urllib2源码
开篇
这段时间有空就会看看urllib2的源码,里面可以学习的东西还真不少,也有些值的借鉴的思想,比如关于一系列handler处理的操作。另外里面还用了个设计模式,应该是command模式。这些东西以后慢慢分析。
首先说下这里面个需要关注的几个对象:
1、build_opener:
用来构建处理器(handlers),建造一些默认的或者通过参数传递进来的handler到openerdirector中待用。
2、 openerdirector:
在其中,建立process_request和process_response两个字典,根据添加时handler中的方法来将handler分别放入两个字典中,其中的key是handler的协议(如HTTPHandler,就是http),调用openerdirctor的open方法时会首先调用process_request字典中对应协议的handler进行处理,然后处理之后的结果在通过processs_response字典中的handler进行处理,最终返回数据。
3、request对象:
在调用openerdirector中的open方法时会先根据传入的url构建request对象。这个对象的内容主要是这个url的协议(http或者https),还有会对url进行分解,参数处理,域名处理等等一系列的动作。
上面三个是在后面的分析中都会用到的,这里就先大概有个印象,下篇开始到代码内部穿梭。
[补充]
觉得篇文章没有起到给大家一个整体概念的作用,这里再补充下。
大体的一个流程就是,在使用urllib2.urlopen(url)的时候,首先就是由build_opener来构建一个OpenerDirector实例,构建的过程其实就是将一些列handler处理器放入到OpenerDirector的属性中,然后调用OpenerDirector的open方法,这个方法首先会根据你的url以及data参数生成一个Request对象,Request根据是否有data这个数据来确定是发送get请求还是post请求。
有了Request之后,就会经过之前被添加到OpenerDirector的属性中的那些handler来处理,最后处理成一个response对象。这个response类似于文件对象,有read、readline、readlines等方法。
大概流程就是这样。
简单的urlopen
对上面的几个对象有个基本概念之后,再来深究下代码,从一个最普通的urllib2.urlopen()开始,先来熟悉下第一个重点对象:build_opener这个函数。
大家最为熟悉的一段代码:
res = urllib2.urlopen('http://python.org')
这段代码的作用就是打开 http://python.org
这个网站,返回一个response对象。
下面咱们来深入到这个urlopen函数中,来看下代码:
def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
"""
对外的访问函数
"""
global _opener
if _opener is None:
_opener = build_opener()
return _opener.open(url, data, timeout)
在程序第一次执行urlopen操作的时候,其实就是构建了一个全局的_opener对象,然后用这个_opener对象来处理url以及data。这样做的好处就是如果你在程序中要多次调用urlopen,就不会频繁构建opener对象了。当然这个opener也不是一次加载就再也不可变了,urllib2提供了install_opener这个方法,你可以在客户端调用build_opener然后用前面的那个install_opener来加载。
这段代码很简单,起作用就是构建一个opener对象,所以咱来看下它是如何构建这个_opener对象的。
不过在此之前需要先了解下我们一直在说的opener是什么?
这个_opener其实就是OpenerDirector的一个实例,OpenerDirector上篇已经大概说了,目前我们需要了解的就是在这个对象中定义了几个字典属性,这些字典就是用来存放对应的handler的。
self.handlers = []
# manage the individual handlers
self.handle_open = {}
self.handle_error = {}
self.process_response = {}
self.process_request = {}
最主要的三个字典是process_request、handle_open、process_response,分别存放处理request、打开request、处理response的handler。
所谓的handler是什么呢?顾名思义,就是处理器,目前只需要知道有一群handler,分别用来处理不同的对象,然后对应的处理结果。
有了这样的认识我们先来看下build_opener()函数是如何来构建这个OpenerDirector对象的。
def build_opener(*handlers):
import types
def isclass(obj):
return isinstance(obj, (types.ClassType, type))
opener = OpenerDirector()
#这里定义了一系列的handler类
default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
HTTPDefaultErrorHandler, HTTPRedirectHandler,
FTPHandler, FileHandler, HTTPErrorProcessor]
if hasattr(httplib, 'HTTPS'):#判断是否支持https
default_classes.append(HTTPSHandler)
skip = set()
for klass in default_classes:
#处理用户函数参数handler中是否有相同的类在默认类中。
for check in handlers:
if isclass(check):
if issubclass(check, klass):
skip.add(klass)
elif isinstance(check, klass):
skip.add(klass)
for klass in skip:
default_classes.remove(klass)
#这一步,将默认的类实例化之后加入opener中
for klass in default_classes:
opener.add_handler(klass())
#然后把参数中的handler类实例化,加到OpenerDirector中。
for h in handlers:
if isclass(h):
h = h()
opener.add_handler(h)
return opener
这个函数的整个过程其实相对简单,只是把系统默认的handler和用户传入的自定义handler参数进行了对比弃重。最后把所有的handler都实例化通过opener.add_handler方法添加给OpenerDirector。最后返回构建好的OpenerDirector实例。
关于这个build_opener就先说到这里,下篇再来说OpenerDirector的add_handler的具体流程。
另外,最近一直在思考一个问题,如何把这些(我学到的东西,比如urllib2)东西能够更好的,更清晰易懂的让读者明白。自己理解urllib2的源码不难,难的是以何种方式或者说何种组织结构来写能让人更容易懂。不知道各位有没有什么好的建议或者好的书籍推荐。
目前我自己的想法就是尽量让每篇文章涉及的未知知识点少,每篇文章也尽量内容单一,这样读起来会不会容易些?
探索OpenerDirector的add_handler
OpenerDirector是怎么把这些handler分类的
上篇文章说到,在build_opener中只是调用了OpenerDirector的add_handler方法,并不是直接操作的属性来完成handler的添加的。那么来看看OpenerDirector.add_handler具体做了些什么工作。
这个函数的作用其实很简单,就是传进来的Handler进行分类。既然是分类那就要有分类的依据了,那么分类的依据是什么呢?
看一下这四个属性就知道了:
self.handle_open = {}
self.handle_error = {}
self.process_response = {}
self.process_request = {}
它要把你传递来的Handler分别放到这四个字典中,显然对应的Handler就应该会包含open或者error或者resonse或者request的属性或者方法,说到这里来看下代码就了解了:
def add_handler(self, handler):
'''
根据协议添加handler到不同的字典中,
所有的handler都会存放到handlers这个list中。
'''
if not hasattr(handler, "add_parent"):
raise TypeError("expected BaseHandler instance, got %r" %
type(handler))
added = False
for meth in dir(handler):
#过滤掉命名恰好和下面判断规则一致的函数。
# 但这几个其实并不是需要的函数。
if meth in ["redirect_request", "do_open", "proxy_open"]:
# oops, coincidental match
continue
i = meth.find("_")
protocol = meth[:i]
condition = meth[i+1:]
if condition.startswith("error"):
j = condition.find("_") + i + 1
kind = meth[j+1:]
try:
kind = int(kind)
except ValueError:
pass
lookup = self.handle_error.get(protocol, {})
self.handle_error[protocol] = lookup
elif condition == "open":
kind = protocol
lookup = self.handle_open
elif condition == "response":
kind = protocol
lookup = self.process_response
elif condition == "request":
kind = protocol
lookup = self.process_request
else:
continue
handlers = lookup.setdefault(kind, [])
if handlers:
bisect.insort(handlers, handler)
else:
handlers.append(handler)
added = True
if added:
#bisect.insort(self.handlers, handler)
handler.add_parent(self)
从这个函数我们可以看到关于分类的具体处理过程,其实就是通过遍历这个handler对象的所有方法,然后根据其中是否存在某指定方法来进行分类的。比如HttpHanlder中有http_open这个方法,那么就会被放到handle_open中。至于代码中其他的操作都是对一些基本属性和对那些会产生冲突的方法的过滤。
最后存入字典的key也需要注意一下,这个key就是对应的协议,而此事的值并不是单独的handler对象,而是一个列表,这说明,如果有两个Handler(比如AHandler和BHandler)中含有同样的http_open方法,如果是这样的话,在后面要处理对应的http open请求的话就需要通过这两个handler依次处理。
最后还有一个被我注释掉的一句代码,这个其实urllib2作者也有写注释,handlers这个列表只是为了保持兼容。
每个handler类都继承同一个BaseHandler,拥有add_parent方法,这个方面的作用是为了在handler中同OpenerDirector进行通信。
这个函数的功能也就这些了,不过从这里我看到了另外的东西,就是:约定。
所有的这些都是基于一个约定,约定handler中的关键函数一定要是网络协议加上对应的方法,约定每个handler必须有一个add_parent方法,以及其他的一些约定。
了解这些约定的目的一个是方便理解urllib2在处理url的过程,另外一个就是方便自己以后编写一些扩展handler。
不知道说得是不是够清晰,我自己觉得还不是很清晰,或许在写到最后一篇的时候才会真正清晰。
这一篇和上一篇把urlopen中的构建opener对象的过程都学习了一下,下一篇就来学习这两篇构建opener是怎么处理你给定的url的。
用opener打开你的url
在前面两篇文章 《urllib2源码解读二(简单的urlopen)》 和 《urllib2源码解读三》 中已经构造了一个opener了,我分析的过程看起来比较麻烦,其实理解之后发现也就那些逻辑罢了。有了这个opener之后,我们就可以用它来打开/读取url。整个过程都在opener.open(url)这个函数中。
这个函数的流程是清晰的,接受三个参数:fullurl,data,timeout。fullurl其实有两种形式:一种是url,另一种是Request对象。通过data参数来控制发送什么方式的http请求,GET还是POST。函数处理一个url的大体步骤是这样的:
构造Request对象。
对Request进行预处理,主要是晚上一个Request的信息,如header的处理或者cookie的处理。
然后用httplib中的对应协议的类,对这个Request进行处理。(httplib 是python中http 协议的客户端实现,用来与 HTTP 服务器进行交互)
最后就是扫尾了,看看你返回来的Response是否是一个有错误,有错误的就进行错误处理,比如说抛出一个“urlopen error...”这样的错误。
上一步没有错误的话,你就会得到一个经过httplib处理完成之后返回的Response对象,这个Response有点像一个文件对象,直接用read()即可。
基于上面的步骤,贴山代码来瞅一眼:
def open(self, fullurl, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
'''
一开始先建立Request对象,
然后处理使用request的handler处理request请求,
从上一步得到response之后,通过处理response的handler来处理
其中,在处理request的时候,会用到上面的职责链。
'''
# accept a URL or a Request object
if isinstance(fullurl, basestring):
req = Request(fullurl, data)
else:
req = fullurl
if data is not None:
req.add_data(data)
req.timeout = timeout
protocol = req.get_type()
# 预处理Request
meth_name = protocol+"_request"
for processor in self.process_request.get(protocol, []):
meth = getattr(processor, meth_name)
req = meth(req)
# 处理Request
response = self._open(req, data)
#处理Resonse
meth_name = protocol+"_response"
for processor in self.process_response.get(protocol, []):
meth = getattr(processor, meth_name)
response = meth(req, response)
return response
只需看那三块代码即可,都是基于之前文章中已经学习到的OpenerDirector的三个属性,处理方式都是遍历一遍对应协议的handler,然后挨个处理一遍或者说加工一遍更切贴,很像是一个流水线的工厂。
就拿第一个处理——request处理来说,首先会从process_request中获取出能处理http请求的handler,默认的情况下这个process_request中的http协议只对应一个处理器:HTTPHandler。因此会使用HTTPHandler中的http_request方法来处理。其余的两个处理过程也是一样。
这段代码并不是完全,因为其中还调用了一个私有方法(从命名上来说)self._open(req, data),先看下代码,然后再说下我对其中的疑问。
def _open(self, req, data=None):
#用默认的open进行处理
result = self._call_chain(self.handle_open, 'default',
'default_open', req)
if result:
return result
#用协议对应的open处理
protocol = req.get_type()
result = self._call_chain(self.handle_open, protocol, protocol +
'_open', req)
if result:
return result
#啥也没有得到,说明协议不对或者协议为包含的预期handler中,因此就是未知的处理。
return self._call_chain(self.handle_open, 'unknown',
'unknown_open', req)
def _call_chain(self, chain, kind, meth_name, *args):
""" 类职责链模式,通过遍历list里面的所有handler
来查找能够处理该请求的方法
"""
# Handlers raise an exception if no one else should try to handle
# the request, or return None if they can't but another handler
# could. Otherwise, they return the response.
handlers = chain.get(kind, ())
for handler in handlers:
func = getattr(handler, meth_name)
result = func(*args)
if result is not None:
return result
_open代码中的注释很详细了,而_call_chain这段代码其实就是把for processor in self.process_request.get(protocol, [])....放到一个函数里了。 大概清晰之后,说说我的疑问。
疑问一、为啥要单独写一个_open函数,这三个处理过程都大致一样,一块放到一个函数中应该很清晰。
自问自答曰:可能是在open一个Request时的过程稍微复杂些,因此提取处理代码会更加清晰。我觉得这个理由很好。
疑问二、既然提取出来_call_chain这个函数,为啥不把对Request和Response的处理也用这个函数来做。
这个的原因我想还是为了保证三个处理的独立,区分更明显些吧。
这些疑问在以后的不断实践中回得到答案的,关于urllib2的分析就到此为止吧。
在对代码的分析学习中,最大的收获就是知道了一种程序的组织结构,用建造者模式或者说是职责连模式(知道是啥模型的不妨指点下)来处理多种请求,另外还有一点,函数不是越短越美,而是越清晰越美。