如何使用multiprocessing加速BeautifulSoup爬虫及批量图片下载任务
优化方案及实现
原有代码的核心问题
- 串行执行:所有页面请求、图片下载都是逐个等待完成,IO等待时间被完全浪费,是速度慢的核心原因
- 冗余逻辑:函数定义放在循环内部,每次遍历URL都会重复定义函数,无意义消耗性能
- 无超时设置:网络请求没有超时规则,遇到无响应的站点会无限卡住
- 全局变量风险:原代码用全局变量
z做文件名计数,多进程场景下会出现资源竞争,导致文件名重复/错乱
多进程实现版本
from bs4 import BeautifulSoup import requests import os import pandas as pd from multiprocessing import Pool, cpu_count # 全局配置 FOLDER_NAME = "downloadedpics" REQUEST_TIMEOUT = 10 # 网络请求超时时间,单位秒 MAX_WORKERS = cpu_count() * 2 # 进程数,IO密集型可设为CPU核心数的2-4倍,可根据实际情况调整 # 提前创建存储目录,避免多进程下重复创建报错 if not os.path.exists(FOLDER_NAME): os.mkdir(FOLDER_NAME) def get_image_link(image): """提取图片链接的公共逻辑""" attrs = ["data-srcset", "data-src", "data-fallback-src", "src"] for attr in attrs: try: return image[attr] except KeyError: continue return None def process_url(args): """单个URL的完整处理流程:请求页面、提取图片、下载图片""" url_idx, url = args # 传入url的索引和url本身,用来命名文件避免冲突 count = 0 try: # 请求页面 r = requests.get(url, timeout=REQUEST_TIMEOUT) r.raise_for_status() # 遇到4xx/5xx错误直接抛出异常跳过 soup = BeautifulSoup(r.text, 'html.parser') images = soup.find_all('img', class_='pannable-image') print(f"URL索引{url_idx}:找到{len(images)}张图片") if not images: return 0 for img_idx, image in enumerate(images): img_link = get_image_link(image) if not img_link: continue try: # 下载图片 img_content = requests.get(img_link, timeout=REQUEST_TIMEOUT).content # 用url索引+图片索引命名,完全避免文件名冲突 file_path = os.path.join(FOLDER_NAME, f"{url_idx}_image_{img_idx+1}.jpg") with open(file_path, "wb+") as f: f.write(img_content) count += 1 except Exception: continue print(f"URL索引{url_idx}:成功下载{count}/{len(images)}张图片") return count except Exception: print(f"URL索引{url_idx}:请求失败,跳过") return 0 if __name__ == "__main__": # 读取URL列表 df = pd.read_csv(r'C:\Users\fani\Desktop\History.csv') url_list = list(enumerate(df['url'].tolist())) # 给每个url加索引 # 创建进程池执行任务 with Pool(processes=MAX_WORKERS) as pool: results = pool.map(process_url, url_list) total_download = sum(results) print(f"所有任务执行完成,总共下载{total_download}张图片")
关键逻辑说明
- 进程池管理:使用
multiprocessing.Pool统一管理进程,不需要手动创建销毁进程,效率更高,同时可以限制最大并发数,避免并发太高被站点封禁IP - 避免资源竞争:给每个URL加上独有的索引,用索引命名文件,彻底解决多进程下全局计数变量的冲突问题
- 异常兼容:所有网络请求都加了超时和异常捕获,不会因为个别站点/图片的问题卡住整个任务
- 参数可调整:
MAX_WORKERS可以根据你的网络情况调整,如果出现频繁请求失败可以适当调低数值
可选优化:多线程版本(更适合IO密集型场景)
爬取场景属于典型的IO密集型任务,用多线程也可以达到几乎相同的提速效果,且资源消耗更低,只需要把主函数部分替换为以下代码即可,其余逻辑完全不变:
from concurrent.futures import ThreadPoolExecutor if __name__ == "__main__": df = pd.read_csv(r'C:\Users\fani\Desktop\History.csv') url_list = list(enumerate(df['url'].tolist())) MAX_WORKERS = 16 # 线程数可以设得更高,比如16-32,根据网络情况调整 with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: results = list(executor.map(process_url, url_list)) total_download = sum(results) print(f"所有任务执行完成,总共下载{total_download}张图片")
内容的提问来源于stack exchange,提问作者Sheykh Bahaei
相关产品推荐
相关产品推荐

