库结构
.
├── adapters.py
├── api.py
├── auth.py
├── certs.py
├── compat.py
├── cookies.py
├── exceptions.py
├── help.py
├── hooks.py
├── __init__.py
├── _internal_utils.py
├── models.py
├── packages.py
├── sessions.py
├── status_codes.py
├── structures.py
├── utils.py
└── __version__.py
整体结构
这是我第一次看源码,版本为requests-2.18.4.说实话很吃力,有这几个原因
- 英文水平不够,有些注释理解起来吃力
- 引入了很多底层库和模块,不知道其作用
- 不知道从何看起,各个模块互相引用
- 有一些兼容py2,py3的代码
- 对HTTP协议传输时的各种内容不了解(即使我懂计网)
因为无法从细节处理解,所以我决定从宏观层面了解requests是如何运作的。首先需要找到requests的入口,不过入口很多,比如get
,post
,head
等等,但其实他们最终都是调用了requests.request()方法,只是在调用时传入了不同的HTTP请求方法名。因为模块很多,且相互之间互相引用,很容易看了后面,忘了前面,所以我在草稿纸上从入口开始,画了一个大概的框架图,一直到返回response,但是不好上传图片,用文字描述一下。
首先入口函数是requests.request(),接受一个
method, url, **kwargs
,从而可以发送http的各种请求,相关配置信息比如cookie, 请求体data,身份认证等也在这时传入request()方法调用了sessions.py中的Session类的request()方法,接下来提到的request()方法都是Session类的
该request()方法先生成一个models.py中Request对象,然后用这个对象作为参数生成一个PreparedRequest对象,然后request()方法调用Session的send()方法得到响应,并返回。
第三步中的send方法,先生成了一个HTTPAdapter对象,然后调用了HTTPAdapter.send()取得响应,并返回。
框架大概就是这样,对所有的细节都没有考虑。
挑几个我看的懂的模块讲讲
init.py
- 检查了urllib3和chardet的版本,使用assert来保证版本正确
- 引入了requests对外开放的接口,基本信息
version.py
保存版本,作者等信息
api.py
定义了库的对外接口,包括request,get,options,head,post,put,patch,delete这些接口,除了request,其他方法最终都还是调用request方法,类似于request('get', url', **kwargs)
,而request是调用了sessions.py中的Session类的request方法,抛开这个类的其他功能不谈,这个类是一个上下文管理器,保证请求后连接的关闭操作。
sessions.py
这个模块最重要的就是Session类,继承了SessionRedirectMixin类。初始化Session类的时候,为请求的一些选项赋值默认信息。前面说过了Session类是一个上下文管理器,因为其定义了__enter__
和__exit__
两个方法。
Session类与api中一样,也定义了get,post,head这些方法,最终也都是调用了Session类的request方法。request方法接受api中接口传入的参数,并用这些参数创建一个Request对象,但是这个对象在我看来似乎没做什么事,只是用来创建PreparedRequest()对象,那为什么不直接创建PrepardRequest对象呢?PreparedRequest对象对传入的参数进行了一些处理。之后把这个对象和一些其他参数作为参数调用Session的send方法,send方法调用了HTTPAdapter的send方法,获得最终对象,展开了讲太多了,就介绍的简单了写。
compat.py
因为py2,py3的标准库和内置的编码方式有变化,这个模块是根据python版本不同引入不同的标准库,并修改一定的标准库名字。。
structures.py
定义了两个继承自dict
的类,分别为CaseInsensitiveDict和LookupDict。第一个类的存储方式很奇特,内部定义了一个OrderedDict数据对象_store,所有的操作都是对这个对象操作,_store以传入的Key的小写作为key,(key,value)这个元祖作为值。是为了将所有形式的key,无论大小写,只要字母是一样的,就只存一遍。举个例子
header = CaseInsensitiveDict()
header['accEPT'] = 'json'
# _store['accept'] = ('accEPT', 'json')
header['Accept'] = 'json'
# _store['accept'] = ('Accept', 'json')
其他模块
还有很多模块,就不一一介绍了,全都介绍篇幅会太大了。
收获
Python最佳实践
- 引入同一个包内的其他py文件
from . import a, b
from .a import function
- 同一个包引入大量模块,函数
from xxx import (
xxxx, xxxx, xxxx,
xxxx, xxxx, xxxx
)
- 函数,方法说明
def request(method, url, **kwargs):
"""函数的功能,总体概括
:param method: xxxxx.
:param url: xxxxx.
:param xxx: xxxx.
:return: xxxx
:rtype: xxx
Usage::
>>> import requests
>>> req = requests.request('GET', 'url')
"""
最佳实践里存在一个困惑,那就是flake8要求每行字符个数不大于80,就算放宽要求到达100,也有些许注释超过这个数值。
编码问题
编码问题是个很头疼的问题,requests库里充斥着大量的编码转换功能,主要有两个原因
- py2, py3的字符串编码方式不同(不了解py2)
- http协议的各部分与python字符串编码的冲突
因为计算机保存字符都是0,1串,把同一个我们能看到的抽象的字符标示成这种0,1串的话,根据不同的编码方式,就可以转换成不同的0,1串,但有时候这样不同的编码方式需要在一起使用,就需要统一成一种方式。
在Python3中,str
默认是unicode编码,而unicode这种编码方式支持的字符个数比较多,所以可以作为一种中间编码,在两种编码方式中转换。即假设有A,B两种不同编码,一个A类型的串需要编码为B类型的串,则先需要将A类型的串解码成unicode串,在将这个unicode串编码成B类型的串。在Python中decode
为解码,encode
为编码。
string = '中国人'
print(string.encode('utf-8'))
# b'\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba'
print(string.encode('gbk'))
# b'\xd6\xd0\xb9\xfa\xc8\xcb'
temp = b'\xd6\xd0\xb9\xfa\xc8\xcb'
print(temp.decode('gbk'))
# '中国人'
print(temp.decode('utf-8'))
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
异常捕获
库中有一个模块专门定义了一些异常,代码中也经常出现try...except...的组合,甚至是嵌套组合,捕捉的异常类型非常的细,防御性编程是让代码稳健运行必不可少的部分,但捕捉的太粗太泛就会导致错误定位不精准。
魔法方法
Python中有很多魔法方法,那些在类中以__开头和结尾的方法,在代码中出现很多次,几乎每一个模块都有,而我平时很少用到,在日常写代码的时候很少去继承Python内置的对象,除了object,在解决问题的方法上也有一些差距。
getattr, hasattr
这些也是代码中高频语法,但是还不能理解相对于直接使用Obj.value有什么优势。
总结
差不多是把源代码都看了一遍,但很多地方都是一眼扫过去,没有关注它的作用,更多是关注他的代码风格。可以说差距真的是非常大,差距来自对问题的解决方法上以及对python语言本身的理解上。
第一次读源码肯定是比较难的,所以应该找一些小一点的,容易理清脉络的库或项目,然后从最外层的框架入手,从宏观上了解每一部分的作用,按照这个框架去深入,而不能一个模块一个模块独立开来阅读。看不懂的地方,有的多看几遍后会明了一些,有的可能还是不懂,那么就不要纠结,继续看下去,不能奢求读完源码就学会了作者的所有技术,但对自己肯定是有潜移默化的影响。