说到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://flupy.org/data/flags/cn/cn.gif`
的内容是一面中国国旗(希望大家做一个爱国的好公民)
![img](http://flupy.org/data/flags/cn/cn.gif)
当然,国旗不只这一面,我们我们只要吧`url`中的`cn/cn.gif`换成别的国家,就可以拿到别的国家的旗子。
例如:`URL — http://flupy.org/data/flags/us/us.gif`
的内容是一面美国国旗(us是美国的国家代码)
![img](http://flupy.org/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,因此我们通过`base_url`和`contry_codes`的形式分别存储URL共同的部分与不同的部分。
```python
BASE_URL = 'http://flupy.org/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`,并提交给循环
记住,`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中。
联系客服