multiprocessing.Pool.map批量爬取链接触发MaybeEncodingError报错
问题根因
你遇到的MaybeEncodingError: RecursionError('maximum recursion depth exceeded while pickling an object')以及之前多套爬取方案不稳定,核心是3个问题:
- Jupyter Notebook环境下,定义在单元格内的函数、引用了全局作用域非基础类型对象(requests.Session、BeautifulSoup实例、大列表等)的函数,在被multiprocessing序列化传输时会递归回溯整个作用域链,超过Python默认递归深度直接报错;嵌套进程池的写法会触发daemon进程无法创建子进程的限制,进一步加重对象引用循环问题。
- 之前的单线程、asyncio、线程池方案没有做异常隔离、请求重试、限流落盘,单条链接报错就会中断整个任务,请求频率过高触发目标站连接拒绝、TCP连接泄漏后自然跑不到全量完成。
- 多进程场景下如果把主进程初始化的Session、解析器对象传给子进程,这类带锁、连接池的复杂对象无法被正常序列化,也会触发pickle递归错误。
修复步骤
1. 先解决多进程序列化报错
在Notebook最开头(重启内核后第一个运行的单元格)执行以下代码,强制进程启动模式为spawn,避开fork模式的作用域引用bug:
import multiprocessing multiprocessing.set_start_method('spawn', force=True)
注意:这段代码必须放在所有爬虫逻辑、进程池初始化代码之前,不能重复执行。
2. 改造爬取Worker为纯函数
Worker函数不要引用任何Notebook全局作用域的可变对象,不要嵌套进程池,每个子进程独立初始化请求会话,返回值只使用字符串、字典、数字这类基础Python类型,绝对不要返回Response、BeautifulSoup这类带复杂引用的实例,参考实现:
import requests from bs4 import BeautifulSoup import time import random def crawl_single_link(url: str) -> dict: # 子进程内部独立初始化Session,禁止从主进程传入Session对象 session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" }) max_retry = 3 for retry_cnt in range(max_retry): try: # 随机限流,避免请求过密被站点封禁 time.sleep(random.uniform(0.3, 1.8)) resp = session.get(url, timeout=15) resp.raise_for_status() resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "html.parser") # 以下替换为你自己的页面解析逻辑 meeting_name = soup.select_one("h1.title").get_text(strip=True) if soup.select_one("h1.title") else "" address = soup.select_one(".address").get_text(strip=True) if soup.select_one(".address") else "" table_data = {} for row in soup.select("table.detail tr"): cells = row.select("td, th") if len(cells) >= 2: table_data[cells[0].get_text(strip=True)] = cells[1].get_text(strip=True) session.close() return { "url": url, "meeting_name": meeting_name, "address": address, "table_data": str(table_data), "status": "success" } except Exception as e: if retry_cnt == max_retry - 1: session.close() return {"url": url, "status": "failed", "err_msg": str(e)} time.sleep(2 * (retry_cnt + 1))
如果改完还是报序列化错误,直接把这个Worker函数存到同目录下的crawl_utils.py文件里,在Notebook里用from crawl_utils import crawl_single_link导入,彻底解决顶层作用域序列化问题。
3. 替换进程池调用逻辑,边爬边落盘
不要用Pool.map等待全量任务完成再返回结果,用imap_unordered按爬取完成顺序实时写入CSV,避免中途崩溃丢失几小时的爬取进度,进程数不要开太高,4-8个足够,进程过多反而会触发站点限流:
import csv from multiprocessing import Pool if __name__ == "__main__": # 替换为你的33488条链接列表 with open("target_urls.txt", "r", encoding="utf-8") as f: url_list = [line.strip() for line in f if line.strip()] # 边爬边写,实时刷盘 with Pool(processes=6) as pool, open("meeting_result.csv", "w", encoding="utf-8-sig", newline="") as csvf: writer = csv.DictWriter(csvf, fieldnames=["url", "meeting_name", "address", "table_data", "status", "err_msg"]) writer.writeheader() # chunksize设为16-32,减少进程间通信开销 for res in pool.imap_unordered(crawl_single_link, url_list, chunksize=16): writer.writerow(res) csvf.flush()
稳定性补充说明
- 爬取过程中如果出现大量连接失败,把进程数调低到4,把随机延时范围调到1-3秒,避免触发目标站IP封禁。
- 不需要用嵌套进程池、itertools做复杂的结果包装,越简单的纯函数逻辑越不容易触发序列化错误。
- 爬取完成后单独筛选CSV里
status为failed的链接二次补爬即可,不需要每次都从头跑全量。
内容的提问来源于stack exchange,提问作者CMWolfe
相关产品推荐
相关产品推荐

