urllib2 源码解析

475次阅读  |  发布于2年以前

1引言

Python 2 中的 requests 库基于 urllib2 模块实现,因此有必要了解 urllib2 模块的 API 使用与原理。

本文将结合 requests 库,详细介绍 urllib2 模块。本文所有描述基于 Python 2。

2 介绍

在使用 urllib2 的 API 之前,简单介绍其中两个重要概念,opener 与 handler。

2.1 opener

urllib2 中使用 opener 获取 URL(fetch a URL)。

调用 urllib2.urlopen 方法时创建默认 opener,也支持用户自定义。

opener 是 urllib2.OpenerDirector 实例,用于管理各种类型的 handler。

2.2 handler

opener 通过 handler 完成请求的调用。

handler 是抽象的处理者,定义了处理请求的相关接口,不同 handler 中对应各种方法的不同实现。

自定义 opener 实际上就是自定义 handler。

3 API 使用

分别使用 urllib2 的 API 完成无参 GTE 请求与传参请求。

3.1 无参 GET 请求

3.1.1 urllib2.urlopen

urllib2 对外提供 urlopen 函数接口,用于获取 URL。

因此,请求调用可以仅一步完成:

>>> import urllib2
>>> response = urllib2.urlopen('http://python.org/') 

response 对象是一个类文件对象。

>>> response
<addinfourl at 81364488L whose fp = <socket._fileobject object at 0x0000000004CC71C8>>
>>> resp.fileno()
744L
>>> resp.fp
<socket._fileobject object at 0x0000000004CC71C8>

因此,调用 read 方法可以获取响应正文,调用 readline 方法可以获取到正文的第一行。

>>> html = response.read()
>>> response.readline()
'<!doctype html>\n'

示例中调用 urllib2.urlopen 一个函数就可以完成请求响应的全过程,内部封装了多层处理,当然这种默认处理并不满足全部场景,因此特殊场景下需要调用内部处理的方法,下面介绍内部处理中用到的部分方法。

3.1.2 urllib2.Request

请求调用可以分两步完成:

先有请求对象,后有响应对象,这样也比较合乎逻辑。

>>> request = urllib2.Request('http://python.org/') 
>>> response = urllib2.urlopen(request)

可见,urllib2.urlopen 函数的第一个参数 url 可以是 URL 地址,也可以是 Request 对象。

3.1.3 build_opener

上面提到,urllib2 中使用 opener 获取 URL,到目前为止,使用的都是默认的 opener。

默认的 handler 仅支持基本功能,不支持代理、cookie 等其他的 HTTP/HTTPS 高级功能,这种情况下可以使用自定义 handler。

自定义 handler 时,请求调用可以分四步完成:

调用 urllib2.urlopen 一个函数完成的请求可以拆分为如下四步,作用相同。

# 构建 HTTPHandler 处理器对象,支持 HTTP 请求
>>> http_handler = urllib2.HTTPHandler()

# 创建支持处理 HTTP 请求的 opener 对象(OpenerDirector 实例对象)
>>> opener = urllib2.build_opener(http_handler)

>>> request = urllib2.Request('http://python.org/') 

# 调用自定义 opener 对象的 open 方法,发送 request 请求
>>> response = opener.open(request)

当然,实际业务中不会这样使用。通常是当默认 handler 无法满足需求时,在不改动源码的前提下通过自定义 handler 实现功能扩展。

比如测试过程中可以通过设置 debuglevel 参数开启 debug log,从而将请求处理中的收包和发包的报头打印在日志中,而不需要人为抓包。

# 默认 debuglevel=0,表示关闭 debug log
>>> http_handler = urllib2.HTTPHandler(debuglevel=1)

比较普遍的应用是在爬虫/反爬虫过程中通过自定义 handler 使用代理 IP。

requests v0.2.0 中就使用到了 build_opener 方法。

如下所示,调用 _get_opener 方法创建 opener 方法时,如果不需要验证,直接返回 urllib.urlopen 方法,否则调用 build_opener 方法创建自定义 opener 并返回 urllib.open 方法。

def _get_opener(self):
    """Creates appropriate opener object for urllib2."""

    if self.auth:

        # 密码管理对象,create a password manager
        authr = urllib2.HTTPPasswordMgrWithDefaultRealm()

        authr.add_password(None, self.url, self.auth.username, self.auth.password)
        handler = urllib2.HTTPBasicAuthHandler(authr)
        opener = urllib2.build_opener(handler)

        # use the opener to fetch a URL
        return opener.open
    else:
        return urllib2.urlopen

3.1.4 install_opener

再新增一步,请求调用可以分五步完成:

示例如下所示,其中在调用 install_opener 方法后调用 urllib2.urlopen 函数发起请求。

>>> http_handler = urllib2.HTTPHandler()
>>> opener = urllib2.build_opener(http_handler)
>>> urllib2.install_opener(opener)
>>> request = urllib2.Request('http://python.org/') 
>>> response = urllib2.urlopen(request)

那么,哪种场景下需要调用 install_opener 方法呢?

如果之后全部请求都需要使用自定义代理(opener) ,可以调用 install_opener 方法将其设置为全局默认,这种情况下,无论调用 urllib2.urlopen,还是 opener.open 函数,都会使用自定义代理。不过通常不需要调用该方法。

requests v0.2.1 中就使用到了 install_opener 方法,将支持文件上传功能的两个自定义 handler 设置为默认 opener。

def register_openers():
    """Register the streaming http handlers in the global urllib2 default
    opener object.

    Returns the created OpenerDirector object."""
    handlers = [StreamingHTTPHandler, StreamingHTTPRedirectHandler]
    if hasattr(httplib, "HTTPS"):
        handlers.append(StreamingHTTPSHandler)

    opener = urllib2.build_opener(*handlers)

    urllib2.install_opener(opener)

    return opener

3.2 传参请求

3.2.1 GET

urllib2 模块完成 GET 请求可以分三步完成:

请求示例如下所示。

>>> import urllib
>>> import urllib2

# 请求地址与请求参数
>>> URL = "https://sl.se"
>>> params_dict = {"lat": "35.696233", "long": "139.570431"}

# 构建请求参数,拼接查询字符串
>>> params_encode = urllib.urlencode(params_dict)
>>> URL_params = "%s?%s" % (URL, params_encode)
>>> URL_params
'https://sl.se?lat=35.696233&long=139.570431'

 # 发送请求
>>> response = urllib2.urlopen(URL_params)

# 处理响应
>>> response.getcode()
200

3.2.2 POST

urllib2 模块完成 POST 请求可以分两步完成:

通过 data 参数区分 GET 与 POST 请求方法。

请求示例如下所示,注意其中没有拼接查询参数与 URL,而是将参数作为 data(body)传给 urlopen 方法。

>>> url = "http://fanyi.youdao.com/translate"
>>> form_data = {
        "type":"AUTO",
        "i":"i love python",
        "doctype":"json",
        "xmlVersion":"1.8",
        "keyform":"fanyi.web",
        "ue":"utf-8",
        "action":"FY_BY_ENTER",
        "typoResult":"true"
    }

>>> data = urllib.urlencode(form_data)
>>> data
'keyform=fanyi.web&ue=utf-8&action=FY_BY_ENTER&i=i+love+python&xmlVersion=1.8&type=AUTO&doctype=json&typoResult=true'

>>> request = urllib2.Request(url, data=data)
>>> response = urllib2.urlopen(request)
>>> response.getcode()
200

4 源码

4.1 urlopen

调用 urlopen 方法是使用 urllib2 模块发起请求时最简单的调用方式。

函数的入参是 URL / Request 对象和数据,出参是类文件对象 response。

_opener = None

def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
            cafile=None, capath=None, cadefault=False, context=None):
    global _opener
    ...
    if context:
        https_handler = HTTPSHandler(context=context)
        opener = build_opener(https_handler)
    elif _opener is None:
        _opener = opener = build_opener()
    else:
        opener = _opener
    return opener.open(url, data, timeout)

urlopen 中调用 build_opener 方法构建全局对象 _opener 并保存,然后调用 open 方法并传入 URL 与数据。

因此,如果程序中多次调用 urlopen,不需要重复构建 opener 对象。

如果 _opener 对象不为空,表明不是第一次调用 urlopen 或者已经调用 install_opener 方法修改了全局默认对象,不需要再构建 opener 对象。

install_opener 方法的实现非常简单,就是将自定义 handler 设置为全局默认。

def install_opener(opener):
    global _opener
    _opener = opener

从注释中也可以看到,urlopen 方法的基本用法与 urllib 库相同,其中一个区别是支持传入 request 实例。

urlopen(url, data=None) -- Basic usage is the same as original
    urllib.  pass the url and optionally data to post to an HTTP URL, and
    get a file-like object back.  One difference is that you can also pass
    a Request instance instead of URL.  Raises a URLError (subclass of
    IOError); for HTTP errors, raises an HTTPError, which can also be
    treated as a valid response.

可见,调用 urlopen 或 open 两种方法均可以完成请求的发送,不过最终都是调用 open 方法,因此 open 方法是 urllib2 的核心逻辑,将在源码解析的最后部分介绍。

下面首先介绍 build_opener 方法。

4.2 opener & handler

首先创建 handler,然后将其绑定到 opener。

4.2.1 build_opener

build_opener 方法入参是 handlers,出参都是 OpenerDirector 对象,其中可以绑定各种不同的 handler。

def build_opener(*handlers):
    """Create an opener object from a list of handlers.

    The opener will use several default handlers, including support
    for HTTP, FTP and when applicable, HTTPS.

    If any of the handlers passed as arguments are subclasses of the
    default handlers, the default handlers will not be used.
    """
    ...
    opener = OpenerDirector()

    # 默认 handler
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
                       FTPHandler, FileHandler, HTTPErrorProcessor]
    if hasattr(httplib, 'HTTPS'):
        default_classes.append(HTTPSHandler)
 ... # 对比去重

    # 将 handler 绑定到 OpenerDirector 实例
    for klass in default_classes:
        opener.add_handler(klass())

    for h in handlers:
        if isclass(h):
            h = h()
        opener.add_handler(h)

    return opener

注意 handlers 是可选参数,用于支持自定义 handler,如果没有的话,将使用默认 handler,如 HTTPHandler、HTTPSHandler。

build_opener 方法中在对比去重用户传入的自定义 handler 与默认 handler 后,统一调用 add_handler 方法将每个 handler 绑定到 OpenerDirector 实例。

如下所示,判断自定义 handler 与 默认 handler 的关系,如果前者是后者的子类或实例化对象,则删除默认 handler,以用户传入的 handler 为准。

    skip = set()
    for klass in default_classes:
        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)  # 删除默认 handler

从注释中也可以看到,build_opener 方法用于创建 OpenerDirector 实例,支持用户自定义 handler。如果自定义 handler 是默认 handler 的子类,则以自定义 handler 为准。

build_opener -- Function that creates a new OpenerDirector instance.
Will install the default handlers.  Accepts one or more Handlers as
arguments, either instances or Handler classes that it will
instantiate.  If one of the argument is a subclass of the default
handler, the argument will be installed instead of the default.

opener 用于管理 handler,而 handler 用于完成实际的请求

opener 中会绑定多个 handler,而 opener 是 urllib2.OpenerDirector 实例,那么,OpenerDirector 中是如何管理各种 handler 的呢?

4.2.2 OpenerDirector

OpenerDirector 用于管理 handler,每种 handler 实现特定的协议,如 HTTPHandler、HTTPRedirectHandler。

The OpenerDirector manages a collection of Handler objects that do
    all the actual work.  Each Handler implements a particular protocol or
    option.  The OpenerDirector is a composite object that invokes the
    Handlers needed to open the requested URL.  For example, the
    HTTPHandler performs HTTP GET and POST requests and deals with
    non-error returns.  The HTTPRedirectHandler automatically deals with
    HTTP 301, 302, 303 and 307 redirect errors, and the HTTPDigestAuthHandler
    deals with digest authentication.

OpenerDirector 类中有 4 个实例属性,分别管理不同类型的 handler,对应请求的不同处理过程,具体包括 open、 request、response、error。

class OpenerDirector:
    def __init__(self):
  ...
        # manage the individual handlers
        self.handle_open = {}
        self.handle_error = {}
        self.process_response = {}
        self.process_request = {}

初始化时 handle 字典为空,调用 add_handler 时会根据方法名将 handler 保存到对应的 handle 字典,字典的 value 是 handler,key 是网络协议名称。

对于一个 handler,调用 add_handler 方法时,如何判断应该保存到哪个字典呢?

handler 与字典的对应关系基于方法名的约定

比如 HTTPHandler 中 http_open 方法名表示实现了 HTTP 协议 open 过程的方法。

实际上,handler 中的方法名只有以下几类:_error_open_request_response,并约定以协议名称作为前缀。

因此,实现了哪类方法就属于哪种 handler,以哪种协议作为前缀就对应 handle 字典中的哪种协议

4.2.3 add_handler

add_handler 方法中遍历 handler 中的全部方法,分别按照_分隔符拆分方法名,获取到协议名称与请求的处理过程。

def add_handler(self, handler):
    ...
    for meth in dir(handler):
        i = meth.find("_")
        protocol = meth[:i]
        condition = meth[i+1:]

获取到协议名称和处理过程以后,就可以根据处理过程,将不同的 handler 保存到对应的 handle 字典。

        ...
        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
        ...

到现在为止,就创建好了 handler 与 opener,下面就是调用 opener.open(url, data, timeout) 方法发起请求了。

4.3 open

open 方法中主要是分别使用 OpenerDirector 中的三个 handle 字典,包括 handle_open、process_response、process_request。

其中 process_request 用于请求预处理,process_response 用于处理响应,handle_open 用于发起请求获取响应,具体在 _open 方法中实现。

def open(self, fullurl, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
    ...
    # 正则匹配 URL 识别网络协议
 protocol = req.get_type()

 # pre-process request
 meth_name = protocol+"_request"
 for processor in self.process_request.get(protocol, []):
  meth = getattr(processor, meth_name)
  req = meth(req)

 response = self._open(req, data)

 # post-process response
 meth_name = protocol+"_response"
 for processor in self.process_response.get(protocol, []):
  meth = getattr(processor, meth_name)
  response = meth(req, response)

 return response

根据 URL 的网络协议获取字典中保存的 handler,然后依次遍历对应协议的 handler 进行处理。

比如对于 request 处理,首先会从 process_request 字典中获取出可以处理 HTTP 请求的 handler,HTTP 请求默认只有一个处理器:HTTPHandler, 因此会使用 HTTPHandler 中的 http_request 方法来处理,其余两个处理过程类似。

上面提到,urllib2.urlopen 函数的第一个参数 url 可以是 URL 也可以是 Request 对象。

原因是 open 方法中首先会判断参数类型,如果是 URL,会自动创建 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)
    ...

4.4 _open

_open 方法中主要是调用 _call_chain 方法执行网络协议对应的 handler。

def _open(self, req, data=None):
    # 使用默认 handler
 result = self._call_chain(self.handle_open, 'default',
         'default_open', req)
 if result:
  return result

    # 使用协议对应的 handler
 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)

_call_chain 中最终调用 handler 中实现的方法。

4.5 _call_chain

_call_chain 方法类似职责链模式,遍历协议对应的全部 handler,查找可以处理该请求的方法,然后调用。

def _call_chain(self, chain, kind, meth_name, *args):
 # 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

5 知识点

5.1 职责链模式

责任链模式(Chain of Responsibility)是一种设计模式。

思想是通过将多个处理器(handler)串成链处理请求,请求在链上传递,直到其中某个处理成功为止。

在实际场景中,财务审批就是一个责任链模式。假设某个员工需要报销一笔费用,审核者可以分为:

用责任链模式设计此报销流程时,每个审核者只关心自己责任范围内的请求,并且处理它。对于超出自己责任范围的,扔给下一个审核者处理,这样,将来继续添加审核者的时候,不用改动现有逻辑。

职责链模式的优点是使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。

类似 workflow 的流程性事务处理过程就可以通过职责链模式实现。不同场景下的业务实现对应不同的 handler,将其组合成链以后,通过提供统一入口兼容变化的需求,扩展性强。

需要注意的是链中每个 handler 的顺序很重要,顺序不对可能导致处理结果异常。

职责链模式的实现方式有多种,其中 urlib2 中 _call_chain 方法中遍历全部 handler,如果返回 None,则交给下一个 handler 处理。

def _call_chain(self, chain, kind, meth_name, *args):
 handlers = chain.get(kind, ())
 for handler in handlers:
  func = getattr(handler, meth_name)

  result = func(*args)
  if result is not None:
   return result

可见,urllib2 基于职责链模式处理请求,并对外提供接口,以支持用户自定义 handler。

因此,urllib2 可以在不修改已有代码的前提下实现功能的扩展,这也符合开闭原则的要求。

Software entities should be open for extension,but closed for modification.

5.2 urllib vs urllib2

5.2.1 urllib API

urllib2 是增强版的 urllib,因此实际上 urllib 也可以完成基础的请求调用。

如下所示,分别使用 urllib 的 API 发起 GET 与 POST 请求。

GET 请求

>>> import urllib
>>> params = urllib.urlencode({"lat": "35.696233", "long": "139.570431"})
>>> f = urllib.urlopen("https://sl.se?%s" % params)
>>> f.getcode()
200

POST 请求

>>> import urllib
>>> params = urllib.urlencode({"lat": "35.696233", "long": "139.570431"})
>>> f = urllib.urlopen("https://sl.se?", params)
>>> f.getcode()
200

5.2.2 关系

urllib2 可以认为是增强版的 urllib,如:

而对于 urllib 中提供的功能,urllib2 中也没有重新造轮子,如:

如下所示,urllib2.AbstractHTTPHandler 类中将请求结果保存到 urllib.addinfourl 类,可见最终返回的 response 是 addinfourl 对象。

class AbstractHTTPHandler(BaseHandler):
    def do_open(self, http_class, req, **http_conn_args):
        ...
        from urllib import addinfourl

        resp = addinfourl(fp, r.msg, req.get_full_url())
        resp.code = r.status
        resp.msg = r.reason
        return resp

因此,Python 2 中通常 urllib 与 urllib2 两者结合使用。

5.3 正则表达式

不同的网络协议对应不同的处理过程,Request 类的 get_type 方法中通过拆分 URL 识别对应的网络协议。

    def get_type(self):
        if self.type is None:
            self.type, self.__r_type = splittype(self.__original)
            if self.type is None:
                raise ValueError, "unknown url type: %s" % self.__original
        return self.type

实际上是调用 urllib.splittype 函数,其中通过调用 re.match 方法匹配 URL 拆分网络协议与路径。

如果正则表达式不匹配,表明 URL 格式不合法,返回 None,否则返回协议名称。

_typeprog = None
def splittype(url):
    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

不过如果当前 URL 对应的网络协议暂不支持会怎么处理呢?

上面提到,_open 方法中会调用三次 _call_chain 方法,分别是默认 handler、协议对应的 handler、异常 handler。当协议不支持时,对应异常 handler。

def _open(self, req, data=None):
    ...
    # 协议异常或没有协议的 handler
 return self._call_chain(self.handle_open, 'unknown',
       'unknown_open', req)

UnknownHandler 类的 unknown_open 方法中抛出 URLError 异常。

class UnknownHandler(BaseHandler):
    def unknown_open(self, req):
        type = req.get_type()
        raise URLError('unknown url type: %s' % type)

6 结论

urllib2 是增强版的 urllib,Python 2 中通常 urllib 与 urllib2 两者结合使用。

urllib2 中有两个重要概念,opener 与 handler。其中:

urllib2 最简单的调用方式是直接调用 urllib2.urlopen 方法,其中将使用默认 opener。

urllib2 基于职责链模式处理请求,并对外提供接口,以支持用户自定义 handler。

每种 handler 对应不同的网络协议,其中可以自定义请求处理过程中的具体实现,并约定方法名的格式为【网络协议 + 处理过程】。

7 待办

本文来自读者朋友「鸟山明」的合作投稿

参考教程

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8