Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

使用Python进行并发编程-asyncio篇(三)

$
0
0

这是「使用python进行并发编程」系列的最后一篇。我特意地把它安排在了16年最后一天,先祝各位元旦快乐。

重新实验上篇的效率对比的实现

在第一篇我们曾经对比并发执行的效率,但是请求的是httpbin.org这个网站。很容易受到网络状态和其服务质量的影响。所以我考虑启用一个本地的eb服务。那接下来选方案吧。

我用sanic提供的 不同方案的例子 ,对tornado、aiohttp+ujson+uvloop、sanic+uvloop三种方案,在最新的Python 3.6下,使用 wrk 进行了性能测试。

先解释下上面提到的几个关键词:

aiohttp。一个实现了PEP3156的HTTP的服务器,且包含客户端相关功能。最早出现,应该最知名。 sanic。后起之秀,基于Flask语法的异步Web框架。 uvloop。用Cython编写的、用来替代asyncio事件循环。作者说「它在速度上至少比Node.js、gevent以及其它任何Python异步框架快2倍」。 ujson。比标准库json及其社区版的simplejson都要快的JSON编解码库。

使用的测试命令是:

wrk -d20s -t10 -c200 http://127.0.0.1:8000

表示使用10个线程、并发200、持续20秒。

在我个人Mac上获得的结果是:

方案 tornado aiohttp + ujson + uvloop sanic + uvloop 平均延时 122.58ms 35.49ms 11.03ms 请求数/秒 162.94 566.87 2.02k

所以简单的返回json数据,看起来sanic + uvloop是最快的。首先我对市面的各种Benchmark的对比是非常反感的,不能用hello world这种级别的例子的结果就片面的认为某种方案效率是最好的, 一定 要根据你实际的生产环境,再不行影响线上服务的前提下,对一部分有代表性的接口进程流量镜像之类的方式去进行效率的对比。而我认可上述的结果是因为正好满足我接下来测试用到的功能而已。

写一个能GET某参数返回这个参数的sanic+uvloop的版本的例子:

from sanic import Sanic from sanic.response import json app = Sanic(__name__) @app.route('/get') async deftest(request): a = request.args.get('a') return json({'args': {'a': a}}) if __name__ == '__main__': app.run(host='127.0.0.1', port=8000)

然后把之前的效率对比的代码改造一下,需要变化如下几步:

替换请求地址,也就是把httpbin.org改成了localhost:8000 增加要爬取的页面数量,由于sanic太快了(无奈脸),12个页面秒完,所以改成 NUMBERS = range(240) 由于页面数量大幅增加,不能在终端都打印出来。而且之前已经验证过正确性。去掉那些print

看下效果:

python3 scraper_thread.py Use requests+ThreadPoolExecutor cost: 0.9809930324554443 Use asyncio+requests+ThreadPoolExecutor cost: 0.9977471828460693 Use asyncio+aiohttp cost: 0.25928187370300293 Use asyncio+aiohttp+ThreadPoolExecutor cost: 0.278397798538208

可以感受到asyncio+aiohttp依然是最快的。随便挺一下Sanic,准备有机会在实际工作中用一下。

asyncio在背后怎么运行的呢?

在 Asynchronous Python 这篇文章里面我找到一个表达的不错的asyncio运行的序列图。例子我改编如下:

import asyncio async defcompute(x, y): print('Compute {} + {} ...'.format(x, y)) await asyncio.sleep(1.0) return x + y async defprint_sum(x, y): result = await compute(x, y) print('{} + {} = {}'.format(x, y, result)) loop = asyncio.get_event_loop() loop.run_until_complete(print_sum(1, 2)) loop.close()

运行的过程是这样的:


使用Python进行并发编程-asyncio篇(三)
如何把同步的代码改成异步的

之前有位订阅我的公众号的同学问过这个问题,我想了一个例子来让事情变的清楚。

首先看一个同步的例子:

defhandle(id): subject = get_subject_from_db(id) buyinfo = get_buyinfo(id) change = process(subject, buyinfo) notify_change(change) flush_cache(id)

可以看到,需要获取subject和buyinfo之后才能执行process,然后才能执行notify_change和flush_cache。

如果使用asyncio,就是这样写:

import asyncio async defhandle(id): subject = asyncio.ensure_future(get_subject_from_db(id)) buyinfo = asyncio.ensure_future(get_buyinfo(id)) results = await asyncio.gather(subject, buyinfo) change = await process(results) await notify_change(change) loop.call_soon(flush_cache, id)

原则上无非是让能一起协同的函数异步化(subject和buyinfo已经是Future对象了),然后通过gather获取到这些函数执行的结果;有顺序的就用call_soon来保证。

继续深入,现在详细了解下一步还有什么其他解决方案以及其应用场景:

包装成Future对象。上面使用了ensure_future来做,上篇也说过,也可以用loop.create_task。如果你看的是老文章可能会出现asyncio.async这种用法,它现在已经被弃用了。如果你已经非常熟悉,你也可以直接使用asyncio.Task(get_subject_from_db(id))这样的方式。

回调。上面用到了call_soon这种回调。除此之外还有如下两种:

loop.call_later(delay, func, *args)。延迟delay秒之后再执行。 loop.call_at(when, func, *args)。 某个时刻才执行。

其实套路就是这些罢了。

爬虫分析

可能你已经听过 开源程序架构 系列书了。今天我们将介绍第四本 500 Lines or Less 中的 爬虫项目 。顺便说一下,这个项目里面每章都是由不同领域非常知名的专家而写,代码不超过500行。目前包含web服务器、决策采样器、Python解释器、爬虫、模板引擎、OCR持续集成系统、分布式系统、静态检查等内容。值得大家好好学习下。

我们看的这个例子,是实现一个高性能网络爬虫,它能够抓取你指定的网站的全部地址。它是由MongoDB的C和Python驱动的主要开发者 ajdavis 以及Python之父Guido van Rossum一起完成的。BTW, 我是ajdavis粉儿!

如果你想看了解这篇爬虫教程可以访问: A Web Crawler With asyncio Coroutines ,这篇和教程关系不大,是一篇分析文章。

我们首先下载并安装对应的依赖:

git clone https://github.com/aosabook/500lines cd 500lines python3 -m pip install -r requirements.txt

运行一下,看看效果:

python3 crawler/code/crawl.py -q python-cn.org --exclude github ... http://python-cn.org:80/user/zuoshou/topics 200 text/html utf-8 13212 0/22 http://python-cn.org:80/users 200 text/html utf-8 34156 24/41 http://python-cn.org:80/users/online 200 text/html utf-8 11614 0/17 http://python-cn.org:80/users/sort-posts 200 text/html utf-8 34642 0/41 http://python-cn.org:80/users/sort-reputation 200 text/html utf-8 34721 15/41 Finished 2365 urls in 47.868 secs (max_tasks=100) (0.494 urls/sec/task) 4 error 36 error_bytes 2068 html 42735445 html_bytes 98 other 937394 other_bytes 195 redirect 4 status_404 Todo: 0 Done: 2365 Date: Fri Dec 30 22:03:50 2016 local time

可以看到 http://python-cn.org 有2365个页面,花费了47.868秒,并发为100。

这个项目有如下一些文件:

tree crawler/code -L 1 crawler/code ├── Makefile ├── crawl.py ├── crawling.py ├── reporting.py ├── requirements.txt ├── supplemental └── test.py

其中主要有如下三个程序:

crawl.py是主程序,其中包含了参数解析,以及事件循环。 crawling.py抓取程序,crawl.py中的异步函数就是其中的Crawler类的crawl方法。 reporting.py顾名思义,生成抓取结果的程序。

本文主要看crawling.py部分。虽然它已经很小(加上空行才275行),但是为了让爬虫的核心更直观,我把其中的兼容性、日志功能以及异常的处理去掉,并将处理成Python 3.5新的async/await语法。

首先列一下这个爬虫实现什么功能:

输入一个根链接,让爬虫自动帮助我们爬完所有能找到的链接 把全部的抓取结果存到一个列表中 可以排除包含某些关键词链接的抓取 可以控制并发数 可以抓取自动重定向的页面,且可以限制重定向的次数 抓取失败可重试

目前对一个复杂的结果结构常定义一个namedtuple,首先把抓取的结果定义成一个FetchStatistic:

FetchStatistic = namedtuple('FetchStatistic', ['url', 'next_url', 'status', 'exception', 'size', 'content_type', 'encoding', 'num_urls', 'num_new_urls'])

其中包含了url,文件类型,状态码等用得到的信息。

然后实现抓取类Crawler,首先是初始化方法:

classCrawler: def__init__(self, roots, exclude=None, strict=True, # What to crawl. max_redirect=10, max_tries=4, # Per-url limits. max_tasks=10, *, loop=None): self.loop = loop or asyncio.get_event_loop() self.roots = roots self.exclude = exclude self.strict = strict self.max_redirect = max_red

Viewing all articles
Browse latest Browse all 9596

Trending Articles