python requests库源码阅读
python requests
库提供了丰富便捷的http
接口,属于python
中最常用的第三方库之一。之前用requests
做并发请求的时候遇到过一些问题,因此想深入了解这个库的用法和实现原理,就试着阅读了这个库的源码,做一下梳理和记录。
代码梳理
-
api.py实现了基本的对外接口,即
get,post,delete
等http
方法名命名的一系列函数,经常在代码中看到有人将这些方法又封装了一个通用函数, 实际上这些方法就是通过调用一个更为通用的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
默认只支持http
和https
两种schema
。
-
adapter.py 最终实现了
session
中依赖的HTTPAdapter
类,这个类通过urllib3
中的Retry
和PoolManager
提供了http
长连接连接池的维护和错误重试机制以及其他一些http请求过程中的细节问题,比如代理的处理等。为了弄清楚基本的session机制实现,我大概看了一下Retry
和PoolManager
的实现。其中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)
启发
-
对于常用类库还是要多了解其源码和实现逻辑,在很多情况下才可以发挥其最大威力。
-
类库的视线需要考虑到各种情况,但通过各种默认方式和参数可以屏蔽掉底层复杂的实现,为最常用功能提供最非常的接口,大多数情况下能用得很爽,如果遇到比较特殊的情况又可以很容易定制。
-
一些非核心功能可以通过Mixin模式来实现,既保证了主干类逻辑的简单清晰,也实现了相关功能。
微信公众号
欢迎关注我的微信公众号: allen_thing(allen独白), 后期主要会在微信公众号分享自己的一些总结和思考
微信公众号二维码