Python多进程/列表推导式网页爬取代码速度过慢如何优化
问题原因说明
你之前尝试的4种方案没有提速效果,核心原因是对任务类型和工具的适用场景判断错误:
- 普通for循环和列表推导式本质都是单线程同步执行,额外的函数调用还会带来开销,速度没有提升属于正常现象
- 你使用的
multiprocessing.dummy.ThreadPool是线程池,Python GIL锁会导致CPU密集型任务无法通过多线程并行,DOM解析属于纯CPU计算场景,线程池不仅无法提速,线程切换还会增加额外开销 - asyncio方案存在两个问题:一是你写的异步函数内没有任何可等待的IO操作,全是同步CPU计算逻辑,即使不报错也不会有提速效果;二是
RuntimeError报错是因为你在已经运行事件循环的环境(如Jupyter、IPython控制台)中调用了asyncio.run(),但这个问题和性能无关,不需要花时间解决。
可行提速方案(按效果优先级排序)
1. 最低成本优化:替换解析器+减少重复查询开销
这一步改完通常就能把3分钟的处理时间压缩到10秒以内,不需要改整体逻辑:
- 首先把BS4的默认解析器
html.parser换成lxml,解析速度直接提升3-5倍,先安装依赖:pip install lxml,初始化soup时指定解析器:soup = BeautifulSoup(html_content, 'lxml') - 你当前代码在循环中反复调用
select_one,每次调用都会执行CSS选择器匹配逻辑,开销极大,可以改为直接遍历tr节点的子元素,避免重复查询。优化后的代码示例:
ROWS = [] # 提前一次性取出所有目标tr,避免循环内重复查询DOM target_trs = soup.select('tr[id^=mix]') for tr in target_trs: dt = "" H_team = "" A_team = "" # 直接遍历tr的后代节点,按class匹配目标内容,不需要反复跑CSS选择器 for node in tr.descendants: if node.name != "td": continue node_cls = node.get("class", []) if "h" in node_cls: dt = node.text elif "Home" in node_cls: H_team = node.text.strip() elif "Away" in node_cls: A_team = node.text.strip() ROWS.append([dt, H_team, A_team])
2. 多进程并行优化(适合超大数据量场景)
CPU密集型任务要绕过GIL实现并行,必须使用真正的多进程,注意不能直接把BS4元素对象传给子进程,跨进程序列化对象的开销会抵消并行收益,正确做法是把tr节点转成原始HTML字符串再分块传给子进程处理:
import os from multiprocessing import Pool from bs4 import BeautifulSoup def parse_tr_chunk(tr_str_list): chunk_res = [] for tr_str in tr_str_list: tr = BeautifulSoup(tr_str, "lxml").tr dt = tr.select_one('.h').text H_team = tr.select_one('td.Home').text.strip() A_team = tr.select_one('td.Away').text.strip() chunk_res.append([dt, H_team, A_team]) return chunk_res if __name__ == "__main__": # 主进程先读入HTML生成soup with open("target.html", "r", encoding="utf-8") as f: soup = BeautifulSoup(f.read(), "lxml") # 把所有tr转成原始字符串,不要传BS4对象 all_tr_str = [str(tr) for tr in soup.select('tr[id^=mix]')] # 按CPU核心数拆分任务块 core_num = os.cpu_count() chunk_size = len(all_tr_str) // core_num tr_chunks = [all_tr_str[i:i+chunk_size] for i in range(0, len(all_tr_str), chunk_size)] # 多进程处理 with Pool(core_num) as pool: part_results = pool.map(parse_tr_chunk, tr_chunks) # 合并所有结果 ROWS = [] for part in part_results: ROWS.extend(part)
3. 极致速度优化:替换解析库
如果追求毫秒级解析速度,可以直接放弃BeautifulSoup,使用lxml的原生XPath接口解析,速度是BS4+lxml的5-10倍,3000行tr的解析基本可以在100毫秒内完成:
from lxml import etree with open("target.html", "r", encoding="utf-8") as f: tree = etree.HTML(f.read()) ROWS = [] # 用XPath一次性匹配所有目标tr for tr in tree.xpath('//tr[starts-with(@id, "mix")]'): dt = tr.xpath('.//*[contains(@class, "h")]/text()')[0] H_team = tr.xpath('./td[contains(@class, "Home")]/text()')[0].strip() A_team = tr.xpath('./td[contains(@class, "Away")]/text()')[0].strip() ROWS.append([dt, H_team, A_team])
注意:异步IO只适合网络请求、文件读写这类IO密集型场景,本地DOM解析是纯CPU计算,用异步IO没有任何提速效果,不需要在这个场景下尝试异步方案。
内容的提问来源于stack exchange,提问作者Khaled Koubaa
相关产品推荐
相关产品推荐

