Python多进程爬取车辆数据报错:无法pickle _thread.RLock对象
嘿,我来帮你搞定这个爬取效率拉胯和多进程踩坑的问题!你的场景太典型了——单进程爬取速度感人,想转多进程又接连碰壁,咱们一步步拆解解决。
一、先搞定multiprocessing.Process的Pickle错误
问题根源
你遇到的TypeError: can't pickle _thread.RLock objects,90%的概率是因为你把**带锁的对象(比如requests.Session实例)**直接传给了子进程。requests的Session内部用了_thread.RLock来保证线程安全,而这个对象是没法被pickle序列化的——多进程之间传递数据必须序列化,自然就报错了。
解决方案
别在主进程创建Session,让每个子进程自己初始化独立的Session!这样既避开了序列化问题,还能让每个进程拥有专属的连接池,爬取效率反而更高。
示例代码:
import multiprocessing import requests from bs4 import BeautifulSoup # 假设你用这个解析页面 def crawl_single_url(url): # 子进程内部单独创建Session session = requests.Session() try: response = session.get(url, timeout=15) response.raise_for_status() # 主动抛出HTTP错误 # 这里写你的解析逻辑,提取154种车辆属性 data = parse_car_info(response.text) return data except Exception as e: print(f"爬取{url}失败: {str(e)}") return None finally: session.close() def parse_car_info(html): soup = BeautifulSoup(html, "html.parser") # 示例:提取品牌、价格等属性,根据实际页面结构调整 car_data = { "brand": soup.find("div", class_="brand").text.strip(), "price": soup.find("span", class_="price").text.strip(), # 剩下152种属性的提取逻辑... } return car_data if __name__ == "__main__": # 假设你已经有了包含28万条URL的列表 urls = ["https://example.com/car1", "https://example.com/car2", ...] processes = [] max_concurrent = 20 # 根据你的CPU核心数调整,比如8核就开16-20个进程 # 分批处理URL,避免一次性启动太多进程 for batch_start in range(0, len(urls), max_concurrent): batch_urls = urls[batch_start:batch_start+max_concurrent] for url in batch_urls: p = multiprocessing.Process(target=crawl_single_url, args=(url,)) p.start() processes.append(p) # 等待当前批次的进程全部完成,再处理下一批 for p in processes: p.join() processes.clear()
二、解决multiprocessing.Pool在Jupyter Notebook无限卡住的问题
问题根源
Jupyter的运行环境和普通Python脚本不一样——它的主进程已经在交互式环境中运行,multiprocessing.Pool的启动机制(尤其是Windows下的spawn模式)会和Jupyter的事件循环冲突,导致进程彻底卡住。
解决方案
给你三个靠谱的解决办法,按推荐程度排序:
1. 用concurrent.futures.ProcessPoolExecutor替代multiprocessing.Pool
concurrent.futures是更上层的封装API,在Jupyter里的兼容性好得多,写法也更简洁:
from concurrent.futures import ProcessPoolExecutor import multiprocessing import requests def crawl_single_url(url): # 和上面爬取逻辑一致 session = requests.Session() try: response = session.get(url, timeout=15) data = parse_car_info(response.text) return data except Exception as e: print(f"爬取{url}失败: {str(e)}") return None finally: session.close() def parse_car_info(html): # 你的解析逻辑 pass def save_to_db(data): # 示例:把数据保存到数据库或文件 pass if __name__ == "__main__": urls = ["https://example.com/car1", ...] # 进程数设为CPU核心数的2-4倍,平衡CPU和网络IO max_workers = multiprocessing.cpu_count() * 3 with ProcessPoolExecutor(max_workers=max_workers) as executor: # 批量提交爬取任务 results = executor.map(crawl_single_url, urls) # 逐个处理返回结果 for result in results: if result: save_to_db(result)
2. 给multiprocessing.Pool指定启动方式(仅Windows)
如果你非要用multiprocessing.Pool,可以在代码开头强制设置启动方式为spawn,并且确保所有逻辑都在if __name__ == "__main__"块里:
import multiprocessing # 强制设置启动方式为spawn,适配Windows和Jupyter环境 multiprocessing.set_start_method('spawn') def crawl_single_url(url): # 爬取逻辑同上 pass if __name__ == "__main__": urls = ["https://example.com/car1", ...] with multiprocessing.Pool(processes=multiprocessing.cpu_count()*3) as pool: results = pool.map(crawl_single_url, urls) # 处理结果
3. 用Jupyter魔法命令脱离交互式环境
把你的多进程代码写成独立的Python脚本,然后用Jupyter的%%script魔法命令运行,彻底避开Jupyter主进程的干扰:
%%script python import multiprocessing import requests def crawl_single_url(url): # 爬取逻辑 pass if __name__ == "__main__": urls = ["https://example.com/car1", ...] with multiprocessing.Pool() as pool: pool.map(crawl_single_url, urls)
三、额外的效率优化建议
- 控制并发数:别贪多开几十上百个进程,一般CPU核心数的2-4倍就够了,太多会导致CPU上下文切换频繁,反而拖慢速度。
- 代理IP池:如果爬取频繁被网站限制,一定要加代理池,避免IP被封导致大量失败请求。
- 异步+多进程结合:如果你的爬取瓶颈是网络IO,可以用
aiohttp做异步爬取,再配合多进程,效率能再上一个台阶。 - 结果批量保存:别爬一条存一条,攒几十条再批量写入数据库/文件,减少IO开销。
内容的提问来源于stack exchange,提问作者Orhan Solak

