requests v0.2.0 源码解析

2,652 阅读13分钟

1 引言

Python 向来以库丰富而广受欢迎,而 requests 库正是其中的代表。

学习 request 库有助于理解 HTTP 请求的处理流程,包括 Web 开发、测试、爬虫等多领域中都难免用到。

本系列对 requests 库进行源码解析,本文是第一篇,讲解 v0.2.0 版本。

2 requests

2.1 介绍

requests 库是 Python 中一个用于发送 HTTP 请求的第三方库。

实际上 requests 模块封装了 Python 的标准库,对外提供了更简洁的 API。

其中老版本 requests 适用于 Python2,内部使用 urllib 与 urllib2,新版本适用于 Python3,内部使用 urllib3。

简单介绍下 urllib、urllib2 与 urllib3 三者之间的关系:

  • Python 2 urllib,Python 1.2 中引入,原生 HTTP 客户端库;
  • Python2 urllib2,Python 1.6 中引入,升级版的 urllib,兼容 urllib;
  • Python3 urllib,对 Python 2 中的 urllib 进行了重写;
  • Python3 urllib3,第三方库,与 Python 的标准库没有关系。

2.2 版本

requests 库当前最早的历史版本是 v0.2.0,其中仅包含一个 __init__.py 和 core.py 文件。

因此基本上所有的代码都在 core.py 一个文件中,并且其中仅有 405 行代码。

requests 源码解析系列将从 v0.2.0 版本开始。

如下所示,拉取代码并切换到 v0.2.0 版本。

拉取最新代码
git clone https://github.com/psf/requests.git
# 切换到 v0.2.0 版本
git checkout v0.2.0
# 进入项目目录
cd requests

2.3 工具

工欲善其事必先利其器,建议使用 vscode 并安装 git history 插件,便于进行版本代码差异对比。

git-hsitory-extension

支持切换到指定分支或标签的代码。

requests-file-history

切换到 v0.2.1 版本后,支持版本代码差异对比。

request-git-diff-v0.2.0-v0.2.1

2.4 README

README 的第一部分中作者解释了开发 requests 的原因。

由于当前现有的处理 HTTP 请求的模块不好用,因此作者决定开发一个简单好用的 HTTP 模块,具体实现基于现有的内置 HTTP 模块(如 urllib、urllib2),实际上就是封装后提供更简洁的 API。

Requests: The Simple (e.g. usable) HTTP Module
==============================================

Most existing Python modules for dealing HTTP requests are insane. I have to look up *everything* that I want to do. Most of my worst Python experiences are a result of the various built-in HTTP libraries (yes, even worse than Logging). 

But this one's different. This one's going to be awesome. And simple.

Really simple.

README 的第二部分中作者介绍了 requests 库的基本功能。

具体包括支持五种请求方法、支持请求头、支持参数、可以进行简单的 HTTP 验证。

Features
--------

- Extremely simple GET, HEAD, POST, PUT, DELETE Requests
    + Simple HTTP Header Request Attachment
    + Simple Data/Params Request Attachment
- Simple Basic HTTP Authentication
    + Simple URL + HTTP Auth Registry

2.5 History

版本信息中显示 2011-02-13 发布 v0.0.1,然后第二天 2011-02-14 发布 v0.2.0。

不过当前 github 仓库中已经找不到 v0.0.1 版本了,作者也记录下了自己的心路历程,其中也经历了沮丧。

History
-------

0.2.0 (2011-02-14)
++++++++++++++++++

* Birth!


0.0.1 (2011-02-13)
++++++++++++++++++

* Frustration
* Conception

因此,requests 中给用户提供了极简的调用 HTTP 请求的方式,而所谓极简就是相对于已有的 urllib 的方法发起请求,那么,首先对比下两种方式,体验极简的魅力。

3 API 使用

首先分别使用 urllib + urllib2 与 requests 库发起 GET / POST 请求,对比 API 的使用区别。

从中可以看到 requests 模块封装了 urllib2 库,对外提供了更简洁的 API。

然后在进入源码之前,查看 test_requests 模块中提供的测试方法,其中提供了访问源码的入口。

3.1 GET 请求

3.1.1 urllib + urllib2

处理 GET 请求,流程主要包括以下三步:

  • 调用 urllib.urlencode 方法构建请求参数
  • 调用 urllib2.urlopen 方法发起请求,返回 response 对象
  • 处理响应

请求示例如下所示。

>>> import urllib
>>> import urllib2

# 请求地址与请求参数
>>> URL = "https://sl.se"
>>> params_dict = {"lat": "35.696233", "long": "139.570431"}

# 构建请求参数,拼接查询字符串
>>> params_encode = urllib.urlencode(params_dict)
>>> URL_params = "%s?%s" % (URL, params_encode)
>>> URL_params
'https://sl.se?lat=35.696233&long=139.570431'

 # 发送请求
>>> resp = urllib2.urlopen(URL_params)
>>> resp
<addinfourl at 68331400L whose fp = <socket._fileobject object at 0x0000000004388448>>
>>> type(resp)
<type 'instance'>

# 处理响应
>>> resp.info()
<httplib.HTTPMessage instance at 0x000000000412EEC8>
>>> resp.getcode()
200
>>> resp.read()
...

注意其中 urllib2.urlopen 方法的返回值 response 对象是一个类文件对象。

3.1.2 requests

处理 GET 请求,流程主要包括以下两步:

  • 调用 requests.get 方法发送请求,返回 response 对象
  • 处理响应

请求示例如下所示。

>>> import requests
>>> resp = requests.get(URL, params_dict)
>>> resp
<Response [200]>
>>> resp.headers
{'content-length': '9951', 'x-xss-protection': '1; mode=block', 'x-content-type-options': 'nosniff', 'x-powered-by': 'ASP.NET', 'set-cookie': 'ASP.NET_SessionId=05tybaloxfjwq4gsowdsml0c; path=/; secure; HttpOnly; SameSite=Lax', 'x-aspnet-version': '4.0.30319', 'connection': 'close', 'cache-control': 'private', 'date': 'Sat, 27 Aug 2022 12:02:16 GMT', 'x-frame-options': 'sameorigin', 'referrer-policy': 'origin-when-cross-origin', 'content-type': 'text/html; charset=utf-8'}

>>> resp.status_code
200
>>> resp.content
...

3.2 POST 请求

3.2.1 urllib + urllib2

处理 POST 请求,流程主要包括以下三步:

  • 调用 urllib.urlencode 方法构建请求参数
  • 调用 urllib2.urlopen 函数收到 response
  • 处理响应

请求示例如下所示。

>>> import urllib
>>> import urllib2

>>> url = "http://fanyi.youdao.com/translate"
>>> form_data = {
        "type":"AUTO",
        "i":"i love python",
        "doctype":"json",
        "xmlVersion":"1.8",
        "keyform":"fanyi.web",
        "ue":"utf-8",
        "action":"FY_BY_ENTER",
        "typoResult":"true"
    }

>>> data = urllib.urlencode(form_data)
>>> data
'keyform=fanyi.web&ue=utf-8&action=FY_BY_ENTER&i=i+love+python&xmlVersion=1.8&type=AUTO&doctype=json&typoResult=true'

>>> resp = urllib2.urlopen(url, data)
>>> resp.getcode()
200

不同于处理 GET 请求,urllib2 处理 POST 请求时没有拼接 URL 与查询字符串,而是将 data 参数传给 urlopen 方法,data 参数作为 request body。

原因是 urllib2 中通过 data 参数区分 GET 与 POST 请求。

3.2.2 requests

处理 POST 请求,流程主要包括以下两步:

  • 调用 requests.post 方法发送请求,返回 response 对象
  • 处理响应

请求示例如下所示。

>>> import requests
>>> resp = requests.post(url, data=form_data)
>>> resp.status_code
200

3.3 对比

3.3.1 urllib2 vs requests

简单对比 urllib + urllib2 与 requests 的 API 使用方法:

  • 构建参数,第一种需要用户调用 urllib.urlencode 方法进行参数编码,第二种不需要;
  • 请求地址,第一种需要用户按照格式拼接 URL 与查询字符串,第二种不需要;
  • 请求方法,第一种需要用户调用 urllib2.urlopen 方法打开 URL 地址,第二种直接调用 requests.get 方法即可;
  • 处理响应,第一种分别调用 .info()、.getcode()、.read() 方法获取请求头、状态码、响应正文,第二种访问 .headers、.status_code、.content 属性,见名知意。

可见,requests 封装后提供的接口相比于原生 urllib2 要好用很多,不需要人工处理参数,甚至仅调用对应的请求方法一行命令就可以完成请求的全过程。

3.3.2 requests get vs post

简单对比 requests 模块处理 GET 请求与 POST 请求的区别:

  • 请求方法,前者调用 requests.get 方法,后者调用 requests.post 方法;
  • 参数类型,前者是位置参数,后者是关键字参数,参数名是 data,这也与内部封装的 urllib2 保持一致。

具体如下所示,给对应方法传入字典类型的参数,就可以完成请求处理。

>>> resp = requests.get(URL, params_dict)
>>> resp = requests.post(url, data=form_data)

3.4 测试方法

下面介绍下源码中提供的两个测试方法,作为访问源码的入口。两者都是 GET 请求,区别在于是否存在权限认证。

3.4.1 test_HTTP_200_OK_GET

test_HTTP_200_OK_GET 方法中调用 get 方法发起 HTTP 请求,并判断返回响应的状态码。

import unittest

import requests


class RequestsTestSuite(unittest.TestCase):
   """Requests test cases."""
   
   def test_HTTP_200_OK_GET(self):
       r = requests.get('http://google.com')
       self.assertEqual(r.status_code, 200)

3.4.2 test_AUTH_HTTPS_200_OK_GET

访问部分网站需要用户名密码,test_AUTH_HTTPS_200_OK_GET 中存在权限认证。

def test_AUTH_HTTPS_200_OK_GET(self):
   auth = requests.AuthObject('requeststest', 'requeststest')
   url = 'https://convore.com/api/account/verify.json'
   r = requests.get(url, auth=auth)

   self.assertEqual(r.status_code, 200)

下一步分析 get 方法的源码实现。

4 requests 源码

下面以 get 请求为例讲解 requests 处理请求的流程。

4.1 get

get 方法的主要流程包括:

  • 首先创建一个 Request 对象,用于封装请求过程中全部的请求参数与响应
  • 权限认证
  • 发送请求,处理响应,最终返回 Response 对象
def get(url, params={}, headers={}, auth=None):
   """Sends a GET request. Returns :class:`Response` object.

   :param url: URL for the new :class:`Request` object.
   :param params: (optional) Dictionary of GET Parameters to send with the :class:`Request`.
   :param headers: (optional) Dictionary of HTTP Headers to sent with the :class:`Request`.
   :param auth: (optional) AuthObject to enable Basic HTTP Auth.
   """

   # 封装请求参数与响应
   r = Request()
   
   r.method = 'GET'
   r.url = url
   r.params = params
   r.headers = headers
   # 权限认证
   r.auth = _detect_auth(url, auth)

   # 发送请求,处理响应
   r.send()
   
   return r.response

其中,调用 _detect_auth 方法进行权限认证,具体将在下文中讲解。

4.2 Request

4.2.1 __init__

请求对象 Request 的构造方法中首先包括所有的请求参数,比如请求地址、请求头、请求参数等,其次还有一个实例属性用于保存响应。

class Request(objct):
   """The :class:`Request` object. It carries out all functionality of
   Requests. Recommended interface is with the Requests functions.
   
   """
   
   _METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE')
   
   def __init__(self):
      self.url = None
      self.headers = dict()
      self.method = None
      self.params = {}
      self.data = {}
      self.response = Response()
      self.auth = None
      self.sent = False

4.2.2 setattr

类属性中保存 requests 中支持的请求方法,共五种。

_METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE')

属性赋值时自动调用 __setattr__ 方法,其中校验传入的请求方法。

def __setattr__(self, name, value):
   if (name == 'method') and (value):
      if not value in self._METHODS:
         raise InvalidMethod()
   
   object.__setattr__(self, name, value)

如果传入不支持的请求方法,赋值时抛出 InvalidMethod 异常。

class InvalidMethod(RequestException):
   """An inappropriate method was attempted."""   

class RequestException(Exception):
   """There was an ambiguous exception that occured while handling your request."""

4.3 send

send 是处理请求的核心逻辑,主要流程包括:

  • 请求参数编码
  • 发送请求
  • 处理响应

send 方法中不同的请求方法对应的逻辑相同,本文主要介绍 GET 请求。

下面依次介绍 send 方法中 GET 请求的主要流程。

4.3.1 urllib.urlencode

处理 GET 请求时,如果请求参数是字典类型,调用 urllib.urlencode 方法完成参数编码,实际上就是参数格式化。

然后,在 Request 对象内部创建 _Request 对象,该对象继承 urllib2.Request 类,并将请求方法与请求参数传入。

# url encode GET params if it's a dict
if isinstance(self.params, dict):
   params = urllib.urlencode(self.params)
else:
   params = self.params

req = _Request(("%s?%s" % (self.url, params)), method=self.method)

值得一提的是与 GET 请求不同,在处理 POST 请求时,首先创建 _Request 对象,然后在调用 urllib.urlencode 方法完成参数编码以后,将请求参数保存到 _Request 对象中作为 request body。

req = _Request(self.url, method='POST')

# url encode form data if it's a dict
if isinstance(self.data, dict):
    req.data = urllib.urlencode(self.data)
else:
    req.data = self.data

4.3.2 _Request

_Request 对象中调用父类 urllib2.Request 的初始化方法,该对象用于实际的请求发送。

class _Request(urllib2.Request):
   """Hidden wrapper around the urllib2.Request object. Allows for manual
   setting of HTTP methods.
   """
   
   def __init__(self, url,
               data=None, headers={}, origin_req_host=None,
               unverifiable=False, method=None):
      urllib2.Request.__init__( self, url, data, headers, origin_req_host,
                          unverifiable)
      self.method = method

上面提到,urllib2 中就是通过 data 参数来区分 GET 与 POST 方法,从 urllib2.Request 类的 get_method 方法中也可以看到。

class Request:
	def get_method(self):
        if self.has_data():
            return "POST"
        else:
            return "GET"

4.3.3 urllib2.urlopen

调用 _get_opener 方法创建 opener。

opener = self._get_opener()

_get_opener 方法中首先判断是否需要验证,如果不需要验证,直接返回 urllib.urlopen 方法,否则创建自定义 opener 并返回 urllib.open 方法,返回的方法用于发起请求。

实际上,urllib.urlopen 方法内部也是调用 urllib.open 方法,因此这里看起来使用了两种方法,本质上是同一种。

def _get_opener(self):
   """ Creates appropriate opener object for urllib2.
   """
   
   if self.auth:

      # create a password manager
      authr = urllib2.HTTPPasswordMgrWithDefaultRealm()

      authr.add_password(None, self.url, self.auth.username, self.auth.password)
      handler = urllib2.HTTPBasicAuthHandler(authr)
      opener = urllib2.build_opener(handler)

      # use the opener to fetch a URL
      return opener.open
   else:
      return urllib2.urlopen

4.4.4 发送请求

最终发送请求是调用刚返回的 opener 方法。

resp = opener(req)

实际上,如果没有权限认证,调用 opener 方法等于调用 urllib2.urlopen 方法。

resp = urllib2.urlopen(urllib2.Reuqest())

4.4.5 处理响应

处理响应其实主要是将 _Request 对象的响应组装到 Request.response 属性中。

self.response.status_code = resp.code
self.response.headers = resp.info().dict
if self.method.lower() == 'get':
   self.response.content = resp.read()

4.5 Response

Response 类中仅有两个方法,包括构建方法与 __repr__ 方法。

4.5.1 __init__

Response 类中保存了需要返回的响应,其中包括响应正文、状态码、响应头。

class Response(object):
   """The :class:`Request` object. All :class:`Request` objects contain a
   :class:`Request.response <response>` attribute, which is an instance of
   this class.
   """

   def __init__(self):
      self.content = None
      self.status_code = None
      self.headers = dict()

4.5.2 __repr__

__repr__方法用于格式化输出,其中仅打印状态码。

class Response(object):
    
    def __repr__(self):
       try:
          repr = '<Response [%s]>' % (self.status_code)
       except:
          repr = '<Response object>'
       return repr

上面介绍 send 方法中跳过了权限认证过程,下面予以详细介绍。

4.6 权限认证

4.6.1 流程

有权限认证的 HTTP 请求示例如下所示。

>>> c_auth = requests.AuthObject('kennethreitz', 'xxxxxxx')
>>> requests.add_autoauth('https://convore.com/api/', c_auth)
>>> r = requests.get('https://convore.com/api/account/verify.json')

因此,有权限认证时的实现流程相对复杂,具体包括:

  • 实例化 requests.AuthObject,保存用户名密码;
  • 调用 requests.add_autoauth,关联 URL 与用户名密码;
  • send 方法中调用 _detect_auth 方法,判断传入的 URL 与 auth 是否已授权。

4.6.2 AuthObject

首先,实例化 AuthObject 类,保存传入的用户名密码。

class AuthObject(object):
   def __init__(self, username, password):
      self.username = username
      self.password = password

4.6.3 add_autoauth

然后,调用 requests.add_autoauth 方法,其中将 URL 与 AuthObject 对象作为元组保存在全局变量 AUTOAUTHS 中。

AUTOAUTHS = []

def add_autoauth(url, authobject):
	global AUTOAUTHS
	
	AUTOAUTHS.append((url, authobject))

注意测试方法 test_AUTH_HTTPS_200_OK_GET 中未调用 add_autoauth 方法。

原因是对于单次授权的场景,并不需要调用该方法中修改全局变量,也可以对某一域名进行设置,类似后期增加的 session 功能。

4.6.4 _detect_auth

send 方法中调用 _detect_auth 方法,判断用户传入的 URL 与 auth 是否已授权。

r.auth = _detect_auth(url, auth)

该方法中最终调用 _get_autoauth 方法。

def _detect_auth(url, auth):
   """Returns registered AuthObject for given url if available, defaulting to
   given AuthObject."""

   return _get_autoauth(url) if not auth else auth

4.6.5 _get_autoauth

_get_autoauth 方法中遍历全局变量 AUTOAUTHS,如果 URL 已授权,则返回注册的 auth 认证,否则返回 None 表示未授权。

def _get_autoauth(url):
   """Returns registered AuthObject for given url if available.
   """
   
   for (autoauth_url, auth) in AUTOAUTHS:
      if autoauth_url in url: 
         return auth

   return None

注意判断 URL 是否授权时使用的是 str.in 方法,而非等值判断,原因是当前缀(如域名)相同时,仅需要一次认证。

>>> requests.add_autoauth('https://convore.com/api/', c_auth)
>>> r = requests.get('https://convore.com/api/account/verify.json')

返回的 auth 认证保存在 Request.auth 属性中。

def get(url, params={}, headers={}, auth=None):
    
   # 封装请求参数与响应
   r = Request()
   ...
   # 权限认证
   r.auth = _detect_auth(url, auth)

通过认证以后, _get_opener 方法中使用到了 _get_autoauth 中返回的 auth 认证,用于 urllib2 中创建 handler。

def _get_opener(self):
   
   if self.auth:

      # create a password manager
      authr = urllib2.HTTPPasswordMgrWithDefaultRealm()

      authr.add_password(None, self.url, self.auth.username, self.auth.password)
      handler = urllib2.HTTPBasicAuthHandler(authr)
      opener = urllib2.build_opener(handler)

      # use the opener to fetch a URL
      return opener.open
   else:
      return urllib2.urlopen

注意 add_autoauth 方法供外部用户调用,_detect_auth 与 _get_autoauth 方法都有单前置下划线,表示是私有方法,类对象与子类可以访问。

5 结论

requests 模块封装了 Python 的标准库(Python 2:urllib 与 urllib2;Python 3:urllib3),简化了用户调用,由此可见 API 简洁的重要性。

requests 库当前最早的历史版本是 v0.2.0,其中仅包含一个 __init__.py 和 core.py 文件。

因此基本上所有的代码都在 core.py 一个文件中,并且其中仅有 405 行代码。

实际上,仅有的 405 代码也有冗余代码待优化,而如今,随着功能的丰富,requests 的代码量已经远超当初,可见迭代的重要性,代码是一步步优化出来的。

6 知识点

6.1 网络

6.1.1 URL

URL(Uniform Resource Locator,统一资源定位符)是当前最通用的网络资源定位方式。

URL 的基本格式如下所示:

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<fragment>

其中:

  • schema / protocol,传输协议,最常见的是 HTTP 协议;
  • user,连接服务器使用的用户名;
  • password,连接服务器使用的密码;
  • host,主机名,存放资源的服务器的域名系统(DNS)主机名或 IP 地址;
  • port,端口号,省略时使用传输协议的默认端口,如 HTTP 的默认端口是 80;
  • path,路径,由零或多个“/”符号隔开的字符串,一般用来表示主机上的一个目录或文件地址;
  • params,参数,用于指定特殊参数的可选项;
  • query,查询,用于给动态网页传递参数,如果有多个参数,使用 & 符号隔开,每个参数的名与值使用 = 符号隔开;
  • fragment,信息片段,用于指定网络资源中的片段。例如一个网页中有多个名词解释,可使用 fragment 直接定位到某一名词解释。

6.2 Python

6.2.1 __setattr__

属性赋值时自动调用 __setattr__ 方法,其中校验传入的请求方法。

def __setattr__(self, name, value):
   if (name == 'method') and (value):
      if not value in self._METHODS:
         raise InvalidMethod()
   
   object.__setattr__(self, name, value)

重写 __setattr__ 方法的应用场景是什么呢?

如下所示,通过重写 __setattr__ 方法实现属性赋值时对值的验证。

class Student(object):
    def __init__(self, name):
        self.name = name

    def __setattr__(self, key, value):
        if len(value) < 3:
            raise ValueError("Name length need >= 3")


s = Student(name="To")  # ValueError

6.2.2 自定义异常类

Request 类中 __setattr__ 方法属性赋值时校验传入的请求方法,如果是不支持的请求方法,在赋值时抛出 InvalidMethod 异常。

注意其中自定义异常类的类体为空,而不会报错语法错误。

class InvalidMethod(RequestException):
   """An inappropriate method was attempted."""   

class RequestException(Exception):
   """There was an ambiguous exception that occured while handling your request."""

7 待办

  • urllib:opener、handler

本文是来自读者朋友鸟山明的投稿

参考教程