说到Python,爬虫可能是大家第一个想到的。 然而,Python在爬虫方面已经产生了巨大的变化。虽然本次的小程序并不简单,不过你们可以尝试着了解一下,在Python3中如何书写一个爬虫。 相关库本次,我们使用的库并不是`requests`库,而是仅仅支持`Python3.4.2+`的`aiohttp`。 首先我们需要搭建相关的环境: 安装`aiohttp` ```shell $ sudo pip3 install aiohttp ``` 安装`cchardet` ```shell $sudo pip3 install cchardet ``` 安装`aiodns` ```shell $ sudo pip3 install aiodns ``` 以上库基本上都没有依赖问题,可以直接安装。如果出现安装问题,可以加[QQ群]()来咨询更有经验的开发者 ## 目标 例如:`URL — http:///data/flags/cn/cn.gif` 的内容是一面中国国旗(希望大家做一个爱国的好公民) ![img](http:///data/flags/cn/cn.gif) 当然,国旗不只这一面,我们我们只要吧`url`中的`cn/cn.gif`换成别的国家,就可以拿到别的国家的旗子。 例如:`URL — http:///data/flags/us/us.gif` 的内容是一面美国国旗(us是美国的国家代码) ![img](http:///data/flags/us/us.gif) 因此,我们今天的目标就是: `CN IN US ID BR PK NG BD RU JP MX PH VN ET EG DE IR TR CD FR` 这20个旗子 构造URL代码为了构造这些目标,我们必须要先获得URL,因此我们通过`base_url`和`contry_codes`的形式分别存储URL共同的部分与不同的部分。 ```python BASE_URL = 'http:///data/flags' CONTRY_CODES = ('CN IN US ID BR PK NG BD RU JP ' 'MX PH VN ET EG DE IR TR CD FR').split() DEST_DIR = 'DOWNLOADS/' ``` 程序设计主流程: 主流程一半是出现再`__name__`测试中的入口函数,使用它的目的一半是提供一个统一的接口,并且提供一个清晰的逻辑,以方便我们添加一些额外的信息。 ```python import time def main(runable, *args, **kwargs): t0 = time.time() count = runable(*args, **kwargs) elapsed = time.time() - t0 msg = '\n{} flags downloaded in {:.2f}s' print(msg.format(count, elapsed)) ``` 该主流程,只有两个功能 1. 用来调用本程序真正的代码 2. 用来对整个代码的执行计时 实际流程: 在实际的流程中,我们需要开启一个事件循环,来处理我们的任务。 因此我们需要得到一个事件循环`loop`。来执行我们非阻塞的`coroutine`。当然,时间循环一单用完,需要关闭。 ```python import asyncio def download_many(cc_list): loop = asyncio.get_event_loop() coroutines = [download_one(cc) for cc in sorted(cc_list)] wait_coroutines = asyncio.wait(coroutines) res, _ = loop.run_until_complete(wait_coroutines) loop.close() return len(res) ``` 在这个流程中,`wait`可以通过许多的`coroutine`来构造一个非阻塞的`coroutine`。而且最后这个`coroutine`是在所有`coroutine`完成时才完成。 其实这个流程也只完成了两件事: * 构造了循环 * 构造需要被时间循环执行的`coroutine`,并提交给循环 处理单个爬取的coroutine记住,`download_many`的依然是一个普通的函数,但是它的作用是操作时间循环去调度`coroutine`,因此我们需要开始写我们的`coroutine`。 由上一部分可知,对应没一个旗子的`download_one`就是一个`coroutine` ```python import sys import os async def download_one(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) async with aiohttp.ClientSession() as session: await fetch_flag(session, url, cc+'.gif') return cc async def fetch_flag(session, url, filename): path = os.path.join(DEST_DIR, filename) with async_timeout.timeout(10): async with session.get(url) as response: with open(path, 'wb') as fp: while True: chunk = await response.content.read(512) if not chunk: break fp.write(chunk) ``` 此处分块处理的原因是因为,不确定图片的大小,可以保证不要一次性占用太多的内存空间。 从上面看出,所有的`coroutine`都是异步的,如果读者自己尝试一下。就会发现,与普通的排队式爬虫相比,速度提高了近16倍,甚至更多。 与使用多线程爬虫的效果相当,但是占用更少的资源,可以腾出更多的内存空间。 待改进其实,笔者我在此处,埋了一个伏笔,本程序还有待改进,它现在唯一不完美的地方是,对`pool`的重复利用不好,最好把session放入download_many中。 |
|