requests源码阅读:requests发起一个请求的时候,做了什么?

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的内容

  1. Create the Request.对象
  2. prepare_request
  3. merge_environment_settings:处理一些发送的配置和环境编程进行合并,
  4. 合并传入的timeout和allow_redirects的配置
  5. 发送请求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对象中的进行合并,并且将一些数据的结构进行预处理,比如将字典转换成对象

  1. 处理cookies:
    1. 将一个传入的的cookie字典转换为RequestsCookieJar对象。RequestsCookieJar是一个兼容类,兼容http.cookiejar.CookieJar,提供了更多的便捷方法,比如一些字典的方法。
    2. 合并session的cookie,调用merge_cookies方法
  2. 从环境中获取基本的认证凭证。通过session的变量trust_env=True(默认为True)而且没有设置auth,会从环境变量中获取认证信息。
    1. 获取环境中的认证信息,实际是通过获取.netrc文件进行认证。寻找的路径为[‘~/.netrc’, ‘~/_netrc’]
    2. 如果没有找到就返回None,找到则调用netrc库进行解析,得到一个长度为2的元组存储认证信息,如(user, pass)
  3. 组装成一个PreparedRequest对象:
    1. 将Request对象的信息(method,url,files,data,json)给到PreparedRequest对象
    2. 合并session的headers,将header处理中大小写无关的字典对象CaseInsensitiveDict
    3. 合并session的params,将params处理成一个有序字典OrderedDict
    4. 合并session的auth,将auth处理成一个有序字典OrderedDict
    5. 合并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_proxyhttps_proxyhttp://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_proxyhttps_proxy是最常用的环境变量。然后将去掉(_proxy 后缀)作为key,实际值作为value添加到 proxies 字典中。对应的是def getproxies_environment中的逻辑
  • 注意的一点是,若是macos,他在读取不到任何代理配置的时候,还会通过macos 框架的SystemConfiguration获取代理信息。对应的是def getproxies_macosx_sysconf中的逻辑
有关于verify的处理

如果verify为True或者请求本身并没有设置verify(bool),verfiy的值就会读取环境变量中的REQUESTS_CA_BUNDLECURL_CA_BUNDLEverify。三者取或

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

  1. 从链接池(可能是代理链接吃,也可能是普通的链接池)中获取一个连接池,返回的是urllib3.connectpool.HTTPConnectionPool对象conn.
    1. 在使用代理的情况下,adapter会维护一个proxy的连接池字典,根据不同的代理获取不同的连接池。
    2. 不用用代理的情况下,adapter会维护一个连接池集合字典,会使用请求上下文(如host,schema,headers等)根据一定的规则生成一个key,然后根据key返回对应的连接池
  2. 校验SSL证书,其中有三个比较关键的请求参数:
    1. 若是https且需要需要校验,且verify不为False,那么就需要校验。设置conn.cert_reqs的值为CERT_REQUIRED
      1. 这里verify的值在merge_environment_settings时已经处理过了,他可能是str类型,也可能是bool类型。
        1. 是str的情况:可能是REQUESTS_CA_BUNDLECURL_CA_BUNDLE的其中一个数值,若是REQUESTS_CA_BUNDLECURL_CA_BUNDLE,则直接使用这个证书或证书目录,并将对应的证书解析结果放到conn中
        2. 是bool的情况:若是False,则不校验
    2. 若不需要校验,设置conn.cert_reqs的值为CERT_NONE
  3. 处理一些请求参数,包括cookies,headers,auth,超时参数Timeout
    1. 超时使用的是创建一个urllib3.util的Timeout超时器
  4. 然后调用HTTPConnectionPoolurlopen发送请求。urlopen最终调用的HTTPConnectionPool的request方法将数据发送出去,得到返回体
  5. 将返回码,返回体,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

  1. 他会解析response的headers是否包含有location和http的状态码是否是301,302,303,307,308。然后获取对应的重定向url。
  2. 若是请求没有超过session中最大的重定向max_redirects次数(默认30),则进行重定向
  3. 构造新的请求,比如新的url,proxy配置,cookie配置等,其中有很多的注意点
    1. 结束原来的请求释放请求池的资源,因为重定向和原来的请求是在同一个session进行的
    2. 需要兼容RFC 1808 Section 4,比如重定向的url不带有https或者http等schema,则使用原来请求的schema
    3. 需要兼容RFC 7231 7.1.2fragment的处理,保留其在文档中的当前位置,即使服务器将其重定向到新的 URL。
    4. 需要兼容RFC 7231 7.1.2。比如将’/path/to/resource’转化为’http://domain.tld/path/to/resource’
    5. 若不是307或者308,则将清空"Content-Length", “Content-Type”, “Transfer-Encoding”:在HTTP重定向中,如果请求方法从POST变为GET,那么Content-Type头就不再适用,因为GET请求不包含请求体。因此,requests库应该在重定向时自动移除Content-Type头。Content-Length也应该被移除。Transfer-Encoding头用于指定传输编码(如chunked),这通常与请求体的处理有关。在重定向到GET请求时,请求体被移除
  4. 继续进行重定向,直到重定向结束

实际上requests库做了什么?

  1. 底层调用python自身的urllib3
  2. 基于urllib3,引入了会话(session)的机制,使得在请求的时候在于可以利用公共配置,比如使用了session的配置和环境变量的配置,拥有更高级别的会话管理功能
  3. 封装了request和response对象,使得在处理的时候更易处理
  4. 基于会话级别提供了重定向的自动处理能力

附录

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对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

smith成长之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值