工作两年了,一直用 python 写一些 API 之类的东西,自动化框架也有涉及,却一直感觉对个人技能提升缓慢。决定开这个坑,是之前看到@wangshunping的read requests,生动有趣,可惜 0.8.0 之后没有更新了。待我稍稍有了一点看源码的动力,就想接着下去写。真是漫漫长路啊,4409 个 commit,1000 多个 PR,更何况还有珠玉在前,实在没有把握能把这块硬骨头给啃下来,写一点是一点吧。作为 python 的小学生,一些错误在所难免,希望大家指出,互相讨论。 下面就开始吧!
目标
0.8.0 (2011-11-13)
++++++++++++++++++
* Keep-alive support!
* Complete removal of Urllib2
* Complete removal of Poster
* Complete removal of CookieJars
* New ConnectionError raising
* Safe_mode for error catching
* prefetch parameter for request methods
* OPTION method
* Async pool size throttling
* File uploads send real names
源码阅读
v0.7.1
0.7.1 (2011-10-23)
++++++++++++++++++
* Move away from urllib2 authentication handling.
* Fully Remove AuthManager, AuthObject, &c.
* New tuple-based auth system with handler callbacks.
- 移除
urllib2
的 authentication 处理 - 完全移除
AuthManager
,AuthObject
和。。。&c? - 新的元组形式的
auth
机制和处理器回调函数。
1. 移除urllib2
的 authentication 处理
添加一个auth.py
文件,加入了自己实现的 auth 处理器,包含http_basic
和http_digest
,分别对应 Headers 中Autohorization
以Basic
和Digest
开头的情形。
2. 完全删除AuthManager
, AuthObject
和。。。&c?
由于接口改用了 session,于是就没有必要使用AuthManager
储存认证信息。使用自己实现的处理器,完全删除models.py
中相关的代码。
3. 新的元组形式的auth
机制和处理器回调函数。
现在:
self.auth = auth_dispatch(auth)
if self.auth:
auth_func, auth_args = self.auth
r = auth_func(self, *auth_args)
self.__dict__.update(r.__dict__)
def dispatch(t):
"""Given an auth tuple, return an expanded version."""
if not t:
return t
else:
t = list(t)
# Make sure they're passing in something.
assert len(t) >= 2
# If only two items are passed in, assume HTTPBasic.
if (len(t) == 2):
t.insert(0, 'basic')
# Allow built-in string referenced auths.
if isinstance(t[0], basestring):
if t[0] in ('basic', 'forced_basic'):
t[0] = http_basic
elif t[0] in ('digest',):
t[0] = http_digest
# Return a custom callable.
return (t[0], tuple(t[1:]))
通过dispatch
函数,若传入二元元组,则默认前面加上'basic'
,使用http_basic
处理,否则需要指定处理类型。支持自定义处理器:
def pizza_auth(r, username):
"""Attaches HTTP Pizza Authentication to the given Request object.
"""
r.headers['X-Pizza'] = username
return r
Then, we can make a request using our Pizza Auth::
>>> requests.get('http://pizzabin.org/admin', auth=(pizza_auth, 'kenneth'))
<Response [200]>
v0.7.2
0.7.2 (2011-10-23)
++++++++++++++++++
* PATCH Fix.
修正 BUG(略)
v0.7.3
0.7.3 (2011-10-23)
++++++++++++++++++
* Digest Auth fix.
修正 Digest Auth 的 BUG 主要是删除了一些 debug 的 print 语句,估计当时作者脑子也不清醒了,我还注意到他改了一个文件头的”~“的长度,是有够无聊的!0.7.1 到 0.7.3 都在一个多小时内完成,小伙子动力很足啊!
v0.7.4
0.7.4 (2011-10-26)
++++++++++++++++++
* Sesion Hooks fix.
主要是一些代码的美化和小 BUG,给session
加了一个keep_alive
参数,暂时还没用上,应该是为以后做准备。
v0.7.5
0.7.5 (2001-11-04)
++++++++++++++++++
* Response.content = None if there was an invalid repsonse.
* Redirection auth handling.
咦?日期穿越了 10 年?哈哈,什么时候会改呢?
- 如果是无效响应则
content = None
- 重定向认证处理
1. 无效响应content = None
加入一个 Error Handling:
try:
self._content = self.raw.read()
except AttributeError:
return None
2. 重定向认证处理
一个 BUG,原来是用 dispatch 后的 auth 构造新的 Request 会导致错误,现在使用self._auth
保存原始 auth 并传入新的 Request 对象。
v0.7.6
0.7.6 (2011-11-07)
++++++++++++++++++
* Digest authentication bugfix (attach query data to path)
- Digest 认证的 BUG 修复(在路径后附上 query)
原来:
path = urlparse(r.request.url).path
现在:
p_parsed = urlparse(r.request.url)
path = p_parsed.path + p_parsed.query
我注意到日期问题已经修复了:
Updated your 2001, to 2011… unless you went back in time ;)
这个幽默。
v0.8.0
0.8.0 (2011-11-13)
++++++++++++++++++
* Keep-alive support!
* Complete removal of Urllib2
* Complete removal of Poster
* Complete removal of CookieJars
* New ConnectionError raising
* Safe_mode for error catching
* prefetch parameter for request methods
* OPTION method
* Async pool size throttling
* File uploads send real names
- 支持
keep_alive
参数(填坑来了) - 完全抛弃
urllib2
- 完全抛弃
Poster
- 完全抛弃
CookieJars
- 新的
ConnectionError
抛出 - 安全的处理异常机制。
- 为请求方法加入
prefetch
参数 - 新的
OPTION
方法 - 节省 Async 池的大小
- 上传文件发送真实文件名
1. 支持keep_alive
参数
作者在 v0.8.0 全面转向urllib3
,这是个第三方的轮子,它相对于urllib2
最大的改进是可以重用 HTTP 连接,不用每个 request 都新建一个连接了。这样大大加快了大量 request 时的响应速度。
self.poolmanager = PoolManager(
num_pools=self.config.get('pool_connections'),
maxsize=self.config.get('pool_maxsize')
)
proxy = self.proxies.get(_p.scheme)
if proxy:
conn = poolmanager.proxy_from_url(url)
else:
# Check to see if keep_alive is allowed.
if self.config.get('keep_alive'):
conn = self._poolmanager.connection_from_url(url)
else:
conn = connectionpool.connection_from_url(url)
keep_alive
是默认打开的,在urllib3
中维护了一个连接池,当对某个 url 进行请求时,会从连接池中取出该连接,然后发送请求时直接调用此连接的子方法。
2. 完全抛弃urllib2
删除了models.py
中用来发送请求的build_opener
函数,使用urllib3
的conn.urlopen
方法。
3.完全抛弃Poster
同上,用一个轮子换了另一个轮子。。
4. 完全抛弃CookieJars
上测试
def test_session_persistent_cookies(self):
s = requests.session()
# Internally dispatched cookies are sent.
_c = {'kenneth': 'reitz', 'bessie': 'monke'}
r = s.get(httpbin('cookies'), cookies=_c)
r = s.get(httpbin('cookies'))
# Those cookies persist transparently.
c = json.loads(r.content).get('cookies')
assert c == _c
# Double check.
r = s.get(httpbin('cookies'), cookies={})
c = json.loads(r.content).get('cookies')
assert c == _c
# Remove a cookie by setting it's value to None.
r = s.get(httpbin('cookies'), cookies={'bessie': None})
c = json.loads(r.content).get('cookies')
del _c['bessie']
assert c == _c
# Test session-level cookies.
s = requests.session(cookies=_c)
r = s.get(httpbin('cookies'))
c = json.loads(r.content).get('cookies')
assert c == _c
# Have the server set a cookie.
r = s.get(httpbin('cookies', 'set', 'k', 'v'), allow_redirects=True)
c = json.loads(r.content).get('cookies')
assert 'k' in c
# And server-set cookie persistience.
r = s.get(httpbin('cookies'))
c = json.loads(r.content).get('cookies')
assert 'k' in c
处理响应的 cookie:
if 'set-cookie' in response.headers:
cookie_header = response.headers['set-cookie']
c = SimpleCookie()
c.load(cookie_header)
for k,v in c.items():
cookies.update({k: v.value})
# Save cookies in Response.
response.cookies = cookies
cookies = self.cookies
self.cookies.update(r.cookies)
发送请求时:
if self.cookies:
# Skip if 'cookie' header is explicitly set.
if 'cookie' not in self.headers:
# Simple cookie with our dict.
c = SimpleCookie()
for (k, v) in self.cookies.items():
c[k] = v
# Turn it into a header.
cookie_header = c.output(header='').strip()
# Attach Cookie header to request.
self.headers['Cookie'] = cookie_header
使用了标准库里的SimpleCookie
处理和生成 cookie,而读取 cookie 全部都是字典类型。其实这些都是为了新的urllib3
接口而服务的,从原来的各种 Handler 改成conn.urlopen
以后原来的东西都相应的变化。
5. 新的ConnectionError
6. 安全模式
直接看代码吧:
except MaxRetryError, e:
if not self.config.get('safe_mode', False):
raise ConnectionError(e)
else:
r = None
except (_SSLError, _HTTPError), e:
if not self.config.get('safe_mode', False):
raise Timeout('Request timed out.')
所谓安全模式就是不抛出异常。
7. 新的prefetch
参数
也是urllib3
支持的参数,当为True
时,在发送请求时就读取响应内容,否则跟原来一样调用content
方法时读取。至于这个有什么用我还不是太懂,因为我发现当prefetch=True
时读取content
会出错并且无法获取响应内容,疑似 BUG,先放在这里。
8. OPTION
请求方法
Option 是一种 HTTP 的请求类型,返回当前 url 支持的全部方法。
9. 节省 async 池的大小
原来:
jobs = [gevent.spawn(send, r) for r in requests]
gevent.joinall(jobs)
现在:
if size:
pool = Pool(size)
pool.map(send, requests)
pool.join()
else:
jobs = [gevent.spawn(send, r) for r in requests]
gevent.joinall(jobs)
大概就是传入一个size
参数,所有的异步请求都在这个有限大小的池里处理,嗯,又是池,真是一个好用的东西。
10. 上传文件时包含真实文件名
看代码:
def guess_filename(obj):
"""Tries to guess the filename of the given object."""
name = getattr(obj, 'name', None)
if name and name[0] != '<' and name[-1] != '>':
return name
嗯,怎么得到真实文件名?靠猜啊,没有就拉倒。
后记
呼,终于整完了,v0.8.0 包含一个大的重构,我这个累的啊。第一次写这种东西,感觉不是很满意,代码太多了自己的试验不太够,总的也就能理解 80% 左右吧。不管怎样,谢谢大家的阅读,欢迎交流。