Python 3 爬虫|第9章:使用 asyncio + aiohttp 并发下载
Synopsis: 支持 HTTP 协议的异步网络 I/O 库:aiohttp,我们的爬虫需要使用该库的 Client 功能。需要结合 asyncio 一起使用,经过测试,单线程的异步编程比多线程版本的性能还要好一些,毕竟没有创建线程的开销和线程间上下文切换。另外,如果你下载的是视频等大文件,此时,将网络数据保存到本地磁盘的这个过程,本身是阻塞的,所以它会阻塞事件循环。asyncio 不支持磁盘 I/O 异步,需要使用aiofiles,其实它背后也只是创建一个线程池而已。后续爬虫实战时,动辄下载数十万个文件,为保持控制台清爽,需要使用 progressbar2 显示进度条,而详细信息将保存到日志文件中
代码已上传到 https://github.com/wangy8961/python3-concurrency ,欢迎 star
1. aiohttp
Asynchronous HTTP Client/Server for asyncio and Python
上一篇博客介绍的 asyncio
提供了基于 socket
的异步 I/O,支持 TCP
和 UDP
协议,但是不支持应用层协议 HTTP
,而 aiohttp
就是为此而生
1.1 安装
1.2 基本使用
import aiohttp import asyncio async def fetch(session, url): 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://www.madmalls.com') print(html) loop = asyncio.get_event_loop() loop.run_until_complete(main())
2. asyncio + aiohttp
aiohttp Client Quickstart 文档要求 HTTP 客户端需要首先创建 aiohttp.ClientSession()
异步上下文管理器,并且 不能为每个请求单独创建 session
async with aiohttp.ClientSession() as session
: 会自动管理 session 的创建与销毁async with session.get('http://httpbin.org/') as response
: 会自动管理 HTTP 连接的建立与关闭await response.read()
: 异步获取响应数据,read()
返回二进制数据,text()
返回字符串形式的 HTML 文档
# 请使用Python 3.7 import os import time import asyncio import aiohttp from logger import logger basepath = os.path.abspath(os.path.dirname(__file__)) # 当前模块文件的根目录 def setup_down_path(): '''设置图片下载后的保存位置,所有图片放在同一个目录下''' down_path = os.path.join(basepath, 'downloads') if not os.path.isdir(down_path): os.mkdir(down_path) logger.info('Create download path {}'.format(down_path)) return down_path def get_links(): '''获取所有图片的下载链接''' with open(os.path.join(basepath, 'flags.txt')) as f: # 图片名都保存在这个文件中,每行一个图片名 return ['http://192.168.40.121/flags/' + flag.strip() for flag in f.readlines()] async def download_one(session, image): logger.info('Downloading No.{} [{}]'.format(image['linkno'], image['link'])) t0 = time.time() async with session.get(image['link']) as response: image_content = await response.read() # Binary Response Content: access the response body as bytes, for non-text requests filename = os.path.split(image['link'])[1] with open(os.path.join(image['path'], filename), 'wb') as f: f.write(image_content) t1 = time.time() logger.info('Task No.{} [{}] runs {:.2f} seconds.'.format(image['linkno'], image['link'], t1 - t0)) async def download_many(): down_path = setup_down_path() links = get_links() tasks = [] # 保存所有任务的列表 async with aiohttp.ClientSession() as session: # aiohttp建议整个应用只创建一个session,不能为每个请求创建一个seesion for linkno, link in enumerate(links, 1): image = { 'path': down_path, 'linkno': linkno, # 图片序号,方便日志输出时,正在下载哪一张 'link': link } task = asyncio.create_task(download_one(session, image)) # asyncio.create_task()是Python 3.7新加的,否则使用asyncio.ensure_future() tasks.append(task) results = await asyncio.gather(*tasks) return len(results) if __name__ == '__main__': t0 = time.time() count = asyncio.run(download_many()) msg = '{} flags downloaded in {:.2f} seconds.' logger.info(msg.format(count, time.time() - t0))
测试结果平均为 0.89
秒,比 多线程
版本还要快!
3. 使用 uvloop
http://www.madmalls.com/blog/post/asyncio-howto-in-python3/#23 说明了 uvloop
替换 asyncio
中的默认事件循环策略后,性能还能进一步提升:
3.1 安装
3.2 使用 uvloop.EventLoopPolicy()
增加两行:
4. aiofiles
章节 2 中的示例,f.write(image_content)
是本地磁盘 I/O,它是阻塞型操作,当获取网络图片数据后,保存到磁盘的这个过程会短暂的阻塞整个线程。如果你获取的是多个高清视频文件,这个过程就会阻塞很长时间了,而 asyncio
只支持网络异步 I/O,要实现这个需求,我们需要用到 aiofiles
安装:
0 条评论
评论者的用户名
评论时间暂时还没有评论.