Python使用ProcessPoolExecutor执行爬虫任务未保存全部文件是什么原因?
问题核心原因分析
你代码里的疏漏主要集中在多进程资源共享逻辑、变量作用域、异常捕获三个方面,具体如下:
- 多进程下全局变量不共享,全局状态完全失效
你定义的crawled去重列表、全局crawler实例、site、title这些变量都是主进程独立的,子进程启动时只会复制一份初始状态的副本,各子进程之间的修改完全不会同步:- 去重逻辑完全无效,不同子进程可能重复处理同一个url
- 全局
crawler实例的content、soup属性会被多个进程的爬取任务互相覆盖,你看到的请求成功日志可能根本不是当前url的结果,写文件时很容易出现内容错乱、写入空内容甚至报错终止 - 全局
title变量不会随url变化,多个url生成的文件名完全一致,后续写入会直接覆盖之前的文件,这是文件丢失的最核心原因
- 异常捕获不完整,任务失败无感知
你只捕获了crawler.crawl执行的异常,后续文件名生成、文件写入、crawler.soup转字符串等步骤出现的异常(比如文件名包含非法字符、磁盘权限不足、soup对象为空等)完全没有捕获,子进程任务直接静默失败,你只会看到前面的请求成功日志,不知道后续写入已经崩了 - 任务提交后未校验执行结果
你提交任务后没有获取Future对象的执行结果,哪怕任务抛出异常终止,主进程也不会有任何提示,大量失败任务你完全感知不到
修复方案
按照以下步骤修改即可解决文件缺失问题:
- 主进程提前完成url去重,不要在子进程做去重逻辑
- 移除所有全局变量,把
site、对应url的title作为参数传入crawl函数,每次执行crawl时新建独立的crawler实例,避免进程间状态互相干扰 - 文件名增加url的唯一标识(比如url的md5值),同时清洗title里的非法文件名字符,确保不会重名覆盖
- 完整捕获任务全流程异常,同时获取
Future对象的执行结果,及时发现运行错误
修复后参考代码
import hashlib from concurrent.futures import ProcessPoolExecutor from datetime import date from multiprocessing import freeze_support # 替换为你自己的crawler类导入路径 from your_module import crawler def crawl(current_url, site, title): try: # 每次任务新建独立的crawler实例,避免进程间状态冲突 current_crawler = crawler() current_crawler.crawl(current_url, use_session=False, timeout=20) print(f'Fetched {current_url}: {len(current_crawler.content)} bytes') today = date.today() # 文件名加url的md5前缀确保唯一,同时清洗title里的非法文件名字符 url_md5 = hashlib.md5(current_url.encode('utf-8')).hexdigest()[:8] safe_title = "".join([c for c in title if c not in r'\/:*?"<>|']) filename = f'{site} - {today.strftime("%Y-%m-%d")} {safe_title}_{url_md5}.html' with open(filename, 'w', encoding='utf-8') as html_out: html_out.write(f'{today.strftime("%Y-%m-%d")} @ {current_url}\n') html_out.write(str(current_crawler.soup)) return True, current_url except Exception as e: print(f'处理{current_url}失败: {str(e)}') return False, current_url if __name__ == '__main__': freeze_support() # 替换为你自己的url列表,每个元素为(url, 站点名, 页面对应标题)的元组 list_of_urls = [ ("https://example.com/1", "example站点", "页面1标题"), ("https://example.com/2", "example站点", "页面2标题"), ] # 主进程提前完成url去重 seen_urls = set() unique_tasks = [] for url, site, title in list_of_urls: if url not in seen_urls: seen_urls.add(url) unique_tasks.append((url, site, title)) with ProcessPoolExecutor(max_workers=4) as worker: # 提交所有任务并保存future对象 futures = [worker.submit(crawl, url, site, title) for url, site, title in unique_tasks] # 等待所有任务执行完成,统计结果 success_count = 0 for future in futures: success, url = future.result() if success: success_count += 1 print(f"全部任务执行完成,成功{success_count}个,失败{len(futures)-success_count}个")
内容的提问来源于stack exchange,提问作者Rob M
相关产品推荐
相关产品推荐

