并发编程在爬虫中的应用
在之前的课程中,我们已经为大家介绍了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方式的网络资源获取和文件写入操作,需先安装第三方库aiohttp和aiofile,安装命令如下:
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体验
