python之requests模块高级用法
一、会话对象
①会话对象让你能够跨请求保持某些参数。它也会在同一个 Session 实例发出的所有请求之间保持 cookie, 期间使用 urllib3库 的 connection pooling 【连接池】功能。
所以如果向同一主机发送多个请求,底层的 TCP 连接将会被重用【同一服务器地址发起的多个请求不需要重新token身份认证,比如对同一服务器不同资源发起的请求】,从而带来显著的性能提升。
会话对象具有主要的 Requests API 的所有方法。
实例:跨请求【注意这里是跨请求而不是跨域】保持一些 cookie:
import requests session = requests.Session() session.get('http://httpbin.org/cookies/set/sessioncookie/123456789') r = session.get("http://httpbin.org/cookies") print(r.text)
运行结果:
②会话对象也可用来为请求时提供缺省数据。这是通过 为会话对象的属性提供数据 来实现的:
import requests session = requests.Session() session.auth = ('user', 'pass') session.headers.update({'x-test': 'true'}) # both 'x-test' and 'x-test2' are sent response = session.get('http://httpbin.org/headers', headers={'x-test2': 'true'}) print(response.request.headers)
运行结果:【任何传递给请求方法的字典都会与已设置会话层数据合并【requests headers】。方法层的参数覆盖会话的参数。】
③【注意】即使使用会话对象,方法级别的参数不会被跨请求保持。
例如:第一个请求发送 cookie,第二个请求无法获取cookie:
import requests session = requests.Session() r = session.get('http://httpbin.org/cookies', cookies={'from-my': 'browser'}) # 这里的cookie是通过参数的方式发送至服务器,而不是通过请求头的方式 print(r.json()) # '{"cookies": {"from-my": "browser"}}' r = session.get('http://httpbin.org/cookies') print(r.json()) # '{"cookies": {}}'
④会话对象可以用作前后文管理器:
这样可以确保 with 区块退出后会话对象可以及时关闭,即使发生了异常也一样。【正常情况下使用会话对象,请求结束过后需要通过调用会话对象的 session.close() 关闭】
import requests with requests.Session() as session: response = session.get('http://httpbin.org/cookies/set/sessioncookie/123456789') print(response.request.headers)
二、请求与响应对象
任何时候调用 requests.*() 方法请求服务器时其实是在做两件主要的事情。
其一,构建一个 Request请求对象, 该对象将被发送到某个服务器请求或查询一些资源。
其二,一旦Requests请求对象得到一个从服务器返回的响应就会产生一个Response响应对象。【Response响应对象中包含服务器返回的所有信息, 也包含原来创建的Requests请求
对象。】
三、准备的请求【Prepared Request】
当通过 requests.request 方式或者会话对象的方式发起请求并响应,收到一个 Response 响应对象时, request 属性其实是使用了 PreparedRequest 。有时在发送请求之前,你需要对 requests body 或者 requests headers(或者别的什么东西)做一些额外处理,下面演示了一个简单的做法:
from requests import Request, Session session = Session() req = Request('GET', url, data=data, headers=header ) prepped = req.prepare() # do something with prepped.body # do something with prepped.headers resp = session.send(prepped, stream=stream, verify=verify, proxies=proxies, cert=cert, timeout=timeout ) print(resp.status_code)
由于你没有对 Request
对象做什么特殊事情,你立即准备和修改了 PreparedRequest
对象,然后把它和别的参数一起发送到 requests.*
或者 Session.*
。
然而,上述代码会失去 Requests Session
对象的一些优势, 尤其 Session
级别的状态,例如 cookie 就不会被应用到你的请求上去。要获取一个带有状态的 PreparedRequest
, 请用Session.prepare_request()
取代 Request.prepare()
的调用,如下所示:
from requests import Request, Session session = Session() req = Request('GET', url, data=data headers=headers ) prepped = session.prepare_request(req) # do something with prepped.body # do something with prepped.headers resp = s.send(prepped, stream=stream, verify=verify, proxies=proxies, cert=cert, timeout=timeout ) print(resp.status_code)
四、SSL 证书验证
Requests库默认附带了一套它信任的根证书;Requests可以为HTTPS请求验证SSL证书,就像web浏览器一样。SSL验证默认是开启的;(即 verify 参数默认为 True )
如果要想检查某个服务器的 SSL 证书,可以使用 verify参数;如果证书验证失败,Requests 会抛出 SSLError。
默认情况下, verify 参数为 True 。verify 参数仅应用于验证服务器主机证书。
例如1:在该服务器上没有设置 SSL,所以在请求时Requests检查该服务器上的SSL证书失败,无法正常像该服务器发出请求。
requests.get('https://kennethreitz.com', verify=True) # requests.exceptions.SSLError: hostname 'kennethreitz.com' doesn't match either of '*.herokuapp.com', 'herokuapp.com'
例如2:Github 网站设置了 SSL:
import requests response = requests.get('https://github.com', verify=True) # <Response [200]>
例如3:将 verify 参数设置为 False ,Requests 会忽略对 SSL 证书的验证。
import requests # 设置verify参数为False,不校验请求服务器上的SSL证书 requests.get('https://kennethreitz.com', verify=False) # <Response [200]>
例如4:对于私有证书,可以传递一个 CA_BUNDLE 文件路径(CA证书)给 verify 参数。也可以设置 REQUEST_CA_BUNDLE 环境变量。
例如5:指定一个本地证书用作客户端证书,可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:
import requests requests.get('https://kennethreitz.com', cert=('/path/server.crt', '/path/key')) # <Response [200]>
如果指定了一个错误路径或一个无效的证书,则抛出异常。如下:
import requests requests.get('https://kennethreitz.com', cert='/wrong_path/server.pem') # SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib
警告:本地证书的私有 key 必须是解密状态。目前,Requests 不支持使用加密的 key。
五、CA 证书
Requests 默认附带了一套它信任的根证书,来自于 Mozilla trust store。然而它们在每次 Requests 更新时才会更新。
这意味着如果你固定使用某一版本的 Requests,你的证书有可能已经太旧了。
从 Requests 2.4.0 版之后,如果系统中装了 certifi 包,Requests 会试图使用它里边的证书。这样用户就可以在不修改代码的情况下更新他们的可信任证书。
为了安全起见,我们建议你经常更新 certifi。
六、响应体内容工作流
默认情况下,当你进行网络请求后,响应体会立即被下载。你可以通过 stream
参数覆盖这个行为,推迟下载响应体直到访问 Response.content
属性:
tarball_url = 'https://github.com/kennethreitz/requests/tarball/master' r = requests.get(tarball_url, stream=True)
此时仅有响应头被下载下来了,连接保持打开状态,因此允许我们根据条件获取内容:
if int(r.headers['content-length']) < TOO_LONG: content = r.content ...
你可以进一步使用 Response.iter_content
和 Response.iter_lines
方法来控制工作流,或者以 Response.raw
从底层 urllib3 的 urllib3.HTTPResponse <urllib3.response.HTTPResponse
读取。
如果你在请求中把 stream
设为 True
,Requests 无法将连接释放回连接池,除非你消耗了所有的数据,或者调用了 Response.close
。 这样会带来连接效率低下的问题。如果你发现你在使用stream=True
的同时还在部分读取请求的 body(或者完全没有读取 body),那么你就应该考虑使用 contextlib.closing
(文档), 如下所示:
from contextlib import closing with closing(requests.get('http://httpbin.org/get', stream=True)) as r: # 在此处理响应
七、保持活动状态(持久连接)
好消息——归功于 urllib3,同一会话内的持久连接是完全自动处理的!同一会话内你发出的任何请求都会自动复用恰当的连接!
注意:只有所有的响应体数据被读取完毕连接才会被释放为连接池;所以确保将 stream
设置为False
或读取 Response
对象的 content
属性。
八、流式上传
Requests支持流式上传,这允许你发送大的数据流或文件而无需先把它们读入内存。要使用流式上传,仅需为你的请求体提供一个类文件对象即可:
with open('massive-body') as f: requests.post('http://some.url/streamed', data=f)
警告:我们强烈建议你用二进制模式(binary mode)打开文件。这是因为 requests 可能会为你提供 header 中的 Content-Length
,在这种情况下该值会被设为文件的字节数。如果你用文本模式打开文件,就可能碰到错误。
九、块编码请求
对于出去和进来的请求,Requests 也支持分块传输编码。要发送一个块编码的请求,仅需为你的请求体提供一个生成器(或任意没有具体长度的迭代器):
def gen(): yield 'hi' yield 'there' requests.post('http://some.url/chunked', data=gen())
对于分块的编码请求,我们最好使用 Response.iter_content()
对其数据进行迭代。在理想情况下,你的 request 会设置 stream=True
,这样你就可以通过调用 iter_content
并将分块大小参数设为 None
,从而进行分块的迭代。如果你要设置分块的最大体积,你可以把分块大小参数设为任意整数。
十、POST 多个分块编码的文件
你可以在一个请求中发送多个文件。例如,假设你要上传多个图像文件到一个 HTML 表单,使用一个多文件 field 叫做 "images":
<input type="file" name="images" multiple="true" required="true"/>
要实现,只要把文件设到一个元组的列表中,其中元组结构为 (form_field_name, file_info)
:
>>> url = 'http://httpbin.org/post' >>> multiple_files = [ ('images', ('foo.png', open('foo.png', 'rb'), 'image/png')), ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))] >>> r = requests.post(url, files=multiple_files) >>> r.text { ... 'files': {'images': 'data:image/png;base64,iVBORw ....'} 'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a', ... }
警告:我们强烈建议你用二进制模式(binary mode)打开文件。这是因为 requests 可能会为你提供 header 中的 Content-Length
,在这种情况下该值会被设为文件的字节数。如果你用文本模式打开文件,就可能碰到错误。
十一、事件挂钩
Requests有一个钩子系统,你可以用来操控部分请求过程,或信号事件处理。
可用的钩子:
response:
- 从一个请求产生的响应
你可以通过传递一个 {hook_name: callback_function}
字典给 hooks
请求参数 为每个请求分配一个钩子函数:
hooks=dict(response=print_url)
callback_function
会接受一个数据块作为它的第一个参数。
def print_url(r): print(r.url)
若执行你的回调函数期间发生错误,系统会给出一个警告。
若回调函数返回一个值,默认以该值替换传进来的数据。若函数未返回任何东西, 也没有什么其他的影响。
我们来在运行期间打印一些请求方法的参数:
>>> requests.get('http://httpbin.org', hooks=dict(response=print_url)) http://httpbin.org <Response [200]>
十二、自定义身份验证
Requests 允许你使用自己指定的身份验证机制。
任何传递给请求方法的 auth
参数的可调用对象,在请求发出之前都有机会修改请求。
自定义的身份验证机制是作为 requests.auth.AuthBase
的子类来实现的,也非常容易定义。Requests 在 requests.auth
中提供了两种常见的的身份验证方案: HTTPBasicAuth
和HTTPDigestAuth
。
假设我们有一个web服务,仅在 X-Pizza
头被设置为一个密码值的情况下才会有响应。虽然这不太可能,但就以它为例好了。
from requests.auth import AuthBase class PizzaAuth(AuthBase): """Attaches HTTP Pizza Authentication to the given Request object.""" def __init__(self, username): # setup any auth-related data here self.username = username def __call__(self, r): # modify and return the request r.headers['X-Pizza'] = self.username return r
然后就可以使用我们的PizzaAuth来进行网络请求:
>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth')) <Response [200]>
十三、流式请求
使用 requests.Response.iter_lines()
你可以很方便地对流式 API (例如 Twitter 的流式 API) 进行迭代。简单地设置 stream
为 True
便可以使用 iter_lines()
对相应进行迭代:
import json import requests r = requests.get('http://httpbin.org/stream/20', stream=True) for line in r.iter_lines(): # filter out keep-alive new lines if line: print(json.loads(line))
警告
iter_lines()
不保证重进入时的安全性。多次调用该方法 会导致部分收到的数据丢失。如果你要在多处调用它,就应该使用生成的迭代器对象:
lines = r.iter_lines() # Save the first line for later or just skip it first_line = next(lines) for line in lines: print(line)
十四、代理
果需要使用代理,你可以通过为任意请求方法提供 proxies
参数来配置单个请求:
import requests proxies = { "http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080", } requests.get("http://example.org", proxies=proxies)
你也可以通过环境变量 HTTP_PROXY
和 HTTPS_PROXY
来配置代理。
$ export HTTP_PROXY="http://10.10.1.10:3128" $ export HTTPS_PROXY="http://10.10.1.10:1080" $ python >>> import requests >>> requests.get("http://example.org")
若你的代理需要使用HTTP Basic Auth,可以使用 http://user:password@host/ 语法:
proxies = { "http": "http://user:pass@10.10.1.10:3128/", }
要为某个特定的连接方式或者主机设置代理,使用 scheme://hostname 作为 key, 它会针对指定的主机和连接方式进行匹配。
proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}
十五、SOCKS
除了基本的 HTTP 代理,Request 还支持 SOCKS 协议的代理。这是一个可选功能,若要使用, 你需要安装第三方库。
你可以用 pip
获取依赖:
pip install requests[socks]
安装好依赖以后,使用 SOCKS 代理和使用 HTTP 代理一样简单:
proxies = { 'http': 'socks5://user:pass@host:port', 'https': 'socks5://user:pass@host:port' }
十六、合规性
Requests 符合所有相关的规范和 RFC,这样不会为用户造成不必要的困难。但这种对规范的考虑导致一些行为对于不熟悉相关规范的人来说看似有点奇怪。
十七、编码方式
Requests 提供了几乎所有HTTP动词的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH、DELETE。以下内容为使用 Requests 中的这些动词以及 Github API 提供了详细示例。
我将从最常使用的动词 GET 开始。HTTP GET 是一个幂等方法,从给定的 URL 返回一个资源。因而,当你试图从一个 web 位置获取数据之时,你应该使用这个动词。一个使用示例是尝试从 Github 上获取关于一个特定 commit 的信息。假设我们想获取Requests的commit a050faf
的信息。我们可以这样去做:
>>> import requests >>> r = requests.get('https://api.github.com/repos/kennethreitz/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad')
我们应该确认 GitHub 是否正确响应。如果正确响应,我们想弄清响应内容是什么类型的。像这样去做:
>>> if (r.status_code == requests.codes.ok): ... print r.headers['content-type'] ... application/json; charset=utf-8
可见,GitHub 返回了 JSON 数据,非常好,这样就可以使用 r.json
方法把这个返回的数据解析成 Python 对象。
>>> commit_data = r.json() >>> print commit_data.keys() [u'committer', u'author', u'url', u'tree', u'sha', u'parents', u'message'] >>> print commit_data[u'committer'] {u'date': u'2012-05-10T11:10:50-07:00', u'email': u'me@kennethreitz.com', u'name': u'Kenneth Reitz'} >>> print commit_data[u'message'] makin' history
到目前为止,一切都非常简单。嗯,我们来研究一下 GitHub 的 API。我们可以去看看文档,但如果使用 Requests 来研究也许会更有意思一点。我们可以借助 Requests 的 OPTIONS 动词来看看我们刚使用过的 url 支持哪些 HTTP 方法。
>>> verbs = requests.options(r.url) >>> verbs.status_code 500
额,这是怎么回事?毫无帮助嘛!原来 GitHub与许多 API 提供方一样,实际上并未实现 OPTIONS 方法。这是一个恼人的疏忽,但没关系,那我们可以使用枯燥的文档。然而,如果 GitHub 正确实现了 OPTIONS,那么服务器应该在响应头中返回允许用户使用的 HTTP 方法,例如:
>>> verbs = requests.options('http://a-good-website.com/api/cats') >>> print verbs.headers['allow'] GET,HEAD,POST,OPTIONS
转而去查看文档,我们看到对于提交信息,另一个允许的方法是 POST,它会创建一个新的提交。由于我们正在使用 Requests 代码库,我们应尽可能避免对它发送笨拙的 POST。作为替代,我们来玩玩 GitHub 的 Issue 特性。
本篇文档是回应 Issue #482 而添加的。鉴于该问题已经存在,我们就以它为例。先获取它。
>>> r = requests.get('https://api.github.com/repos/kennethreitz/requests/issues/482') >>> r.status_code 200 >>> issue = json.loads(r.text) >>> print(issue[u'title']) Feature any http verb in docs >>> print(issue[u'comments']) 3
十八、响应头链接字段
许多 HTTP API 都有响应头链接字段的特性,它们使得 API 能够更好地自我描述和自我显露。
GitHub 在 API 中为 分页 使用这些特性,例如:
>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10' >>> r = requests.head(url=url) >>> r.headers['link'] '<https://api.github.com/users/kennethreitz/repos?page=2&per_page=10>; rel="next", <https://api.github.com/users/kennethreitz/repos?page=6&per_page=10>; rel="last"'
Requests 会自动解析这些响应头链接字段,并使得它们非常易于使用:
>>> r.links["next"] {'url': 'https://api.github.com/users/kennethreitz/repos?page=2&per_page=10', 'rel': 'next'} >>> r.links["last"] {'url': 'https://api.github.com/users/kennethreitz/repos?page=7&per_page=10', 'rel': 'last'}
十九、传输适配器
从 v1.0.0 以后,Requests 的内部采用了模块化设计。部分原因是为了实现传输适配器(Transport Adapter),你可以看看关于它的最早描述。传输适配器提供了一个机制,让你可以为 HTTP 服务定义交互方法。尤其是它允许你应用服务前的配置。
Requests 自带了一个传输适配器,也就是 HTTPAdapter
。 这个适配器使用了强大的 urllib3,为 Requests 提供了默认的 HTTP 和 HTTPS 交互。每当 Session
被初始化,就会有适配器附着在Session
上,其中一个供 HTTP 使用,另一个供 HTTPS 使用。
Request 允许用户创建和使用他们自己的传输适配器,实现他们需要的特殊功能。创建好以后,传输适配器可以被加载到一个会话对象上,附带着一个说明,告诉会话适配器应该应用在哪个 web 服务上。
>>> s = requests.Session() >>> s.mount('http://www.github.com', MyAdapter())
这个 mount 调用会注册一个传输适配器的特定实例到一个前缀上面。加载以后,任何使用该会话的 HTTP 请求,只要其 URL 是以给定的前缀开头,该传输适配器就会被使用到。
传输适配器的众多实现细节不在本文档的覆盖范围内,不过你可以看看接下来这个简单的 SSL 用例。更多的用法,你也许该考虑为``requests.adapters.BaseAdapter`` 创建子类。
示例: 指定的 SSL 版本
Requests 开发团队刻意指定了内部库(urllib3)的默认 SSL 版本。一般情况下这样做没有问题,不过是不是你可能会需要连接到一个服务节点,而该节点使用了和默认不同的 SSL 版本。
你可以使用传输适配器解决这个问题,通过利用 HTTPAdapter 现有的大部分实现,再加上一个ssl_version 参数并将它传递到 urllib3
中。我们会创建一个传输适配器,用来告诉 urllib3
让它使用 SSLv3:
import ssl from requests.adapters import HTTPAdapter from requests.packages.urllib3.poolmanager import PoolManager class Ssl3HttpAdapter(HTTPAdapter): """"Transport adapter" that allows us to use SSLv3.""" def init_poolmanager(self, connections, maxsize, block=False): self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, block=block, ssl_version=ssl.PROTOCOL_SSLv3)
二十、阻塞和非阻塞
使用默认的传输适配器,Requests 不提供任何形式的非阻塞 IO。 Response.content
属性会阻塞,直到整个响应下载完成。如果你需要更多精细控制,该库的数据流功能(见 流式请求) 允许你每次接受少量的一部分响应,不过这些调用依然是阻塞式的。
如果你对于阻塞式 IO 有所顾虑,还有很多项目可以供你使用,它们结合了 Requests 和 Python 的某个异步框架。典型的优秀例子是 grequests 和 requests-futures。
二十一、Header 排序
在某些特殊情况下你也许需要按照次序来提供 header,如果你向 headers
关键字参数传入一个OrderedDict
,就可以向提供一个带排序的 header。然而,Requests 使用的默认 header 的次序会被优先选择,这意味着如果你在 headers
关键字参数中覆盖了默认 header,和关键字参数中别的 header 相比,它们也许看上去会是次序错误的。
如果这个对你来说是个问题,那么用户应该考虑在 Session
对象上面设置默认 header,只要将Session
设为一个定制的 OrderedDict
即可。这样就会让它成为优选的次序。
二十二、超时(timeout)
为防止服务器不能及时响应,大部分发至外部服务器的请求都应该带着 timeout 参数。如果没有 timeout,你的代码可能会挂起若干分钟甚至更长时间。
连接超时指的是在你的客户端实现到远端机器端口的连接时(对应的是`connect()`_),Request 会等待的秒数。一个很好的实践方法是把连接超时设为比 3 的倍数略大的一个数值,因为 TCP 数据包重传窗口 (TCP packet retransmission window) 的默认大小是 3。
一旦你的客户端连接到了服务器并且发送了 HTTP 请求,读取超时指的就是客户端等待服务器发送请求的时间。(特定地,它指的是客户端要等待服务器发送字节之间的时间。在 99.9% 的情况下这指的是服务器发送第一个字节之前的时间)。
如果你制订了一个单一的值作为 timeout,如下所示:
r = requests.get('https://github.com', timeout=5)
这一 timeout 值将会用作 connect
和 read
二者的 timeout。如果要分别制定,就传入一个元组:
r = requests.get('https://github.com', timeout=(3.05, 27))
如果远端服务器很慢,你可以让 Request 永远等待,传入一个 None 作为 timeout 值,然后就冲咖啡去吧。
r = requests.get('https://github.com', timeout=None)