美文网首页程序员Python 运维
python requests库源码阅读

python requests库源码阅读

作者: allen哦 | 来源:发表于2017-10-28 23:58 被阅读1275次

python requests库提供了丰富便捷的http接口,属于python中最常用的第三方库之一。之前用requests做并发请求的时候遇到过一些问题,因此想深入了解这个库的用法和实现原理,就试着阅读了这个库的源码,做一下梳理和记录。

代码梳理

  • api.py实现了基本的对外接口,即get,post,deletehttp方法名命名的一系列函数,经常在代码中看到有人将这些方法又封装了一个通用函数, 实际上这些方法就是通过调用一个更为通用的request方法来实现的:

      def request(method, url, **kwargs):
          with sessions.Session() as session:
              return session.request(method=method, url=url, **kwargs)
    

可以看到这个request方法的是通过sessions.Session()来实现的,一直只知道requests中的 session是用来维持长链接的,但其实requests提供的基本http方法底层也是通过session实现
的,只不过每一个基本方法调用完,这个session就结束了,其相关的资源(包括连接,cookies等)也就
释放了。

  • session.py 实现了整个Session类, 以及一些session功能依赖的外部方法和类(比如其中merge_setting函数实现了将每个请求的参数和session参数的合并,从而可以让一个session中所有请求共用的参数只需要在session中设置,而每个请求可以设置自己特有的参数,同时不影响其他请求,而SessionRedirectMixin类则实现了redirect相关的一些逻辑,这部分逻辑是session必须的,但是又不是session的核心功能,因此通过Mixin类来实现),Session类也暴露了以http方法命名的一系列对外接口,session最核心的功能就是保持会话,因此在prepare_request方法中实现了将session中的headers,cookies,auth等关键参数和每个request的这些参数合并,组合出完整的http请求,最终通过send方法发送出去send方法拿到http请求响应以后,又会将服务端设置的cookies等信息保存在session中,以待下一个request使用。send方法最终发送请求是根据请求schema确定一个实现了BaseAdapter接口的对象来实现的,具体如下:

      # __init__方法部分代码
      self.adapters = OrderedDict()
      self.mount('https://', HTTPAdapter())
      self.mount('http://', HTTPAdapter())
      
      # mount方法,保证后mount的adapter有更高的优先级
      def mount(self, prefix, adapter):
          self.adapters[prefix] = adapter
          keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
    
          for key in keys_to_move:
              self.adapters[key] = self.adapters.pop(key)
      
      # send方法部分代码
      # Get the appropriate adapter to use
      adapter = self.get_adapter(url=request.url)
      r = adapter.send(request, **kwargs)
      
      # get_adapter方法
      def get_adapter(self, url):
          for (prefix, adapter) in self.adapters.items():
    
          if url.lower().startswith(prefix):
              return adapter
              
          raise InvalidSchema("No connection adapters were found for '%s'" % url)
    

也可以看到requests默认只支持httphttps两种schema

  • adapter.py 最终实现了session中依赖的HTTPAdapter类,这个类通过urllib3中的RetryPoolManager提供了http长连接连接池的维护和错误重试机制以及其他一些http请求过程中的细节问题,比如代理的处理等。为了弄清楚基本的session机制实现,我大概看了一下RetryPoolManager的实现。其中Retry控制着重试需要的一些参数,Retry类的参数如下:

       def __init__(self, total=10, connect=None, read=None, redirect=None, status=None,
               method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None,
               backoff_factor=0, raise_on_redirect=True, raise_on_status=True,
               history=None, respect_retry_after_header=True)
    

    其中total是可以重试的总次数,HTTPAdapter中这个参数默认值为3,而connect,read,redirect等则是指该失败状态下最多可以重试的次数,默认情况下等于可以充实的总次数,Retry在每次重试前可以sleep一段时间,这个时间是通过一个简单的退火算法来实现的,backoff_factor控制这个算法的退火因子,其默认值为0,也就是失败直接重试,不进行sleep.

      # 退火算法部分代码(`consecutive_errors_len`是请求失败次数),获取距离下次重试需要`sleep`的时间
      if consecutive_errors_len <= 1:
          return 0
    
      backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
      return min(self.BACKOFF_MAX, backoff_value)
    

PoolManager类则通过一个RecentlyUsedContainer管理着一定数量的http连接池,每一个相同的(schema, host, port)维护一个连接池,每个连接池维护一定数量的http连接,HTTPAdapter类提供的默认值为10个连接池,每个连接池最多维护10个连接。

  • models.py实现了requests中的实体类,如Request, Response等, 也可以看到在Response中通过__bool__方法的重写和很多方法的 @property 使用提供了极简的对外接口。

      # 通过ok属性可以查看Response状态码是否正常
      # Returns True if :attr:`status_code` is less than 400.
      @property
      def ok(self):
      try:
          self.raise_for_status()
      except HTTPError:
          return False
      return True
      
      
      # __bool__方法重写提供了更加方便的检查response状态的方法
      def __bool__(self):
          return self.ok
    
  • auth.py实现了http basic认证和 http digest认证的方法, 可以直接使用

  • structures.py实现了requests依赖的基本数据结构,比如存储headers所使用的CaseInsensitiveDict

定制化

requests提供了非常简易的接口供用户使用,但是并不代表requests底层实现就非常简陋,其底层有相当多的考量,但是这些考量在简单的接口中被一些默认的使用方式或参数屏蔽掉了,平时遇到比较复杂的问题,就需要定制化,自己在一个项目中通过代理模式做过一个简单的参数和异常处理的定制化。

# 通过这个类实现异常和错误的统一处理
class _ErrorResponse(Response):

    def __init__(self, reason):
        super(_ErrorResponse, self).__init__()
        self.reason = reason

    @property
    def ok(self):
        return False

    def __bool__(self):
        return self.ok
    

# requests.Session类的代理类,实现一系列的定制化    
class _SessionWrap(object):
    DEFAULT_RETRY = 3
    
    #引入退火因子,可能会导致失败重试时间延迟增大
    DEFAULT_BACKOFF_FACTOR = 1
    
    HTTP_METHODS = ('HEAD', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH')
    
    # 对pool数量和每个pool中维护的连接数量定制,可以根据实际情况调整
    DEFAULT_POOL_CONNS = 100
    DEFAULT_POOL_MAXSIZE = 100

    def __init__(self, retry=DEFAULT_RETRY,
                 backoff_factor=DEFAULT_BACKOFF_FACTOR):
        
        #retry的定制
        self._retry = Retry(total=retry, backoff_factor=backoff_factor)
        self._http_adapter = HTTPAdapter(pool_connections=self.DEFAULT_POOL_CONNS,
                                         pool_maxsize=self.DEFAULT_POOL_MAXSIZE,
                                         max_retries=retry)
        self._session = requests.session()
        self._session.mount("http://", self._http_adapter)
        self._session.mount("https://", self._http_adapter)
        
    # 实现上下文管理器协议,提供更简约、安全的接口

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        return self._session.close()


     # 通过这个方法来统一处理异常和错误
    def _wrap_http(self, method):
        def http_request(url, **kwargs):
            try:
                response = getattr(self._session, method)(url, **kwargs)
                if not response:
                    msg = u'http request {0} failed, with method {1}, kwargs: {2},' \
                          u'return status_code: {3}, content: {4}'.format(
                            response.url, method, json.dumps(kwargs, ensure_ascii=False),
                            response.status_code, response.content)
                    logger.error(msg)
                    response.reason = msg
            except Exception as e:
                msg = u'http request {0} failed, with method {1}, kwargs: {2}, error: {3}'.format(
                    url, method, json.dumps(kwargs, ensure_ascii=False), str(e))
                logger.exception(msg)
                response = _ErrorResponse(reason=msg)
            return response
        return http_request


     # 代理方法和属性至self._session
    def __getattr__(self, item):
        if item in self.__dict__:
            return self.__dict__[item]
        elif item.upper() in self.HTTP_METHODS:
            return self._wrap_http(item.lower())
        else:
            return getattr(self._session, item)


# 也效仿requests中的session.py提供一个简约的函数接口,屏蔽掉_SessionWrap的复杂实现逻辑
def http_session(retry=3, backoff_factor=1):
    return _SessionWrap(retry=retry, backoff_factor=backoff_factor)

启发

  1. 对于常用类库还是要多了解其源码和实现逻辑,在很多情况下才可以发挥其最大威力。

  2. 类库的视线需要考虑到各种情况,但通过各种默认方式和参数可以屏蔽掉底层复杂的实现,为最常用功能提供最非常的接口,大多数情况下能用得很爽,如果遇到比较特殊的情况又可以很容易定制。

  3. 一些非核心功能可以通过Mixin模式来实现,既保证了主干类逻辑的简单清晰,也实现了相关功能。

微信公众号

欢迎关注我的微信公众号: allen_thing(allen独白), 后期主要会在微信公众号分享自己的一些总结和思考


微信公众号二维码

相关文章

网友评论

    本文标题:python requests库源码阅读

    本文链接:https://www.haomeiwen.com/subject/nneepxtx.html