aiohttp文档翻译

本文总阅读量

前记

状态弃坑中,官方文档更新了结构要重新编排
**完美弃坑,已有人翻译出来:https://hubertroy.gitbooks.io/aiohttp-chinese-documentation/content/ **

aiohtpp 是用于asyncio和Python的HTTP客户端/服务器。
也就是既能充当用户的请求,也能当做一个服务器。

ps:语法使用python3.5的语法,也就是从

1
2
3
@asyncio.coroutine
def coro(...):
ret = yield from f()

改为

1
2
async def coro(...):
ret = await f()

之前为了学习英语+想查文档然后就简单的翻译下
原网址

入门

aiohttp当做请求时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import aiohttp
import asyncio
import async_timeout

async def fetch(session, url):
with async_timeout.timeout(10):
async with session.get(url) as response:
return await response.text()

async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'http://python.org')
print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

当做服务器时

1
2
3
4
5
6
7
8
9
10
11
12
from aiohttp import web

async def handle(request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)

app = web.Application()
app.router.add_get('/', handle)
app.router.add_get('/{name}', handle)

web.run_app(app)

使用aiohttp进行请求

提出请求

先对 url:https://api.github.com/events 进行请求,然后返回文本

1
2
3
4
5
6
7
8
9
10
11
import aiohttp
import asyncio

async def main():
async with aiohttp.ClientSession() as session:
async with session.get('https://api.github.com/events') as resp:
print(resp.status)
print(await resp.text())

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

可以看出根requests的语法很像,代码中有一个ClientSession被调用session的ClientResponse对象resp。我们可以从响应中获得所需的所有信息,除了get外,也支持其他http协议

1
2
3
4
5
6
session.post('http://httpbin.org/post', data=b'data')
session.put('http://httpbin.org/put', data=b'data')
session.delete('http://httpbin.org/delete')
session.head('http://httpbin.org/get')
session.options('http://httpbin.org/get')
session.patch('http://httpbin.org/patch', data=b'data')

需要注意的是,不必要为每个请求创建一个session,当有多个请求时,把url传入session就可以了,session中包含一个连接池,默认情况下打开连接重用和保持活动

JSON请求

通过以下设置,可以让aiohttp发起一个json请求

1
2
async with aiohttp.ClientSession() as session:
async with session.post(json={'test': 'object'})

默认情况下使用python自带的json序列,如果需要更改可以更改ClientSession里的json_serialize参数

1
2
3
4
import ujson

async with aiohttp.ClientSession(json_serialize=ujson.dumps) as session:
async with session.post(json={'test': 'object'})

在url中传递参数

如果有多次相同请求,只是网址中的键/值不一样时,如httpbin.org/get?key=val,那可以使用params关键字参数,把封装好的dict给aiohttp调用

1
2
3
4
params = {'key1': 'value1', 'key2': 'value2'}
async with session.get('http://httpbin.org/get',
params=params) as resp:
assert str(resp.url) == 'http://httpbin.org/get?key2=value2&key1=value1'

如果是需要一对多形式的键/值可以采用如下形式,为每个键指定多个值

1
2
3
4
params = [('key', 'value1'), ('key', 'value2')]
async with session.get('http://httpbin.org/get',
params=params) as r:
assert str(r.url) == 'http://httpbin.org/get?key=value2&key=value1'

也可以使用str来当做参数

1
2
3
async with session.get('http://httpbin.org/get',
params='key=value+1') as r:
assert str(r.url) == 'http://httpbin.org/get?key=value+1'

aiohttp在发送请求之前在内部执行URL 标准化
例如URL(‘http://example.com/путь%30?a=%31')被转换为 URL(‘http://example.com/%D0%BF%D1%83%D1%82%D1%8C/0?a=1')。
如果服务器不接受时,可以通过设置encoded来改变

1
await session.get(URL('http://example.com/%30', encoded=True))

解析的内容

  • 使用text()
    在aiohttp中会根据网站定义的编码来自动解析网页中的内容,当然,也可以通过设置encoding变量来定义我们要解析的编码格式,如果习惯使用resquests的,要注意这里是text(),待括号的
    1
    await resp.text(encoding='windows-1251')
  • 使用read()
    则解析出来的是二进制的内容,且网页内容过多时,无法全部解析
    1
    await resp.read()
  • 使用json()
    解析后自动转化为json格式
    1
    2
    async with session.get('https://api.github.com/events') as resp:
    print(await resp.json())
  • 流响应内容
    就是像我们在用python打开文件时,如果文件过大,都不会一下子去打开的,可能是一行一行打开,也可能是一段一段打开,总之就是为了避免内存爆满,后面的东西无法写入。
    可以使用这样写入
    1
    2
    async with session.get('https://api.github.com/events') as resp:
    await resp.content.read(10)
    但是最好的方式应该为:
    1
    2
    3
    4
    5
    6
    with open(filename, 'wb') as fd:
    while True:
    chunk = await resp.content.read(chunk_size)
    if not chunk:
    break
    fd.write(chunk)

请求的信息(Headers,Cookies)

aiohttp中单的ClientResposeClientResponse对象包含request_info属性,其中包含Headers和cookies,可以通过一定的设置来向服务器发送不同的东西。

  • 定义Headers
    1
    2
    3
    4
    5
    6
    7
    8
    import json
    url = 'https://api.github.com/some/endpoint'
    payload = {'some': 'data'}
    headers = {'content-type': 'application/json'}

    await session.post(url,
    data=json.dumps(payload),
    headers=headers)
  • 定义Cookies
    1
    2
    3
    4
    5
    6
    url = 'http://httpbin.org/cookies'
    cookies = {'cookies_are': 'working'}
    async with ClientSession(cookies=cookies) as session:
    async with session.get(url) as resp:
    assert await resp.json() == {
    "cookies": {"cookies_are": "working"}}

发送表单请求

在session.post()方法中通过定义data可以设置表单的内容

1
2
3
4
payload = {'key1': 'value1', 'key2': 'value2'}
async with session.post('http://httpbin.org/post',
data=payload) as resp:
print(await resp.text())

发送文件

在发送文件时,也可以通过定义filename和content-type来设置文件名和类型

1
2
3
4
5
6
7
8
url = 'http://httpbin.org/post'
data = FormData()
data.add_field('file',
open('report.xls', 'rb'),
filename='report.xls',
content_type='application/vnd.ms-excel')

await session.post(url, data=data)

发送大文件

如果文件过大,可以通过流的形式进行上传

1
2
with open('massive-body', 'rb') as f:
await session.post('http://httpbin.org/post', data=f)

或者可以使用@aiohttp.streamer装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
@aiohttp.streamer
def file_sender(writer, file_name=None):
with open(file_name, 'rb') as f:
chunk = f.read(2**16)
while chunk:
yield from writer.write(chunk)
chunk = f.read(2**16)

# Then you can use `file_sender` as a data provider:

async with session.post('http://httpbin.org/post',
data=file_sender(file_name='huge_file')) as resp:
print(await resp.text())

也可以使用StreamReader对象进行上传,比如从一个请求下来的内容再上传回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def feed_stream(resp, stream):
h = hashlib.sha256()

while True:
chunk = await resp.content.readany()
if not chunk:
break
h.update(chunk)
stream.feed_data(chunk)

return h.hexdigest()

resp = session.get('http://httpbin.org/post')
stream = StreamReader()
loop.create_task(session.post('http://httpbin.org/post', data=stream))

file_hash = await feed_stream(resp, stream)

上传预压缩的数据

要将传递给aiohttp之前已压缩的数据上传,请使用所使用的压缩算法名称(通常是deflate或zlib)作为Content-Encoding头的值来调用请求函数:

1
2
3
4
5
6
7
async def my_coroutine(session, headers, my_data):
data = zlib.compress(my_data)
headers = {'Content-Encoding': 'deflate'}
async with session.post('http://httpbin.org/post',
data=data,
headers=headers)
pass

连接池,Keep-Alive和 cookie共享

ClientSession 支持保持活动的请求和连接池的开箱即用
ClientSession 可用于在多个请求之间共享cookie

1
2
3
4
5
6
7
8
async with aiohttp.ClientSession() as session:
await session.get(
'http://httpbin.org/cookies/set?my_cookie=my_value')
filtered = session.cookie_jar.filter_cookies('http://httpbin.org')
assert filtered['my_cookie'].value == 'my_value'
async with session.get('http://httpbin.org/cookies') as r:
json_body = await r.json()
assert json_body['cookies']['my_cookie'] == 'my_value'

还可以为所有会话请求设置默认标题

1
2
3
4
5
6
async with aiohttp.ClientSession(
headers={"Authorization": "Basic bG9naW46cGFzcw=="}) as session:
async with session.get("http://httpbin.org/headers") as r:
json_body = await r.json()
assert json_body['headers']['Authorization'] == \
'Basic bG9naW46cGFzcw=='

cookie安全

RFC 2109明确禁止Cookie使用IP地址而不是DNS名称来接受URL(例如 http://127.0.0.1:80/cookie)。
默认情况下ClientSession通过aiohttp.CookieJar提供该功能。
通过将unsafe = True传递给 aiohttp.CookieJar构造函数来完成,启用对这种cookie的支持

1
2
jar = aiohttp.CookieJar(unsafe=True)
session = aiohttp.ClientSession(cookie_jar=jar)

连接器

要调整或更改传输层的请求,您可以将自定义连接器传递 给ClientSession

1
2
conn = aiohttp.TCPConnector()
session = aiohttp.ClientSession(connector=conn)

通过连机器限制连接池大小

默认是100个请求,但可以通过limit来设置连接池大小,如果limit是0则代表无限制

1
conn = aiohttp.TCPConnector(limit=30)

如果要限制同时打开链接到同一个端点的数量,可以将limit_per_host 参数传递给连接器((host, port, is_ssl) triple)

1
conn = aiohttp.TCPConnector(limit_per_host=30)

使用自定义名称服务器解析

1
2
3
4
from aiohttp.resolver import AsyncResolver

resolver = AsyncResolver(nameservers=["8.8.8.8", "8.8.4.4"])
conn = aiohttp.TCPConnector(resolver=resolver)

默认情况下,aiohttp使用严格的HTTPS协议检查。通过将verify_ssl设置为False可以放宽认证检查

1
r = await session.get('https://example.com', verify_ssl=False)

如果需要设置自定义ssl参数(例如使用自己的认证文件),您可以创建一个ssl.SSLContext实例并将其传递给ClientSession方法

1
2
3
sslcontext = ssl.create_default_context(
cafile='/path/to/ca-bundle.crt')
r = await session.get('https://example.com', ssl_context=sslcontext)

如果您需要验证自签名证书,则可以执行与上一个示例相同的操作,但可以使用ssl.SSLContext.load_cert_chain()添加密钥

1
2
3
4
5
sslcontext = ssl.create_default_context(
cafile='/path/to/ca-bundle.crt')
sslcontext.load_cert_chain('/path/to/client/public/device.pem',
'/path/to/client/private/device.jey')
r = await session.get('https://example.com', ssl_context=sslcontext)

ssl验证失败时会有明显的错误

1
2
3
4
5
6
#aiohttp.ClientConnectorSSLError:

try:
await session.get('https://expired.badssl.com/')
except aiohttp.ClientConnectorSSLError as e:
assert isinstance(e, ssl.SSLError)
1
2
3
4
5
6
#aiohttp.ClientConnectorCertificateError:

try:
await session.get('https://wrong.host.badssl.com/')
except aiohttp.ClientConnectorCertificateError as e:
assert isinstance(e, ssl.CertificateError)

如果需要跳过两个SSL相关的错误

1
2
3
4
5
6
7
8
9
10
11
#aiohttp.ClientSSLError:

try:
await session.get('https://expired.badssl.com/')
except aiohttp.ClientSSLError as e:
assert isinstance(e, ssl.SSLError)

try:
await session.get('https://wrong.host.badssl.com/')
except aiohttp.ClientSSLError as e:
assert isinstance(e, ssl.CertificateError)

也可以通过SHA256验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Attempt to connect to https://www.python.org
# with a pin to a bogus certificate:
bad_fingerprint = b'0'*64
exc = None
try:
r = await session.get('https://www.python.org',
fingerprint=bad_fingerprint)
except aiohttp.FingerprintMismatch as e:
exc = e
assert exc is not None
assert exc.expected == bad_fingerprint

# www.python.org cert's actual fingerprint
assert exc.got == b'...'

Unix域名sockets

如果您的HTTP服务器使用UNIX域名sockets,则可以使用 UnixConnector:

1
2
conn = aiohttp.UnixConnector(path='/path/to/socket')
session = aiohttp.ClientSession(connector=conn)

使用代理

原话:aiohttp supports支持http/https 代理

然而使用时如果有https代理,会说只支持http代理- -…

1
2
3
4
async with aiohttp.ClientSession() as session:
async with session.get("http://python.org",
proxy="http://some.proxy.com") as resp:
print(resp.status)

如果代理需要账户密码:

1
2
3
4
5
6
async with aiohttp.ClientSession() as session:
proxy_auth = aiohttp.BasicAuth('user', 'pass')
async with session.get("http://python.org",
proxy="http://some.proxy.com",
proxy_auth=proxy_auth) as resp:
print(resp.status)

不过与requests库相反,它不会默认读取环境变量。但是,您以通过 trust_env=True传入aiohttp.ClientSession 构造函数来从HTTP_PROXY或HTTPS_PROXY 环境变量中提取代理配置 (两者都不区分大小写)

1
2
3
async with aiohttp.ClientSession() as session:
async with session.get("http://python.org", trust_env=True) as resp:
print(resp.status)

响应状态码

可以通过如下查看状态码

1
2
async with session.get('http://httpbin.org/get') as resp:
assert resp.status == 200

请求头

1
2
3
4
5
6
7
>>> resp.headers
{'ACCESS-CONTROL-ALLOW-ORIGIN': '*',
'CONTENT-TYPE': 'application/json',
'DATE': 'Tue, 15 Jul 2014 16:49:51 GMT',
'SERVER': 'gunicorn/18.0',
'CONTENT-LENGTH': '331',
'CONNECTION': 'keep-alive'}

虽然是http文件,但是一份特殊的dict根据RFC 7230,HTTP标头名称是不区分大小写的。它也支持与HTTP协议相同的密钥的多个值。
所以,我们可以使用任何我们想要的大小来访问头文件

1
2
3
4
5
>>> resp.headers['Content-Type']
'application/json'

>>> resp.headers.get('content-type')
'application/json'

都使用UTF-8从二进制数据转换为 surrogateescape选项。这在大多数情况下工作正常,但有时如果服务器使用非标准编码,则需要未转换的数据,可以通过使用ClientResponse.raw_headers属性来检索

1
2
3
4
5
6
>>> resp.raw_headers
((b'SERVER', b'nginx'),
(b'DATE', b'Sat, 09 Jan 2016 20:28:40 GMT'),
(b'CONTENT-TYPE', b'text/html; charset=utf-8'),
(b'CONTENT-LENGTH', b'12150'),
(b'CONNECTION', b'keep-alive'))

获取请求的cookie

1
2
3
url = 'http://example.com/some/cookie/setting/url'
async with session.get(url) as resp:
print(resp.cookies['example_cookie_name'])

注意
响应cookie仅包含重定向链中最后一个请求Set-Cookie中的值。要在所有重定向请求之间收集cookie,请使用aiohttp.ClientSession对象

请求历史

如果请求被重定向,则可以使用history属性查看之前的响应

1
2
3
4
5
>>> resp  =  等待 会话。get ('http://example.com/some/redirect/'
>>> resp
<ClientResponse(http://example.com/some/other/url/)[200]>
>>> resp 。历史
(<ClientResponse(http://example.com/some/redirect/)[301]>)

如果没有发生重定向或allow_redirects设置为False,历史记录将是一个空的序列

WebSockets

在aiohttp中WebSockets是开箱即用的,但是必须使用aiohttp.ClientSession.ws_connect()协程来进行客户端websocket连接。它接受一个url作为第一个参数并返回ClientWebSocketResponse,通过该对象,你可以使用响应的方法与websocket服务器进行通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
session = aiohttp.ClientSession()
async with session.ws_connect('http://example.org/websocket') as ws:

async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
if msg.data == 'close cmd':
await ws.close()
break
else:
await ws.send_str(msg.data + '/answer')
elif msg.type == aiohttp.WSMsgType.CLOSED:
break
elif msg.type == aiohttp.WSMsgType.ERROR:
break

注意:只能使用一个websocket任务进行读写(如await ws.receive() 和 async for msg in ws:)
但是可以使用多个异步读写数据(如:ws.send_str(‘data’)

超时

默认情况下所有IO操作都有5分钟的超时时间 可以通过传入timeout参数到ClientSession.get()来设置超时,当设置None或0时,禁用超时检查

1
async with session.get('https://github.com', timeout=60) as r:

如果是按流的形式写入数据,可以使用async_timeout.timeout(),为连接和响应正文读取过程添加了超时

1
2
3
4
5
import async_timeout

with async_timeout.timeout(0.001):
async with session.get('https://github.com') as r:
await r.text()

注意的是超时是累计时间,包括发送请求,重定向,响应解析,消费响应等所有操作。

优雅的关闭(就是比较好的关闭协程???)

当ClientSession在块的末尾关闭(或通过直接调用)时,由于asyncio内部细节,基础连接保持打开状态在实践中,基础连接将在一段时间后关闭。但是,如果事件循环在底层连接关闭之前停止,则会发出警告(启用警告时):

async with.close()ResourceWarning: unclosed transport

为了避免这种情况,必须在关闭事件循环之前添加一个小的延迟,以允许任何打开的底层连接关闭
对于ClientSession没有使用SSL:

1
2
3
4
5
6
7
8
9
10
async def read_website():
async with aiohttp.ClientSession() as session:
async with session.get('http://example.org/') as response:
await response.read()

loop = asyncio.get_event_loop()
loop.run_until_complete(read_website())
# Zero-sleep to allow underlying connections to close
loop.run_until_complete(asyncio.sleep(0))
loop.close()

对于ClientSession使用SSL,在结束之前必须等待很短的时间:

1
2
3
4
5
6
7
8
9
async def read_website():
async with aiohttp.ClientSession() as session:
async with session.get('http://example.org/') as response:
await response.read()

loop = asyncio.get_event_loop()
loop.run_until_complete(read_website())
loop.run_until_complete(asyncio.sleep(0.250))
loop.close()
查看评论