如何解决BeautifulSoup破坏asyncio与aiohttp异步执行的问题?
问题根源:CPU密集操作阻塞了asyncio事件循环
你遇到的这个问题,核心原因在于BeautifulSoup的lxml解析是同步的CPU密集型操作,而asyncio的事件循环是单线程的——当某个协程卡在解析HTML这个耗时的CPU操作上时,事件循环根本没法切换到其他待执行的协程,导致所有请求都只能排队等着当前解析完成,看起来就像变成了同步逐个请求。
简单来说:asyncio的异步优势体现在I/O等待(比如网络请求)时切换协程,但遇到纯CPU计算的阻塞操作,单线程的事件循环就会卡壳。
解决方案:把解析操作放到线程池执行
要解决这个问题,我们需要把BeautifulSoup的解析任务从asyncio的主线程事件循环里剥离,放到线程池中执行,这样事件循环可以在解析任务运行时继续处理其他网络请求协程。
修改你的validate_page函数,用asyncio.get_running_loop().run_in_executor()来包装解析操作:
import asyncio import traceback from bs4 import BeautifulSoup import aiohttp async def fetch(session, url): try: async with session.get(url) as response: return await response.text() except Exception as e: print(url, str(e)) return False # 把解析操作封装成独立的同步函数 def parse_html(html): try: return BeautifulSoup(html, 'lxml') except Exception: print(traceback.format_exc()) return False async def validate_page(session, url): res = await fetch(session, url) if res: # 用线程池执行解析操作,避免阻塞事件循环 loop = asyncio.get_running_loop() soup = await loop.run_in_executor(None, parse_html, res) # 这里可以继续处理soup(比如提取页面内容),按需添加逻辑 return soup is not False async def validate_page_bounded(sem, session, url): async with sem: return await validate_page(session, url) # 示例主函数 async def main(): urls = ["https://example.com", "https://example.org"] # 替换为你的URL列表 sem = asyncio.Semaphore(10) # 自定义并发限制数 async with aiohttp.ClientSession() as session: tasks = [validate_page_bounded(sem, session, url) for url in urls] results = await asyncio.gather(*tasks) print(results) if __name__ == "__main__": asyncio.run(main())
补充说明
- 为什么注释掉解析就正常?因为
fetch里的session.get是I/O操作,asyncio会在等待响应时自动切换到其他协程,不会阻塞事件循环;但解析是纯CPU操作,没有让出事件循环的机会。 - 线程池大小可自定义:
run_in_executor的第一个参数可以传concurrent.futures.ThreadPoolExecutor(max_workers=...),默认会使用一个合理的线程数,一般不需要特意调整。
内容的提问来源于stack exchange,提问作者Felix
相关产品推荐
相关产品推荐

