Python-100-Days/Day61-65/63.并发编程在爬虫中的应用.md

186 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 并发编程在爬虫中的应用
之前的课程,我们已经为大家介绍了 Python 中的多线程、多进程和异步编程,通过这三种手段,我们可以实现并发或并行编程,这一方面可以加速代码的执行,另一方面也可以带来更好的用户体验。爬虫程序是典型的 I/O 密集型任务,对于 I/O 密集型任务来说,多线程和异步 I/O 都是很好的选择,因为当程序的某个部分因 I/O 操作阻塞时,程序的其他部分仍然可以运转,这样我们不用在等待和阻塞中浪费大量的时间。下面我们以爬取“[360图片](https://image.so.com/)”网站的图片并保存到本地为例,为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别,同时也对它们的执行效率进行简单的对比。
“360图片”网站的页面使用了 [Ajax](https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX) 技术,这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说,页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的,而且整个页面还使用了瀑布式加载(一边向下滚动,一边加载更多的图片)。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口,如下图所示,我们需要的图片信息就在服务器返回的 JSON 数据中。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211205221352.png" style="zoom:50%;">
例如要获取“美女”频道的图片我们可以请求如下所示的URL其中参数`ch`表示请求的频道,`=`后面的参数值`beauty`就代表了“美女”频道,参数`sn`相当于是页码,`0`表示第一页(共`30`张图片),`30`表示第二页,`60`表示第三页,以此类推。
```
https://image.so.com/zjl?ch=beauty&sn=0
```
### 单线程版本
通过上面的 URL 下载“美女”频道共`90`张图片。
```Python
"""
example04.py - 单线程版本爬虫
"""
import os
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
download_picture(pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
```
在 macOS 或 Linux 系统上,我们可以使用`time`命令来了解上面代码的执行时间以及 CPU 的利用率,如下所示。
```Bash
time python3 example04.py
```
下面是单线程爬虫代码在我的电脑上执行的结果。
```
python3 example04.py 2.36s user 0.39s system 12% cpu 21.578 total
```
这里我们只需要关注代码的总耗时为`21.578`秒CPU 利用率为`12%`。
### 多线程版本
我们使用之前讲到过的线程池技术,将上面的代码修改为多线程版本。
```Python
"""
example05.py - 多线程版本爬虫
"""
import os
from concurrent.futures import ThreadPoolExecutor
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
with ThreadPoolExecutor(max_workers=16) as pool:
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
pool.submit(download_picture, pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
```
执行如下所示的命令。
```Bash
time python3 example05.py
```
代码的执行结果如下所示:
```
python3 example05.py 2.65s user 0.40s system 95% cpu 3.193 total
```
### 异步I/O版本
我们使用`aiohttp`将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库`aiohttp`和`aiofile`,命令如下所示。
```Bash
pip install aiohttp aiofile
```
`aiohttp` 的用法在之前的课程中已经做过简要介绍,`aiofile`模块中的`async_open`函数跟 Python 内置函数`open`的用法大致相同,只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。
```Python
"""
example06.py - 异步I/O版本爬虫
"""
import asyncio
import json
import os
import aiofile
import aiohttp
async def download_picture(session, url):
filename = url[url.rfind('/') + 1:]
async with session.get(url, ssl=False) as resp:
if resp.status == 200:
data = await resp.read()
async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file:
await file.write(data)
async def fetch_json():
async with aiohttp.ClientSession() as session:
for page in range(3):
async with session.get(
url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}',
ssl=False
) as resp:
if resp.status == 200:
json_str = await resp.text()
result = json.loads(json_str)
for pic_dict in result['list']:
await download_picture(session, pic_dict['qhimg_url'])
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
loop = asyncio.get_event_loop()
loop.run_until_complete(fetch_json())
loop.close()
if __name__ == '__main__':
main()
```
执行如下所示的命令。
```Bash
time python3 example06.py
```
代码的执行结果如下所示:
```
python3 example06.py 0.82s user 0.21s system 27% cpu 3.782 total
```
### 总结
通过上面三段代码执行结果的比较,我们可以得出一个结论,使用多线程和异步 I/O 都可以改善爬虫程序的性能,因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上,而`time`命令的执行结果也告诉我们,单线程的代码 CPU 利用率仅仅只有`12%`,而多线程版本的 CPU 利用率则高达`95%`;单线程版本的爬虫执行时间约`21`秒,而多线程和异步 I/O 的版本仅执行了`3`秒钟。另外,在运行时间差别不大的情况下,多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源,这是因为多线程的调度和切换也需要花费 CPU 时间。至此,三种方式在 I/O 密集型任务上的优劣已经一目了然,当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢,那么使用多线程和异步 I/O 的优势将更为明显,有兴趣的读者可以自行试验。