分享

一个不简单的异步爬虫小程序

 书*金 2017-02-28

说到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中。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多