Python从入门到精通day62

并发编程在爬虫中的应用

在之前的课程中,我们已经为大家介绍了Python中的多线程、多进程和异步编程技术。通过这三种方式,我们可以实现并发或并行编程,既能显著加速代码执行效率,也能提升用户体验。爬虫程序是典型的I/O密集型任务,对于这类任务而言,多线程和异步I/O都是非常合适的解决方案------当程序某一部分因I/O操作阻塞时,其他部分仍可正常运行,避免在等待和阻塞中浪费大量时间。下面我们以爬取"360图片"网站的图片并保存到本地为例,分别展示单线程、多线程和异步I/O三种编程方式的爬虫差异,并对它们的执行效率进行简单对比。

"360图片"网站采用了Ajax技术,这是当前众多网站普遍使用的异步加载数据、局部刷新页面的技术。简单来说,页面上的所有图片,都是通过JavaScript代码异步获取JSON数据后动态渲染生成的,且页面采用瀑布式加载模式(向下滚动页面即可加载更多图片)。我们可以在浏览器的"开发者工具"中,找到提供动态内容的数据接口,如下图所示,我们所需的图片信息,就包含在服务器返回的JSON数据中。

例如,要获取"美女"频道的图片,可请求如下URL,其中参数ch用于指定请求频道,其后的beauty即代表"美女"频道;参数sn相当于页码,0对应第一页(含30张图片),30对应第二页,60对应第三页,以此类推。

复制代码
https://image.so.com/zjl?ch=beauty&sn=0

单线程版本

通过上述URL,下载"美女"频道共90张图片,单线程实现代码如下:

复制代码
"""
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利用率,命令如下:

复制代码
time python3 example04.py 

以下是单线程爬虫代码在本机上的执行结果:

复制代码
python3 example04.py  2.36s user 0.39s system 12% cpu 21.578 total

此处我们重点关注两个核心数据:代码总耗时为21.578秒,CPU利用率仅为12%。

多线程版本

我们利用之前讲解的线程池技术,将单线程代码修改为多线程版本,具体实现如下:

复制代码
"""
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()

执行如下命令,查看多线程版本的执行情况:

复制代码
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方式的网络资源获取和文件写入操作,需先安装第三方库aiohttpaiofile,安装命令如下:

复制代码
pip install aiohttp aiofile

其中,aiohttp的用法我们在之前的课程中已简要介绍;aiofile模块中的async_open函数,与Python内置的open函数用法大致一致,核心区别是它支持异步操作。以下是异步I/O版本的爬虫代码:

复制代码
"""
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()

执行如下命令,查看异步I/O版本的执行情况:

复制代码
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的优势会更加突出,有兴趣的读者可自行尝试验证。

AI工具

新用户🉑体验3天,体验最新最强GPT 5.4 Thinking,关注并私信,备注ai体验

相关推荐
絆人心2 小时前
Python 数据分析核心库:Pandas 与 NumPy 从入门到实战全指南(附电商用户分析完整代码)
python·数据挖掘·数据分析·numpy·pandas·数据处理·电商数据分析
游乐码2 小时前
c#stack
开发语言·c#
Dxy12393102162 小时前
Python如何对XML进行格式化
xml·python
兰.lan2 小时前
【黑马ai测试】黑马头条登录功能测试-发布功能测试-其他功能模块设计
软件测试·人工智能·笔记·python·功能测试·ai·单元测试
想吃砸到牛顿的苹果的籽2 小时前
rk3588部署yolov5需要用rknn_toolkit2,安装环境。
python·深度学习·yolo部署rk3588
cch89182 小时前
Laravel vs 主流PHP框架:终极对决
开发语言·php·laravel
我能坚持多久2 小时前
C++类与对象(中)
开发语言·c++
我姓徐2 小时前
TensorFlow 模型训练和简单部署示例
python·tensorflow
江奖蒋犟2 小时前
【C++】map和set
开发语言·数据结构·c++·set·map