一、场景与挑战
在网络爬虫开发中,我们常常面临以下挑战:
-
需要处理成百上千个页面的数据抓取
-
目标服务器存在反爬机制和请求频率限制
-
单线程模式下载效率低下,难以充分利用带宽
本文以王者荣耀英雄皮肤下载为例(日访问量超过1亿的热门游戏),演示如何通过Python并发编程实现高效数据抓取。
二、技术选型分析
2.1 为什么选择并发线程?
-
I/O密集型场景:网络请求占比90%以上
-
GIL限制:Python线程适合I/O密集型任务
-
资源开销:线程比进程轻量(实测内存占用<50MB)
2.2 技术栈对比
方案 | 开发成本 | 性能 | 资源占用 | 适用场景 |
---|---|---|---|---|
单线程 | 低 | 差 | 低 | 小规模数据 |
多线程 | 中 | 优 | 中 | I/O密集型任务 |
异步IO | 高 | 优 | 低 | 高频次请求 |
分布式爬虫 | 高 | 极优 | 高 | 千万级数据采集 |
三、环境准备
3.1 安装依赖库
pip install requests concurrent-futures tqdm
四、代码解析
4.1 三级任务划分
-
主线程:调度任务队列
heroes = fetch_hero_data(url)
-
线程池:并行处理英雄数据
futures = [executor.submit(process_hero, hero)...]
-
子任务:并行下载多张皮肤
for index, skin_name in enumerate(...): download_image(...)
4.2 结果聚合
for future in as_completed(futures):
result = future.result()
results.append(result)
-
使用as_completed按完成顺序收集结果
-
支持实时进度显示
六、法律与道德边界
-
严格遵守robots.txt协议
-
设置合理请求间隔(本示例为1.2秒)
-
禁止用于商业用途
-
数据仅用于学习研究
七、完整实现
import requests
import os
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
def fetch_hero_data(url):
"""获取英雄数据"""
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
}
try:
response = requests.get(url, headers=header, timeout=10)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"请求失败: {e}")
return None
def download_image(img_url, save_path):
"""下载并保存图片"""
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
}
try:
response = requests.get(img_url, headers=header, stream=True, timeout=15)
if response.status_code == 200:
with open(save_path, 'wb') as f:
for chunk in response.iter_content(1024):
f.write(chunk)
return True
except Exception as e:
print(f"下载失败 {img_url}: {e}")
return False
def process_hero(hero):
"""处理单个英雄数据并下载皮肤"""
hero_info = {
'ename': hero.get('ename'),
'cname': hero.get('cname'),
'title': hero.get('title'),
'skin_list': hero.get('skin_name', '').split('|')
}
# 创建英雄目录
save_dir = f"skins/{hero_info['cname']}"
os.makedirs(save_dir, exist_ok=True)
# 下载所有皮肤
for index, skin_name in enumerate(hero_info['skin_list'], 1):
img_url = f"https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/{hero_info['ename']}/{hero_info['ename']}-bigskin-{index}.jpg"
save_path = f"{save_dir}/{skin_name}.jpg"
if download_image(img_url, save_path):
print(f"已下载:{hero_info['cname']} - {skin_name}")
else:
print(f"失败:{hero_info['cname']} - {skin_name}")
return hero_info
def main():
url = "https://pvp.qq.com/web201605/js/herolist.json"
# 获取英雄列表
heroes = fetch_hero_data(url)
if not heroes:
print("无法获取英雄数据")
return
# 使用线程池处理并发请求(调整为8个worker)
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [executor.submit(process_hero, hero) for hero in heroes]
results = []
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
if result:
results.append(result)
except Exception as e:
print(f"处理数据时出错: {e}")
# 打印统计结果
print("\n下载完成,统计信息:")
print(f"总英雄数: {len(results)}")
for hero in results:
print(f"{hero['cname']}({hero['title']}) 皮肤数: {len(hero['skin_list'])}")
if __name__ == "__main__":
main()