基础阶段
之前一直都有在实践中使用 Requests 库,基本上算是 Python 领域网络请求必备的,之前也一直有听过 Http for human 的说法,Requests 作为一个封装良好的代码库,一直被认为是值得一读的。
在实际的项目中,对 Requests 库有过简单的使用,主要是用于发起 get()
和 post()
请求,因此在阅读源码之前,先完整过了一遍 Requests 官方文档 。Requests 的使用基本没有特别复杂的地方,由于 Requests 的良好封装,使用者可以直接使用 Requests 提供的几个封装良好的方法即可。
本文选择的版本为目前的 v2.22.0,一般情况下,如果希望先从最基础的版本开始的,可以先从更早版本的代码进行阅读。
基础流程
一般情况下,我们使用 Requests 只是为了发起单个网络请求,我们按照最基础的发起单个 get()
请求流程去查看内部处理的顺序,大致的流程如下:
- 创建单个
Session
对象; - 调用
Session.request()
方法发起实际的网络请求,得到返回对象Response
对象;- 利用请求的参数构建
Request
对象; - 利用
Request
对象构建出PrepareRequest
对象用于发起网络请求; - 将
PrepareRequest
对象作为参数调用HTTPAdapter
发起实际网络请求,得到Response
对象的响应;
- 利用请求的参数构建
- 返回
Response
对象作为网络请求返回值;
按照基础的流程来看,Requests 中的几个基础模块包括如下所示:
- 网络请求参数类,包括
Request
和PrepareRequest
- 网络请求返回类,
Response
- 会话管理类,
Session
- 实际网络请求处理类,包括
BaseAdapter
和HTTPAdapter
事实上,阅读文档的话,还能发现 Requests 中还包含一些其他内容,比如 Cookie
管理, Auth
身份验证。在这篇文章暂时不详细介绍,后续再单独介绍相关细节。下面分别对现有的主要功能模块进行介绍:
网络请求参数类
网络请求参数类主要用于保存用户发起网络请求相关的参数,后续可以将保存的参数的对象 PrepareRequest
直接发送给 HTTPAdapter
对象进行实际的网络请求。一个令人费解的地方就在于,为什么会需要两个类 Request
和 PrepareRequest
,而不是直接使用 PrepareRequest
呢?
在前面的基础流程中可以看到,构建用于保存网络请求参数的 PrepareRequest
类主要包括两步:
p = PreparedRequest()
p.prepare(...)
那么看起来直接使用 PrepareRequest
就可以进行实际的网络的网络请求了。那么就看下 Request
也提供的 prepare()
方法做了什么呢?具体的实现如下所示:
# class Request
def prepare(self):
p = PreparedRequest()
p.prepare(
method=self.method,
url=self.url,
headers=self.headers,
files=self.files,
data=self.data,
json=self.json,
params=self.params,
auth=self.auth,
cookies=self.cookies,
hooks=self.hooks,
)
return p
可以看到 Request.prepare()
只是调用了前面的 PrepareRequest
的数据构造的基础流程,和手动构造 PrepareRequest
数据是完全等价了,完全可以不使用 Request
类构建网络请求参数。
那么接下来我们看下构建请求参数对象的 PrepareRequest.prepare()
方法做了什么必要的准备呢?代码如下所示:
# class PrepareRequest
def prepare(self,
method=None, url=None, headers=None, files=None, data=None,
params=None, auth=None, cookies=None, hooks=None, json=None):
self.prepare_method(method)
self.prepare_url(url, params)
self.prepare_headers(headers)
self.prepare_cookies(cookies)
self.prepare_body(data, files, json)
self.prepare_auth(auth, url)
self.prepare_hooks(hooks)
看起来是进行不同类型的基础数据的准备,选择其中一个方法查看相关实现:
# class PrepareRequest
def prepare_method(self, method):
self.method = method
if self.method is not None:
self.method = to_native_string(self.method.upper()
可以看到 prepare_method()
就是将数据进行必要的编解码,将数据转换为与版本无关的数据。其他的 prepare_url()
, prepare_headers()
等方法都做了类似的工作,以及一些其他的数据准备,就不深入准备数据的细节了。
总结下来,网络请求参数类用于保存用户发起网络请求相关的数据,包括method, url, headers 等信息,在数据准备阶段,将数据进行必要的编解码,保存至 PrepareRequest
对象中,后续网络请求模块可以直接使用此对象进行实际的网络请求。
Session 会话管理
理论上我们可以直接通过 PrepareRequest
发起网络请求,为什么需要构造一个 Session 对象,在 Session 对象的上下文中发起网络请求呢?
关于这个问题,官方文档中已经有了相关的介绍 ,使用 Session 对象可以让你跨请求保持参数,在同一个 Session 实例中发出的网络请求可以保持 cookie,同一主机的 TCP 请求会被重用,从而带来性能提升。
在 Requests 中发出网络请求存在着两种方式,分别为如下所示:
-
使用
requests.get()
方式发起网络请求,这种方式可以最终实际调用的代码为:with sessions.Session() as session: return session.request(method=method, url=url, **kwargs)
可以看到实际上也是创建了一个新的
Session
对象用于发起网络请求 -
使用
Session.get()
方式发起网络请求这种方式最终调用
Session.request()
发起网络请求
可以看到两种方式发起网络请求最终是殊途同归,但是可以看到明显的不同之处,第一种方式每次都是创建一个新的 Session 对象,必然无法使用 Session 对象提供的跨请求保持参数的特性,第二种方式可以保持使用同一个 Session 发起网络请求,因此是可以进行跨请求保持参数的。
以 官方文档的 Session 介绍 提供的网站进行实践可以看到第一种方式确实实现了跨请求保持 cookie。那么接下来的问题就是:Session 是如何跨请求保持 cookie 的呢?
由于请求相关的信息都会保存至 PrepareRequest
对象中,那么我们可以跟踪请求参数构建的过程就应该可以看到了:
# class Session
def prepare_request(self, request):
# 从 Request 对象中获取 cookies
cookies = request.cookies or {}
# 在 cookies 类型不是 CookieJar 时进行类型转换
if not isinstance(cookies, cookielib.CookieJar):
cookies = cookiejar_from_dict(cookies)
# 合并本次请求 cookies 和 Session 中的 cookies
merged_cookies = merge_cookies(
merge_cookies(RequestsCookieJar(), self.cookies), cookies)
# 构造实际的网络请求参数
p = PreparedRequest()
p.prepare(
method=request.method.upper(),
url=request.url,
files=request.files,
data=request.data,
json=request.json,
headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
params=merge_setting(request.params, self.params),
auth=merge_setting(auth, self.auth),
cookies=merged_cookies,
hooks=merge_hooks(request.hooks, self.hooks),
)
return p
通过上面的实现可以看到,在构建网络请求参数时,调用了 merge_cookies()
方法将 Session 中的 cookies 与 本次请求的 cookies 合并了,因此使用同一个 Session 对象发起网络请求时才能实现跨请求保持 cookie。至于其他的参数,可以看到调用了 merge_setting()
方法进行了合并。
那么看到了保持参数的秘密在于将本次请求参数与 Session 中的请求参数进行合并,那么接下来的问题就来了,类似 cookie 这样的请求参数是何时写入 Session 中的呢?
初步猜测应该是在请求返回的时候写入的,如果不确定,也可以比较容易确认,盯着 Session.cookies
属性就好了,必然会存在写入的情况。追踪到的实现如下所示:
# class Session
def send(self, request, **kwargs):
# 发起实际的网络请求
adapter = self.get_adapter(url=request.url)
r = adapter.send(request, **kwargs)
# 返回值的历史中存在 cookie,保持至 Session.cookies 中
if r.history:
for resp in r.history:
extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
# 当前请求的 cookies 保持至 Session.cookies 中
extract_cookies_to_jar(self.cookies, request, r.raw)
return r
总结下来,利用 Session 对象,确实可以实现跨请求的参数保持,实现的方式是通过在发起请求时,合并 Session 中的请求参数与本次请求的参数。而在网络请求返回时,会将请求的必要信息,比如 cookie 保存在 Session 中。
实际的网络请求
实际的网络请求类包括 BaseAdapter
和 HTTPAdapter
。其中 BaseAdapter
只定义了基础的接口,而 HTTPAdapter
是对 BaseAdapter
的实现。
从名字上可以大致猜测实现的是适配器模式,即将原有接口进行封装后提供出新的接口。之前了解到 Requests 是依赖 urllib3 进行实际的网络请求的,因此可以合理估计是对 urllib3 接口的封装,提供一个便于处理现有的 PrepareRequest
类型请求,返回 Response
结构的接口。
根据 BaseAdapter
可以了解到,需要提供的接口为 send()
和 close()
,其中 send()
是用于发起网络请求,close()
用于执行清理。下面可以看下 HTTPAdapter
实现的网络请求处理:
# class HTTPAdapter
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
# 获取网络连接
conn = self.get_connection(request.url, proxies)
# 判断是否是块网络请求
chunked = not (request.body is None or 'Content-Length' in request.headers)
if not chunked:
# 非块请求直接调用 url_open() 进行网络请求
resp = conn.urlopen(
method=request.method,
url=url,
body=request.body,
headers=request.headers,
redirect=False,
assert_same_host=False,
preload_content=False,
decode_content=False,
retries=self.max_retries,
timeout=timeout
)
else:
# 块请求需要构造数据,按照字节发送数据
low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT)
low_conn.putrequest(request.method, url, skip_accept_encoding=True)
for header, value in request.headers.items():
low_conn.putheader(header, value)
low_conn.endheaders()
for i in request.body:
low_conn.send(hex(len(i))[2:].encode('utf-8'))
low_conn.send(b'\r\n')
low_conn.send(i)
low_conn.send(b'\r\n')
low_conn.send(b'0\r\n\r\n')
r = low_conn.getresponse(buffering=True)
resp = HTTPResponse.from_httplib(
r,
pool=conn,
connection=low_conn,
preload_content=False,
decode_content=False
)
# 根据网络请求 PrepareRequest 对象和网络请求返回值构造标准返回对象 Response
return self.build_response(request, resp)
对于一般情况下的非块网络请求,可以看到只是调用了 urllib3 提供的 urlopen()
发起网络请求,并利用返回值构造出 Requests 的标准输出类型 Response
。
网络请求返回值
网络请求的返回值用于提供标准的返回类型。是对网络请求返回值的封装。
通过前面的介绍可以知道,实际的网络请求是通过调用 HTTPAdapter.send()
发起的,最后在获取到返回值后调用 HTTPAdapter.build_response()
将返回值构造成 Response
对象,因此对于 Response
对象的了解主要关注 HTTPAdapter.build_response()
的实现即可。下面查看相关实现:
# class HTTPAdapter
# 根据请求对象 PrepareRequest 和 urllib3 的返回值构造出 Response
def build_response(self, req, resp):
response = Response()
# 从 urllib3 的返回值中获取网络状态码
response.status_code = getattr(resp, 'status', None)
# 从 urllib3 中获取 headers 信息
response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
response.encoding = get_encoding_from_headers(response.headers)
# 存储 urllib3 的原始返回值 resp
response.raw = resp
response.reason = response.raw.reason
# 存储网络请求的 url
if isinstance(req.url, bytes):
response.url = req.url.decode('utf-8')
else:
response.url = req.url
# 存储 cookie 至 Response 中
extract_cookies_to_jar(response.cookies, req, resp)
# 将之前的请求信息存储至 Response 中
response.request = req
response.connection = self
return response
通过上面的实现,通过将网络请求参数和 urllib3 的返回值存储至 Response
对象中,后续可以通过标准的接口将数据提供给用户。具体提供了哪些便于使用的接口,可以参考 官方文档的 Response 部分 。至于如何实现这些便于使用的接口,感兴趣的可以自行去查看相关接口的实现。
总结
通过上面的介绍,可以看到对 Requests 的基础流程有了足够的了解。至于各个部分的一些实现细节,这边没有过多涉及。如果在实践中有相关的需求,可以自行去查看各个模块的具体实现即可。