requests 源码阅读
requests 是一个简洁易用的 http-client 库,早期在 github 的 python 项目受欢迎程度可以排名 TOP10。介绍这个项目,我个人觉得还是官方的地道:Requests is an elegant and simple HTTP library for Python,built for human beings. 夸张到是人类就会使用 requests 😃。我们一起阅读一下其源码,学习它是如何实现的。
# 项目结构
本次阅读代码版本是 2.24.0, 从 github 上 clone 项目后,使用 log 命令查看历史信息,找到 tag=2.24.0 的标签,切换版本:
1. git checkout 0797c61fd541f92f66e409dbf9515ca287af28d2
大概浏览一下项目结构和代码,我们可以知道每个模块的功能:
名称 | 描述 |
---|---|
adapters.py | 负责 http 连接的处理,主要适配自 urllib3 库 |
api | api 接口 |
auth | http 认证 |
certs | https 证书处理 |
compat | python 版本适配包 |
cookies | cookie 处理 |
help | 帮助 |
hook | 钩子系统 |
models | 数据模型 |
packages | 兼容包相关 |
sessions | session 处理 |
status_codes | http 状态码 |
structures | 数据结构 |
utils | 工具 |
4000 多行代码,10 多个模块,要全部梳理工作量不小,难度也大。本篇文章我们还是只关注主线,对于支线和细枝末节可以 不求甚解 。
# api 模块
首先还是从 requests 的使用示例出发: shell
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
r.status_code
200
r.headers['content-type']
'application/json; charset=utf8'
r.encoding
'utf-8'
r.text
'{"type":"User"...'
r.json()
{'private_gists': 419, 'total_private_repos': 77, ...}
上面的使用方法由 api 提供:
1. # api.py
2.
3. def request(method, url, **kwargs)
4. with sessions.Session() as session:
5. return session.request(method=method, url=url, **kwargs)
6.
7. def get(url, params=None, **kwargs):
8. kwargs.setdefault('allow_redirects', True)
9. return request('get', url, params=params, **kwargs)
10.
11. ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这种 get-request 的 api 的封装方式,和我们之前读过的 redis 源码类似,可以让使用者更安全方便。request 具体实现代码是从 session 上下文获取一个 session,然后利用 session.request 发送请求。
同时 api 中还包装了 http 的 OPTIONS, HEAD, POST, PUT, PATCH 和 DELETE 方法。
# sessions
sessions.py 对象的创建和上下文:
1. # sessions.py
2.
3. class Session(SessionRedirectMixin):
4.
5. def __init__(self):
6. self.headers = default_headers()
7. self.cookies = cookiejar_from_dict({})
8.
9. # Default connection adapters.
10. self.adapters = OrderedDict()
11. ...
12. self.mount('https://', HTTPAdapter())
13.
14. def mount(self, prefix, adapter):
15. self.adapters[prefix] = adapter
16.
17. def __enter__(self):
18. return self
19.
20. def __exit__(self, *args):
21. for v in self.adapters.values():
22. v.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
session 初始化时候,会创建默认的 http-header,http-cookie 信息,建立 HTTPAdpater 对象。__enter__
和 __exit__
,是上下文装饰器函数,可以用来确保进行 adapter 的 close。
使用 request 方法发送请求:
1. def request(self, method, url,
2. params=None, data=None, headers=None, cookies=None, files=None,
3. auth=None, timeout=None, allow_redirects=True, proxies=None,
4. hooks=None, stream=None, verify=None, cert=None, json=None):
5. req = Request(
6. method=method.upper(),
7. url=url,
8. headers=headers,
9. files=files,
10. data=data or {},
11. json=json,
12. params=params or {},
13. auth=auth,
14. cookies=cookies,
15. hooks=hooks,
16. )
17. ...
18. prep = PreparedRequest()
19. prep.prepare(
20. method=request.method.upper(),
21. url=request.url,
22. files=request.files,
23. data=request.data,
24. json=request.json,
25. headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
26. params=merge_setting(request.params, self.params),
27. auth=merge_setting(auth, self.auth),
28. cookies=merged_cookies,
29. hooks=merge_hooks(request.hooks, self.hooks),
30. )
31. ...
32. adapter = self.get_adapter(url=request.url)
33. ...
34. resp = adapter.send(prep, **send_kwargs)
35. return resp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
request 函数的处理流程,主要分成四步:
使用请求参数封装 Request 对象
生成 PreparedRequest 对象,并对 request 对象进行预先处理
获取对应的 http/https 协议适配器,并用其 send 方法发送请求
将获取的 Response 对象返回
# models
在进行请求过程中创建了 Request,PreparedRequest 对象,同时从 adpater 中返回了 Response 对象,这 3 个对象的具体实现都在 models.py 模块。
1. class Request(RequestHooksMixin):
2.
3. def __init__(self,
4. method=None, url=None, headers=None, files=None, data=None,
5. params=None, auth=None, cookies=None, hooks=None, json=None):
6.
7. ...
8. self.hooks = default_hooks()
9. for (k, v) in list(hooks.items()):
10. self.register_hook(event=k, hook=v)
11.
12. self.method = method
13. self.url = url
14. self.headers = headers
15. self.files = files
16. self.data = data
17. self.json = json
18. self.params = params
19. self.auth = auth
20. self.cookies = cookies
21. ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Request 对象创建比较简单,就是做了一些属性的赋值,然后对外部注入的 hook 进行了一下校验,确保是可以执行的函数和函数集合。
1. def register_hook(self, event, hook):
2. """Properly register a hook."""
3.
4. if event not in self.hooks:
5. raise ValueError('Unsupported event specified, with event name "%s"' % (event))
6.
7. if isinstance(hook, Callable): ## hook 是一个函数
8. self.hooks[event].append(hook)
9. elif hasattr(hook, '__iter__'): # hook 也可以是一个迭代器
10. self.hooks[event].extend(h for h in hook if isinstance(h, Callable))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PreparedRequest 对象则对外部的参数进行更多的验证和准备:
1. class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
2.
3. ...
4.
5. def prepare(self,
6. method=None, url=None, headers=None, files=None, data=None,
7. params=None, auth=None, cookies=None, hooks=None, json=None):
8. """Prepares the entire request with the given parameters."""
9.
10. self.prepare_method(method)
11. self.prepare_url(url, params)
12. self.prepare_headers(headers)
13. self.prepare_cookies(cookies)
14. self.prepare_body(data, files, json)
15. self.prepare_auth(auth, url)
16.
17. ...
18. hooks = hooks or []
19. for event in hooks:
20. self.register_hook(event, hooks[event])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
可以看到 PreparedRequest 对象经过了:
准备 http 方法
准备 url
准备 header
准备 cookie
准备 http-body
准备认证
接受 Request 对象上带来的 hook
hook 我们最后再进行详细介绍,这里以 prepare_headers 为例看看验证过程中都做了什么:
1. def prepare_headers(self, headers):
2. """Prepares the given HTTP headers."""
3.
4. self.headers = CaseInsensitiveDict() # 创建字典
5. if headers:
6. for header in headers.items():
7. # Raise exception on invalid header value.
8. check_header_validity(header) # 验证信息
9. name, value = header
10. self.headers[to_native_string(name)] = value # 赋值
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Response 对象,主要模拟文件操作,raw 保留了二进制数据流,content 属性是获得所有二进制数据,text 属性将二进制数据编码成文本,json 方法则是将文本序列化方法。
1. CONTENT_CHUNK_SIZE = 10 * 1024 # 10k数据
2.
3. class Response(object):
4.
5. def __init__(self):
6. #: File-like object representation of response (for advanced usage).
7. #: Use of raw requires that stream=True be set on the request.
8. #: This requirement does not apply for use internally to Requests.
9. self.raw = None
10.
11. @property
12. def content(self):
13. """Content of the response, in bytes."""
14. ...
15. self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b''
16. ...
17. return self._content
18.
19. @property
20. def text(self):
21. content = str(self.content, encoding, errors='replace')
22. return content
23.
24. def json(self, **kwargs):
25. ...
26. return complexjson.loads(self.text, **kwargs)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
requests 优先使用 simplejson 进行 json 的序列化
iter_content 函数中使用一个生成器来迭代的从流中获取数据。至于流如何得到,稍后看 adapter 的实现。
1. def iter_content(self, chunk_size=1, decode_unicode=False):
2. def generate():
3. # Special case for urllib3.
4. if hasattr(self.raw, 'stream'):
5. try:
6. for chunk in self.raw.stream(chunk_size, decode_content=True):
7. yield chunk
8. stream_chunks = generate()
9. return stream_chunks
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# adapters 模块
具体的 http 请求如何发送的呢?主要就在 HTTPAdapter 中了:
1. class HTTPAdapter(BaseAdapter):
2. def __init__(self, pool_connections=DEFAULT_POOLSIZE,
3. pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
4. pool_block=DEFAULT_POOLBLOCK):
5. ...
6. # 初始化连接池
7. self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
8. block=block, strict=True, **pool_kwargs)
9.
10. def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
11. conn = self.poolmanager.connection_from_url(url) # 获取连接
12.
13. url = self.request_url(request, proxies)
14. self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
15.
16. # 发送请求
17. resp = conn.urlopen(
18. method=request.method,
19. url=url,
20. body=request.body,
21. headers=request.headers,
22. redirect=False,
23. assert_same_host=False,
24. preload_content=False,
25. decode_content=False,
26. retries=self.max_retries,
27. timeout=timeout
28. )
29.
30. return self.build_response(request, resp)
31.
32. def close(self):
33. self.poolmanager.clear() # 连接池关闭
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
这里主要用了urllib3
库提供的 PoolManager 和 urlopen,本篇文章我们就不深入里面的实现了,重点看看如何生成 Response 对象:
1. def build_response(self, req, resp):
2. response = Response()
3.
4. # Fallback to None if there's no status_code, for whatever reason.
5. response.status_code = getattr(resp, 'status', None)
6.
7. # Make headers case-insensitive.
8. response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
9.
10. # Set encoding.
11. response.encoding = get_encoding_from_headers(response.headers)
12. response.raw = resp # 二进制流
13. response.reason = response.raw.reason
14.
15. if isinstance(req.url, bytes):
16. response.url = req.url.decode('utf-8')
17. else:
18. response.url = req.url
19.
20. # Add new cookies from the server.
21. extract_cookies_to_jar(response.cookies, req, resp)
22.
23. # Give the Response some context.
24. response.request = req
25. response.connection = self
26.
27. return response
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
resp 是 urllib3 的 HTTPResponse 实现
cookie 是合并了 Request 和 Response
Response 还引用了 PreparedRequest 对象,可以让 response 的使用更方便
使用 requests 进行 http 请求的过程,主要集中在上面四个模块,现在对其核心过程都有了一定的了解。https 则是在 http 基础上,做了更多的验证等工作。可以简单回顾一下请求执行流程:
api 中封装易用的 API
Session 中进行流程的处理
Request 和 PreparedRequest 对请求进行预处理
Response 对响应进行封装,提供更易用的方法(json)和数据(ok)
# 小技巧
requests 库中还有一些代码,也让使用更简单,可以借鉴。
# json 缩进输出
json 输出的时候定义 indent 参数可以进行缩进,sort_keys
可以进行排序。
1. # help.py
2.
3. """Pretty-print the bug information as JSON."""
4. print(json.dumps(info(), sort_keys=True, indent=2))
2
3
4
5
6
7
下面是示例和展示:
1. a = {
2. "name": "game404",
3. "age": 2
4. }
5. print(json.dumps(a))
6. print(json.dumps(a, sort_keys=True, indent=2)) # 定义indent参数
7. # 输出
8. {"name": "game404", "age": 2}
9. {
10. "age": 2,
11. "name": "game404"
12. }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# structures
structures 模块中定义了 2 个数据结构。普通的 python 字典不可以使用 . 取值, 如果需要使用 . 需要定义对象:
1. # structures.py
2.
3. a = {
4. "name":"game404"
5. }
6. # print(a.name) # AttributeError
7. print(a["name"])
8.
9. # 定义一个数据结构对象
10. class Person(object):
11.
12. def __init__(self, name):
13. self.name = name
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
LookupDict 可以不用定义对象属性又使用 . 取值,这在一些配置类上会很方便:
1. class LookupDict(dict):
2. """Dictionary lookup object."""
3.
4. def __init__(self, name=None):
5. self.name = name
6. super(LookupDict, self).__init__()
7.
8. def __repr__(self):
9. return '<lookup \'%s\'>' % (self.name)
10.
11. def __getitem__(self, key):
12. # We allow fall-through here, so values default to None
13. # 可以使用. 取值的魔法函数
14. return self.__dict__.get(key, None)
15.
16. def get(self, key, default=None):
17. return self.__dict__.get(key, default
18.
19. a = LookupDict(name="game404")
20. a["motto"] = "Life is short, you need Python"
21. a.age = 2
22. print(a["motto"], a.age, a["age"]) # none, 2, 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
CaseInsensitiveDict 定义了大小写不敏感的字典,用来处理 http-header:
1. class CaseInsensitiveDict(MutableMapping):
2.
3. def __init__(self, data=None, **kwargs):
4. self._store = OrderedDict() # 使用额外的_store存储数据
5. if data is None:
6. data = {}
7. self.update(data, **kwargs)
8.
9. def __setitem__(self, key, value):
10. # Use the lowercased key for lookups, but store the actual
11. # key alongside the value.
12. self._store[key.lower()] = (key, value) # 字典的key都转换为小写
13.
14. def __delitem__(self, key):
15. del self._store[key.lower()]
16.
17. cid = CaseInsensitiveDict()
18. cid['Accept'] = 'application/json'
19. print(cid['aCCEPT'] == 'application/json') # True
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
可以看到 CaseInsensitiveDict 对象的dict实际上使用_store 包装了一层:
print(cid.dict) # {'_store': OrderedDict([('accept', ('Accept', 'application/json'))])}
print(cid._store) # OrderedDict([('accept', ('Accept', 'application/json'))])
# status_codes
status_codes 中定义了 http 状态码的语义化名称,比如 OK 是 200 的语义化表达,不懂 http 的人也可以看到 ok 状态。
1. print(requests.codes["ok"], requests.codes.OK, requests.codes.ok, requests.codes.OKAY) #200 200 200 200
2. print(requests.codes.CREATED) # 201
3. print(requests.codes.found) # 302
2
3
4
5
6
其实现方法主要是:
1. # statuc_codes.py
2.
3. codes = LookupDict(name='status_codes')
4. for code, titles in _codes.items():
5. for title in titles:
6. setattr(codes, title, code) # 默认key
7. if not title.startswith(('\\', '/')):
8. setattr(codes, title.upper(), code) # 大写key
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# hook
hooks 提供了一个简单的钩子系统,可以对一个事件名称注册多个处理函数(前面的 register_hook),然后在合适的时候触发就可以获取对数据进行处理, 数据处理过程类似 linux 的管道符号 | :
1. # hooks.py
2.
3. HOOKS = ['response']
4.
5.
6. def default_hooks(): # 初始化默认的事件
7. return {event: [] for event in HOOKS}
8.
9. def dispatch_hook(key, hooks, hook_data, **kwargs):
10. """Dispatches a hook dictionary on a given piece of data."""
11. hooks = hooks or {}
12. hooks = hooks.get(key)
13. if hooks:
14. if hasattr(hooks, '__call__'): # 判断是函数还是函数集合
15. hooks = [hooks]
16. for hook in hooks:
17. _hook_data = hook(hook_data, **kwargs) # 注意hook会返回数据,由下一个函数继续处理
18. if _hook_data is not None:
19. hook_data = _hook_data
20. return hook_data
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
使用方法在:
1. class Session(SessionRedirectMixin):
2.
3. def send(self, request, **kwargs):
4. ...
5. r = adapter.send(request, **kwargs)
6. # Response manipulation hooks
7. r = dispatch_hook('response', hooks, r, **kwargs)
2
3
4
5
6
7
8
9
10
11
12
13
session 在获取到请求后,触发预先定义的钩子,对 response 进行进一步的处理。
# 参考链接
https://requests.readthedocs.io/zh_CN/latest/
https://urllib3.readthedocs.io/en/latest/
https://gist.github.com/kennethreitz42/973705