requests源码阅读:调用requests发起一个请求的时候,探究requests的做了什么?
起源
当我们简单的调用requests发起一个请求的时候,到底发生了什么?让我们从一行代码看起
r = requests.get("https://www.baidu.com")
初步调用分析
根据不同的请求方式调用http method
去request.get() 实际调用的是request("get", url, params=params, **kwargs)
在api.py
的中,我们可以看到request开启一个session去处理请求,然后调用session.request
def request(method, url, **kwargs):
with sessions.Session() as session:
return session.request(method=method, url=url, **kwargs)
核心调用分析
session.request的内容
- Create the Request.对象
- prepare_request
- merge_environment_settings:处理一些发送的配置和环境编程进行合并,
- 合并传入的timeout和allow_redirects的配置
- 发送请求self.send
Request对象
先让我们初步了解一下Request
对象,下方是它的基本结构
对象结构
参数名 | 描述 | 默认值 |
---|---|---|
method | 使用的HTTP方法。 | 必填 |
url | 发送请求的URL。 | 必填 |
headers | 发送的头部字典。 | 必填 |
files | 字典形式的文件列表,用于multipart上传,格式为{filename: fileobject}。 | None |
data | 请求体。如果提供字典或列表形式的元组[(key, value)] ,则会进行表单编码。 | {} |
json | 请求体的JSON数据(如果未指定files或data)。 | None |
params | 附加到URL的URL参数。如果提供字典或列表形式的元组[(key, value)] ,则会进行表单编码。 | {} |
auth | 认证处理器或(user, pass)元组。 | None |
cookies | 附加到此请求的cookies字典或CookieJar。 | None |
hooks | 回调钩子的字典,用于内部使用。 | None |
初始化时,会初始化默认是hooks,目前仅有一个response_hook。如果传递了自定义的hook,同样也会传递给request对象,然后注册进去。让我们继续探究
prepare_request 做了什么?
prepare_request(req) 。传入一个Request对象,返回一个PreparedRequest对象。大体就是将session的一些属性与Request对象中的进行合并,并且将一些数据的结构进行预处理,比如将字典转换成对象
- 处理cookies:
- 将一个传入的的cookie字典转换为RequestsCookieJar对象。RequestsCookieJar是一个兼容类,兼容http.cookiejar.CookieJar,提供了更多的便捷方法,比如一些字典的方法。
- 合并session的cookie,调用merge_cookies方法
- 从环境中获取基本的认证凭证。通过session的变量trust_env=True(默认为True)而且没有设置auth,会从环境变量中获取认证信息。
- 获取环境中的认证信息,实际是通过获取.netrc文件进行认证。寻找的路径为[‘~/.netrc’, ‘~/_netrc’]
- 如果没有找到就返回None,找到则调用netrc库进行解析,得到一个长度为2的元组存储认证信息,如(user, pass)
- 组装成一个PreparedRequest对象:
- 将Request对象的信息(method,url,files,data,json)给到PreparedRequest对象
- 合并session的headers,将header处理中大小写无关的字典对象
CaseInsensitiveDict
- 合并session的params,将params处理成一个有序字典
OrderedDict
- 合并session的auth,将auth处理成一个有序字典
OrderedDict
- 合并session的hooks,将hook处理成一个有序字典
OrderedDict
相关源码
class Session(SessionRedirectMixin):
def prepare_request(self, request: Request) -> PreparedRequest:
cookies = request.cookies or {}
# 处理cookies:
if not isinstance(cookies, cookielib.CookieJar):
cookies = cookiejar_from_dict(cookies)
merged_cookies = merge_cookies(
merge_cookies(RequestsCookieJar(), self.cookies), cookies
)
# 从环境中获取基本的认证凭证
auth = request.auth
if self.trust_env and not auth and not self.auth:
auth = get_netrc_auth(request.url)
# 组装成一个PreparedRequest对象
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_environment_settings做了什么?
def merge_environment_settings(self, url, proxies, stream, verify, cert) -> dict。返回的dict结构为
{"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
主要处理的内容是将请求的proxys,stream,verify,cert和Session的配置进行合并,返回的都是OrderedDict
。其核心代码如下
class Session(SessionRedirectMixin):
def merge_environment_settings(self, url, proxies, stream, verify, cert) -> dict:
# 获取proxy的配置
if self.trust_env:
# Set environment's proxies.
no_proxy = proxies.get("no_proxy") if proxies is not None else None
env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
for k, v in env_proxies.items():
proxies.setdefault(k, v)
# Look for requests environment configuration
# and be compatible with cURL.
if verify is True or verify is None:
verify = (
os.environ.get("REQUESTS_CA_BUNDLE")
or os.environ.get("CURL_CA_BUNDLE")
or verify
)
proxies = merge_setting(proxies, self.proxies)
stream = merge_setting(stream, self.stream)
verify = merge_setting(verify, self.verify)
cert = merge_setting(cert, self.cert)
return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
其中get_environ_proxies的源码如下:
def get_environ_proxies(url, no_proxy=None):
if should_bypass_proxies(url, no_proxy=no_proxy):
return {}
else:
# 实际上是return getproxies_environment() or getproxies_macosx_sysconf()
return getproxies()
有关proxy的处理:
proxy的处理依赖于环境变量中的http_proxy
, https_proxy
, no_proxy
, andall_proxy
。他们是配置http和http的环境变量。在此不过多进行说明,有兴趣的可以看看文章
环境变量 | 用途 | 格式示例 | 备注 |
---|---|---|---|
http_proxy | 设置HTTP请求的代理服务器。 | http://username:password@proxyserver:port | 默认端口为80。 |
https_proxy | 设置HTTPS请求的代理服务器。 | https://username:password@proxyserver:port | 默认端口为443。 |
no_proxy | 指定不应通过代理服务器访问的主机名、域名或IP地址。 | localhost,.example.com,10.0.0.0/8 | 支持域名模式匹配和CIDR表示法的IP地址范围。 |
all_proxy | 设置所有HTTP和HTTPS请求的代理服务器,覆盖http_proxy 和https_proxy 。 | http://username:password@proxyserver:port | 不推荐使用,因为它会覆盖其他代理设置。 |
如果这个请求的url是符合no_proxy的条件,那么不会使用代理。
这个no_proxy的支持格式有很多
- 可以包含一个或多个主机名、域名或IP地址。如localhost,.example.com,10.0.0.0/8。
- 可以使用通配符*来匹配域名的子域。例如,.example.com 会匹配 example.com 及其所有子域,如 sub.example.com
- 可以包含单个IP地址或使用CIDR表示法指定的IP地址范围。例如,10.0.0.0/8 表示匹配10.0.0.0到10.255.255.255的所有IP地址
如果不符合no_proxy的条件,就会通过get_environ_proxies
获取对应的配置
- 扫描
<scheme>_proxy
格式的环境变量,获取代理配置。比如http_proxy
和https_proxy
是最常用的环境变量。然后将去掉(_proxy 后缀)作为key,实际值作为value添加到 proxies 字典中。对应的是def getproxies_environment
中的逻辑 - 注意的一点是,若是macos,他在读取不到任何代理配置的时候,还会通过macos 框架的SystemConfiguration获取代理信息。对应的是
def getproxies_macosx_sysconf
中的逻辑
有关于verify的处理
如果verify为True或者请求本身并没有设置verify(bool),verfiy的值就会读取环境变量中的REQUESTS_CA_BUNDLE
和CURL_CA_BUNDLE
和verify
。三者取或
Session.send方法做了什么
send是最为核心的方法,核心是调用adapter.send的方法和重定向的处理。其核心如下
class Session(SessionRedirectMixin):
def __init__(self):
# do some init
self.adapters = OrderedDict()
self.mount("https://", HTTPAdapter())
self.mount("http://", HTTPAdapter())
def send(self, request, **kwargs):
# 处理请求参数
kwargs = ...
# 根据http还是https协议获取对应的adapter
...
adapter = self.get_adapter(url=request.url)
...
r = adapter.send(request, **kwargs)
# 处理response 的hook,hook的方法在这里被调用
r = dispatch_hook("response", hooks, r, **kwargs)
# 处理重定向
if allow_redirects:
self.resolve_redirects(r, request, **kwargs)
# 处理重定向的history
...
# 不需要重定向的时候,提供一个生成器给到用户手动调用。使用r.next()
if not allow_redirects:
r._next = next(
self.resolve_redirects(r, request, yield_requests=True, **kwargs)
)
get_adapter做了什么?
获取adapter,然后调用adapter.send。adapter会在每个Session对象初始化的时候创建,默认会创建两个adapter,一个https的adapter,一个是http的adapter,adapter是HTTPAdapter
对象。adapter是一个中间件允许requests与不同的HTTP服务进行交互。它主要作用是处理请求的发送和响应的接收,它们可以自定义如何处理请求的发送过程,包括连接的创建、维护和复用,以及如何处理不同类型的响应。get_adapter
就是通过请求的url来获取对应的adapter
adapter.send做了什么?
adapter.send 接收一个PreparedRequest,返回一个Response对象。
其核心代码如下
class HTTPAdapter(BaseAdapter):
def send(self, request, stream=False, timeout=None, verify=None, cert=None):
conn = get_connection(request.url, proxies=request.proxies)
self.cert_verify(conn,...)
# do something with conn
...
timeout = Timeout(...)
resp = conn.urlopen(url=url,....)
# 构建返回体
r = adapter.build_response(request,resp)
def cert_verify(self, conn, url, verify, cert):
if url.lower().startswith("https") and verify:
conn.cert_reqs = "CERT_REQUIRED"
if verify is not True:
# 可能是`REQUESTS_CA_BUNDLE`或`CURL_CA_BUNDLE`的其中一个数值
cert_loc = verify
else:
conn.cert_reqs = "CERT_NONE"
if cert:
conn.cert_file = ...
conn.key_file = ...
- 从链接池(可能是代理链接吃,也可能是普通的链接池)中获取一个连接池,返回的是
urllib3.connectpool.HTTPConnectionPool
对象conn.- 在使用代理的情况下,adapter会维护一个proxy的连接池字典,根据不同的代理获取不同的连接池。
- 不用用代理的情况下,adapter会维护一个连接池集合字典,会使用请求上下文(如host,schema,headers等)根据一定的规则生成一个key,然后根据key返回对应的连接池
- 校验SSL证书,其中有三个比较关键的请求参数:
- 若是https且需要需要校验,且verify不为False,那么就需要校验。设置conn.cert_reqs的值为
CERT_REQUIRED
- 这里verify的值在merge_environment_settings时已经处理过了,他可能是str类型,也可能是bool类型。
- 是str的情况:可能是
REQUESTS_CA_BUNDLE
或CURL_CA_BUNDLE
的其中一个数值,若是REQUESTS_CA_BUNDLE
或CURL_CA_BUNDLE
,则直接使用这个证书或证书目录,并将对应的证书解析结果放到conn中 - 是bool的情况:若是False,则不校验
- 是str的情况:可能是
- 这里verify的值在merge_environment_settings时已经处理过了,他可能是str类型,也可能是bool类型。
- 若不需要校验,设置conn.cert_reqs的值为
CERT_NONE
- 若是https且需要需要校验,且verify不为False,那么就需要校验。设置conn.cert_reqs的值为
- 处理一些请求参数,包括cookies,headers,auth,超时参数
Timeout
等- 超时使用的是创建一个urllib3.util的Timeout超时器
- 然后调用
HTTPConnectionPool
的urlopen
发送请求。urlopen
最终调用的HTTPConnectionPool
的request方法将数据发送出去,得到返回体 - 将返回码,返回体,header和cookie和数据等信息封装成Response对象
重定向如何处理
调用的是resolve_redirects的方法,该方法接受请求request对象和返回response对象。
核心代码如下
def resolve_redirects(
self,
resp,
req,
stream=False,
timeout=None,
verify=True,
cert=None,
proxies=None,
yield_requests=False,
**adapter_kwargs,
):
url = self.get_redirect_target(resp)
...
while url:
prepared_request = req.copy()
...
# 结束原来的请求
resp.close()
# 兼容同样的scheme (see: RFC 1808 Section 4)
if url.startswith("//"):
parsed_rurl = urlparse(resp.url)
url = ":".join([to_native_string(parsed_rurl.scheme), url])
# 兼容处理fragment (RFC 7231 7.1.2)
parsed = urlparse(url)
if parsed.fragment == "" and previous_fragment:
parsed = parsed._replace(fragment=previous_fragment)
elif parsed.fragment:
previous_fragment = parsed.fragment
url = parsed.geturl()
# 兼容RFC 7231的相对路径.
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
if not parsed.netloc:
url = urljoin(resp.url, requote_uri(url))
else:
url = requote_uri(url)
prepared_request.url = to_native_string(url)
# 重新构造请求
self.rebuild_method(prepared_request, resp)
# 清空"Content-Length", "Content-Type", "Transfer-Encoding"
if resp.status_code not in (
codes.temporary_redirect,
codes.permanent_redirect,
):
purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding")
for header in purged_headers:
prepared_request.headers.pop(header, None)
prepared_request.body = None
...
# Override the original request.
req = prepared_request
if yield_requests:
yield req
else:
resp = self.send(
req,
stream=stream,
timeout=timeout,
verify=verify,
cert=cert,
proxies=proxies,
allow_redirects=False,
**adapter_kwargs,
)
extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
# 下一个url
url = self.get_redirect_target(resp)
yield resp
- 他会解析response的headers是否包含有
location
和http的状态码是否是301,302,303,307,308。然后获取对应的重定向url。 - 若是请求没有超过session中最大的重定向
max_redirects
次数(默认30),则进行重定向 - 构造新的请求,比如新的url,proxy配置,cookie配置等,其中有很多的注意点
- 结束原来的请求释放请求池的资源,因为重定向和原来的请求是在同一个session进行的
- 需要兼容
RFC 1808 Section 4
,比如重定向的url不带有https或者http等schema,则使用原来请求的schema - 需要兼容
RFC 7231 7.1.2
fragment的处理,保留其在文档中的当前位置,即使服务器将其重定向到新的 URL。 - 需要兼容
RFC 7231 7.1.2
。比如将’/path/to/resource’转化为’http://domain.tld/path/to/resource’ - 若不是307或者308,则将清空"Content-Length", “Content-Type”, “Transfer-Encoding”:在HTTP重定向中,如果请求方法从POST变为GET,那么Content-Type头就不再适用,因为GET请求不包含请求体。因此,requests库应该在重定向时自动移除Content-Type头。Content-Length也应该被移除。Transfer-Encoding头用于指定传输编码(如chunked),这通常与请求体的处理有关。在重定向到GET请求时,请求体被移除
- 继续进行重定向,直到重定向结束
实际上requests库做了什么?
- 底层调用python自身的
urllib3
库 - 基于
urllib3
,引入了会话(session)的机制,使得在请求的时候在于可以利用公共配置,比如使用了session的配置和环境变量的配置,拥有更高级别的会话管理功能 - 封装了request和response对象,使得在处理的时候更易处理
- 基于会话级别提供了重定向的自动处理能力
附录
Response对象
Response对象是HTTP请求的响应结果,它包含了服务器对HTTP请求的响应信息。下面是Response对象的属性解析:
- _content: 响应体的内容,类型为bytes。
- status_code: HTTP状态码,例如200、404、500等。
- headers: 响应头部信息,类型为dict。
- url: 响应的URL地址。
- history: 响应的历史记录,类型为list,包含了重定向的URL地址。
- encoding: 响应体的编码格式,例如utf-8、gbk等。
- reason: HTTP状态码的原因短语,例如"OK"、"Not Found"等。
- cookies: 响应的Cookie信息,类型为dict。
- elapsed: 响应的耗时,类型为float,单位为秒。
- request: 发起请求的Request对象。