HTTP Client
HTTP 是一种用作获取诸如 HTML 文档这类资源的文件传输协议。同时他也是一种 C/S 协议,即客户端发出请求(request),由服务器接受来应答返回响应(response)。
一个 HTTP 请求库分为两个方面: 首先构建请求,然后等待服务器应答来获取响应。其中 aiohttp 提供了异步的 HTTP 请求支持。这意味着他能够异步处理服务器返回的响应。
报文
HTTP 协议通过 TCP/IP 来传输,其中传输的都是字节流。
请求报文
一个标准的请求包括几个方面:
- HTTP 方法: 其中最为常用的就是 GET 和 POST 方法
- 要获取的资源路径: 也就是我们所说的 URL
- HTTP 协议以及版本号
- 请求头: 其中包含了要告知服务器的元信息
- 请求体: 像 POST 这样的方法需要将一些资源发送给服务器就需要添加请求体
上面的内容会以 HTTP 报文的形式发送给服务器:
POST /search HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */*
Referer: <a href="http://www.google.cn/">http://www.google.cn/</a>
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)
Host: <a href="http://www.google.cn">www.google.cn</a>
Connection: Keep-Alive
Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-FxlRugatx63JLv7CWMD6UB_O_r
hl=zh-CN&source=hp&q=domety
Tips
所有的请求库说白了就是方便的以编程的形式构建 HTTP 报文的过程
响应报文
服务器将应答发回给客户端:
HTTP/1.1 200 OK
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 122
<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>
他包括:
- HTTP 协议以及版本号
- 响应状态码和信息: 300 - OK
- 响应头: 其中包含了要告知客户端的元信息
- 响应体: 呈现给用户的内容
使用 aiohttp 构建请求
一个标准的在 aiohttp 构建请求的方式如下:
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get('http://httpbin.org/get') as resp:
print(resp.status)
asyncio.run(main())
ClientSession
ClientSession是 aiohttp 中所有客户端 API 的心脏和主要入口点。会话的核心是他维护了一个连接池(默认 100 个,注意是链接 100 个不同 host 的网站,而不是当个 host 100 个连接),并且会话中对同一个 host 的请求会共享 cookie。
发起请求
当我们持有了 ClientSession
后可以通过 session.get
、session.post
这样的形式来发起对应请求方法的请求,如果想要对同一个站点发起多个请求,也可以简单的通过下面的方式实现:
async with aiohttp.ClientSession('http://httpbin.org') as session:
async with session.get('/get'):
pass
async with session.post('/post', data=b'data'):
pass
async with session.put('/put', data=b'data'):
pass
Note
session.get
等方法是对内部的 client.request("GET")
的封装
Tips
一定不要为每一个请求都创建一个会话,一个会话里面维护了连接池、Keep-Alive 等能够提高性能
在 URL 中传递参数
如果 URL 需要添加参数,需要构建 http://xxx.org/get?key1=val1&key2=val2
这样的形式,并且如果包含非法字符还需要将他们编码。 aiohttp 提供了更加方便的形式来处理他们:
async with aiohttp.ClientSession() as session:
params = {'key1': 'value1', 'key2': 'value2'}
async with session.get('http://httpbin.org/get', params=params) as resp:
expect = 'http://httpbin.org/get?key1=value1&key2=value2'
assert str(resp.url) == expect
构建请求头
我们可以向请求添加 HTTP 请求头来定制请求。有两种传递形式:
ClientSession
的 headers 参数可以为所有使用该会话的请求共享- 特定于请求的 headers 参数
# 所有使用该会话的请求共享该 headers
headers={"Authorization": "Basic bG9naW46cGFzcw=="}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get("http://httpbin.org/headers") as r:
json_body = await r.json()
assert json_body['headers']['Authorization'] == \
'Basic bG9naW46cGFzcw=='
# 特定于指定请求的 headers 参数
url = 'http://example.com/image'
payload = b'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00'
b'\x00\x00\x01\x00\x01\x00\x00\x02\x00;'
headers = {'content-type': 'image/gif'}
await session.post(url,
data=payload,
headers=headers)
请求体
通常构造 post 请求需要添加请求体,则由 data 参数来指定。需要注意的时 data 的值由请求头中的 Content-Type 决定。
他对应到 HTML 中就是 form 元素的 enctype 属性的值:
<!-- method="post" 才能指定 enctype -->
<form action="/foo" method="post" enctype="multipart/form-data">
<input type="text" name="description" value="一些文本" />
<input type="file" name="myFile" />
<button type="submit">提交</button>
</form>
Tips
实际上还有其他类型,具体要看后端服务器提供的接口。不过 HTML 表单仅仅支持三种类型
Content-Type: application/x-www-form-urlencoded
这个是最简单也是默认的 post 请求体。他的报文如下所示:
POST /test HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
field1=value1&field2=value2
在 aiohttp 中的构造方式就是:
payload = {'key1': 'value1', 'key2': 'value2'}
async with session.post('http://foo.example/test', data=payload) as resp:
print(await resp.text())
Tips
aiohttp 会自动为我们在请求头中添加对应的 Content-Type
Content-Type: multipart/form-data
在一些需要上传文件的地方可能会用到这种形式,他的报文如下所示:
POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575
-----------------------------974767299852498929531610575
Content-Disposition: form-data; name="description"
一些文本
-----------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt"
Content-Type: text/plain
(上传文件 foo.txt 的内容)
-----------------------------974767299852498929531610575--
要构建这类编码可以通过:
url = 'http://httpbin.org/post'
files = {'file': open('foo.txt', 'rb')}
await session.post(url, data=files)
# 也可以显示设置
# 上面实际上是下面的便捷方法
url = 'http://httpbin.org/post'
data = aiohttp.FormData()
data.add_field('file',
open('report.xls', 'rb'),
filename='report.xls',
content_type='application/vnd.ms-excel')
await session.post(url, data=data)
对于比较大的文件也可以使用流式上传:
Tips
同样 aiohttp 会自动为我们在请求头中添加对应的 Content-Type
Content-Type: application/json
这个并不是 form 的官方支持标准,但是 JSON 作为主流的数据共享方式,越来越多的服务器支持这种类型了,甚至大多数请求库都提供了 json 参数来接收他:
超时
默认情况下如果请求超过 300s 没有响应将超时,可以通过 timeout 参数来覆盖这个值。并且由于异步的特殊性,超时需要使用 ClientTimeout
来构造,它包含几个参数:
total
: 整个操作(建立连接、发送请求和读取响应)的最大秒数connect
: 建立连接或等待连接池中的空闲连接的最大秒数sock_connect
: 与服务器建立连接的最大秒数sock_read
: 从服务器读取新数据部分之间允许的最大秒数ceil_threshold
: 触发超时上限的阈值,默认是 5
timeout = aiohttp.ClientTimeout(total=60)
timeout_post = aiohttp.ClientTimeout(total=100)
async with aiohttp.ClientSession(timeout=timeout) as session:
# 使用 session 的
async with session.get(url) as resp:
pass
# 覆盖默认的
async with session.post(url, timeout=timeout_post) as resp:
pass
cookies
重定向
通过 allow_redirects=True
和 max_redirects=10
来设置重定向,默认是开启的最大重定向数是 10 次。
代理
通过 proxy 来指定代理:
async with aiohttp.ClientSession() as session:
async with session.get("http://python.org", proxy="http://proxy.com") as resp:
print(resp.status)
Tips
aiohttps 只支持普通的 HTTP 代理,注意这并不影响之后访问 https 协议网站
解析响应
向服务器发起请求后,服务器会应答并返回响应报文,整个过程是一个黑箱操作,aiohttp 会将响应报文包装成ClientResponse 对象返回供用户使用。
获取响应正文
响应最核心的就是获取响应的正文:
async with session.get('https://api.github.com/events') as resp:
# 最标准的获取方式
# 用于返回以文本形式传输的内容
content = await resp.text(encoding='utf-8')
async with session.get('https://api.github.com/image.png') as resp:
# 如果非文本请求,使用 read() 来返回二进制
# 实际上对于文本内容 resp.read().encode('utf-8') 也可以
img = await resp.read()
async with session.get('https://api.github.com/test.json') as resp:
# 对于响应体中 Content-Type: application/json 的也能够直接通过 json 返回
# 他等价于 json.loads(resp.text())
content = await resp.json()
上面的三个方法都会讲整个数据加载到内存中来返回,这对于非常大的文件来说非常消耗性能,因此 aiohttp 也提供了流式访问的接口:
async with session.get('https://api.github.com/events') as resp:
# resp.content 就是开启数据流
await resp.content.read(10)
# 如果想要流式保存文件,可以使用 iter_chunked 接口:
with open(filename, 'wb') as fd:
async for chunk in resp.content.iter_chunked(chunk_size):
fd.write(chunk)
Tips
注意一旦启用了流即调用了 resp.content
那么 read、json 和 text 方法就无法使用了,即使流中的数据没有消耗完毕
响应状态码
通过 resp.status
和 resp.reason
来获取响应的状态码和对应的状态信息:
async with session.get('https://api.github.com/events') as resp:
# 他们都不是异步的
print(resp.status) # 20
print(resp.reason) # "OK"
还有一个 resp.ok
属性他会在 resp.status < 400
时返回 True 表示请求成功。
重定向历史
如果请求被重定向,整个重定向的调用链被保存在 resp.history: []
中:
resp = await session.get('http://example.com/some/redirect/')
assert resp.status == 200
assert resp.url = URL('http://example.com/some/other/url/')
assert len(resp.history) == 1
assert resp.history[0].status == 301
assert resp.history[0].url = URL('http://example.com/some/redirect/')
Tips
如果没有发生重定向或 allow_redirects = False
那么 resp.history == []