使用Multipart

aiohttp支持功能完备的Multipart读取器和写入器。这俩都使用流式设计,以避免不必要的占用,尤其是处理的载体较大时,但这也意味着大多数I/O操作只能被执行一次。

读取Multipart响应

假设你发起了一次请求,然后想读取Multipart响应数据:

  1. async with aiohttp.request(...) as resp:
  2. pass

首先,你需要使用MultipartReader.from_response()来处理下响应内容。这样可以让数据从响应和连接中分离出来并保持MultipartReader状态,使其更便捷的使用:

  1. reader = aiohttp.MultipartReader.from_response(resp)

假设我们需要接受JSON数据和Multipart文件,但并不需要所有数据,只是其中的一个。

那我们首先需要进入一段循环中,在里面处理Multipart

  1. metadata = None
  2. filedata = None
  3. while True:
  4. part = await reader.next()

所返回的类型取决于下一次循环时的值: 如果是一个正常响应内容那会得到BodyPartReader实例对象,否则将会是一个嵌套MultipartMultipartReader实例对象。记住,Multipart的格式就是递归并且支持嵌套多层。如果接下来没有内容可以获取了,则返回None - 然后就可以跳出这个循环了:

  1. if part is None:
  2. break

BodyPartReaderMultipartReader都可访问内容的headers: 这样就可以使用他们的属性来进行过滤:

  1. if part.headers[aiohttp.hdrs.CONTENT_TYPE] == 'application/json':
  2. metadata = await part.json()
  3. continue

不明确说明的话,不管是BodyPartReader还是MultipartReader都不会读取出全部的内容。BodyPartReader提供一些易用的方法来帮助获取比较常见的内容类型:

  • BodyPartReader.text() 普通文本内容。
  • BodyPartReader.json() JSON内容。
  • BodyPartReader.form() application/www-urlform-encode内容。
    如果传输内容使用了gzipdeflate进行过编码则会自动识别,或者如果是base64quoted-printable这种情况也会自动解码。不过如果你需要读取原始数据,使用BodyPartReader.read()BodyPartReader.read_chunk()协程方法都可以读取原始数据,只不过一个是一次性读取全部一个是分块读取。
    BodyPartReader.filename属性对于处理Multipart文件时可能会有些用处:
    1. if part.filename != 'secret.txt':
    2. continue
    当前的内容不符合你的期待然后要跳过的话只需要使用continue来继续这个循环。在获取下一个内容之前使用await reader.next()确保之前那个已经完全被读取出来了。如果没有的话,所有的内容将会被抛弃然后来获取下一个内容。所以你不用关心如何清理这些无用的数据。
    一旦发现你搜寻的那个文件,直接读就行。我们可以先不使用解码读取:
    1. filedata = await part.read(decode=False)
    之后如果要解码的话也很简单:
    1. filedata = part.decode(filedata)
    一旦完成了关于Multipart的处理,只需要跳出循环就好了:
    1. break

发送Multipart请求

MultipartWriter提供将Python数据转换到Multipart载体(以二进制流的形式)的接口。因为Multipart格式是递归的而且支持深层嵌套,所以你可以使用with语句设计Multipart数据的关闭流程:

  1. with aiohttp.MultipartWriter('mixed') as mpwriter:
  2. ...
  3. with aiohttp.MultipartWriter('related') as subwriter:
  4. ...
  5. mpwriter.append(subwriter)
  6. with aiohttp.MultipartWriter('related') as subwriter:
  7. ...
  8. with aiohttp.MultipartWriter('related') as subsubwriter:
  9. ...
  10. subwriter.append(subsubwriter)
  11. mpwriter.append(subwriter)
  12. with aiohttp.MultipartWriter('related') as subwriter:
  13. ...
  14. mpwriter.append(subwriter)

MultipartWriter.append()用于将新的内容压入同一个流中。它可以接受各种输入,并且决定给这些输入用什么headers
对于文本数据默认的Content-Typetext/plain; charset=utf-8:

  1. mpwriter.append('hello')

二进制则是application/octet-stream:

  1. mpwriter.append(b'aiohttp')

你也可以使用第二参数来覆盖默认值:

  1. mpwriter.append(io.BytesIO(b'GIF89a...'),
  2. {'CONTENT-TYPE': 'image/gif'})

对于文件对象Content-Type会使用Python的mimetypes模块来做判断,此外,Content-Disposition头会把文件的基本名包含进去。

  1. part = root.append(open(__file__, 'rb'))

如果你想给文件设置个其他的名字,只需要操作BodyPartWriter实例即可,使用BodyPartWriter.set_content_disposiition()MultipartWriter.append()方法总会显式的返回和设置Content-Disposition:

  1. part.set_content_disposition('attachment', filename='secret.txt')

此外,你还可以设置些其他的头信息:

  1. part.headers[aiohttp.hdrs.CONTENT_ID] = 'X-12345'

如果你设置了Content-Encoding,后续的数据都会自动编码:

  1. part.headers[aiohttp.hdrs.CONTENT_ENCODING] = 'gzip'

常用的方法还有MultipartWriter.append_json()MultipartWriter.append_form()对JSON和表单数据非常好用,这样你就不需要每次都手动编码成需要的格式:

  1. mpwriter.append_json({'test': 'passed'})
  2. mpwriter.append_form([('key', 'value')])

最后,只需要将根MultipartWriter实例通过aiohttp.client.request()data参数传递出去即可:

  1. await aiohttp.post('http://example.com', data=mpwriter)

后台的MultipartWriter.serialize()对每个部分都生成一个块,如果拥有Content-Encoding或者Content-Transfer-Encoding头信息会被自动应用到流数据上。

注意,在被MultipartWriter.serialize()处理时,所有的文件对象都会被读至末尾,不将文件指针重置到开始时是不能重复读取的。

Multipart使用技巧

互联网上充满陷阱,有时你可能会发现一个支持Multipart的服务器出现些奇怪的情况。
比如,如果服务器使用了cgi.FieldStorage,你就必须确认是否包含Content-Length头信息:

  1. for part in mpwriter:
  2. part.headers.pop(aiohttp.hdrs.CONTENT_LENGTH, None)

另一方面,有些服务器可能需要你为所有的Multipart请求指定Content-Length头信息。但aiohttp并不会指定因为默认是用块传输来发送Multipart的。要实现的话你必须连接MultipartWriter来计算大小:

  1. body = b''.join(mpwriter.serialize())
  2. await aiohttp.post('http://example.com',
  3. data=body, headers=mpwriter.headers)

有时服务器的响应并没有一个很好的格式: 可能不包含嵌套部分。比如,我们请求的资源返回JSON和文件的混合体。如果响应中有任何附加信息,他们应该使用嵌套Multipart的形式。如果没有则是普通形式:

  1. CONTENT-TYPE: multipart/mixed; boundary=--:
  2. --:
  3. CONTENT-TYPE: application/json
  4. {"_id": "foo"}
  5. --:
  6. CONTENT-TYPE: multipart/related; boundary=----:
  7. ----:
  8. CONTENT-TYPE: application/json
  9. {"_id": "bar"}
  10. ----:
  11. CONTENT-TYPE: text/plain
  12. CONTENT-DISPOSITION: attachment; filename=bar.txt
  13. bar! bar! bar!
  14. ----:--
  15. --:
  16. CONTENT-TYPE: application/json
  17. {"_id": "boo"}
  18. --:
  19. CONTENT-TYPE: multipart/related; boundary=----:
  20. ----:
  21. CONTENT-TYPE: application/json
  22. {"_id": "baz"}
  23. ----:
  24. CONTENT-TYPE: text/plain
  25. CONTENT-DISPOSITION: attachment; filename=baz.txt
  26. baz! baz! baz!
  27. ----:--
  28. --:--

在单个流内读取这样的数据是可以的,不过并不清晰:

  1. result = []
  2. while True:
  3. part = await reader.next()
  4. if part is None:
  5. break
  6. if isinstance(part, aiohttp.MultipartReader):
  7. # Fetching files
  8. while True:
  9. filepart = await part.next()
  10. if filepart is None:
  11. break
  12. result[-1].append((await filepart.read()))
  13. else:
  14. # Fetching document
  15. result.append([(await part.json())])

我们换一种方式来处理,让普通文档和与文件相关的读取器成对附到每个迭代器上:

  1. class PairsMultipartReader(aiohttp.MultipartReader):
  2. # keep reference on the original reader
  3. multipart_reader_cls = aiohttp.MultipartReader
  4. async def next(self):
  5. """Emits a tuple of document object (:class:`dict`) and multipart
  6. reader of the followed attachments (if any).
  7. :rtype: tuple
  8. """
  9. reader = await super().next()
  10. if self._at_eof:
  11. return None, None
  12. if isinstance(reader, self.multipart_reader_cls):
  13. part = await reader.next()
  14. doc = await part.json()
  15. else:
  16. doc = await reader.json()
  17. return doc, reader

这样我们就可以更轻快的解决:

  1. reader = PairsMultipartReader.from_response(resp)
  2. result = []
  3. while True:
  4. doc, files_reader = await reader.next()
  5. if doc is None:
  6. break
  7. files = []
  8. while True:
  9. filepart = await files_reader.next()
  10. if file.part is None:
  11. break
  12. files.append((await filepart.read()))
  13. result.append((doc, files))

扩展

Multipart API in Helpers API section.