使用asyncio.gather执行异步函数时出现挂起问题(单独执行可正常运行)
看起来你遇到了个挺头疼的问题——单独await每个异步操作时程序跑得好好的,一用asyncio.gather并发执行就直接挂住,只输出了"Scraping product data"就没动静了。结合你用的是nodriver+FastAPI的场景,我来帮你捋捋可能的原因和解决办法:
可能的核心原因
nodriver的Page对象不支持并发操作
很多浏览器自动化工具(包括nodriver)的页面实例内部其实是有锁机制的,同一时间只能处理一个DOM查询或操作请求。当你用asyncio.gather同时发起十几个page.select_all/page.select调用时,这些请求会互相抢占资源,最后全都堵在那儿,导致整个程序挂起。异常处理掩盖了阻塞细节
你的safe_select和safe_select_all函数里做了全局异常捕获,就算某个协程因为页面操作卡住或者出错,也只会打印日志然后返回None,但asyncio.gather还是会一直等待所有任务完成——如果有任务一直没结束,整个gather就会无限挂着。
可行的解决方案
方案一:放弃页面级并发,改用串行执行
既然单个await能正常运行,那可以把asyncio.gather的并发逻辑改成逐个await的串行方式,虽然少了并发的效率,但能保证程序稳定运行:
# 替换原来的gather代码块 try: # 逐个await获取数据 breadcrumb = await safe_select_all("#desktop-category-nav > div > div > ul > li > a") sales_price = await safe_select("div[data-buy-box-region='price'] > p:nth-child(1)") original_price = await safe_select("div[data-buy-box-region='price'] > p:nth-child(2) > span:nth-child(2)") product_name = await safe_select("#listing-page-cart h1") category_url = await safe_select(".breadcrumb_nav ul > li:last-child > a", "href") num_reviews = await safe_select(".reviews__shop-info h2") last_review_date = await safe_select(".review-card > div > div > div:nth-child(3) p") store_url = await safe_select("#shop_owners_content_toggle a", "href") brand = await safe_select("#shop_owners_content_toggle p") more_from_this_shop_urls = await safe_select_all("div[data-page-type='recs_ribbon_same_shop'] > a", "href") product_details = await safe_select_all("#product_details_content_toggle li div") main_image = await safe_select(".image-carousel-container > ul > li:nth-child(1) > img", "src") last_24_hours = await safe_select("#listing-page-cart > div:nth-child(1) > p") results = [ breadcrumb, sales_price, original_price, product_name, category_url, num_reviews, last_review_date, store_url, brand, more_from_this_shop_urls, product_details, main_image, last_24_hours ] except Exception as e: print(f"Error during fetching: {str(e)}", flush=True) results = [None] * 13
方案二:给任务添加超时机制
如果想保留并发逻辑,可以给每个协程加上超时,这样某个任务卡住时会触发超时异常,帮你定位到底是哪个选择器出了问题:
import asyncio # 修改gather部分的代码 try: results = await asyncio.gather( asyncio.wait_for(safe_select_all("#desktop-category-nav > div > div > ul > li > a"), timeout=15), asyncio.wait_for(safe_select("div[data-buy-box-region='price'] > p:nth-child(1)"), timeout=15), # 其他任务也用同样的方式包裹 asyncio.wait_for(safe_select("#listing-page-cart > div:nth-child(1) > p"), timeout=15) ) except asyncio.TimeoutError as e: print("某个页面操作超时了,检查对应的选择器或页面加载状态", flush=True) except Exception as e: print(f"Error during concurrent fetching: {str(e)}", flush=True) results = [None] * 13
方案三:检查nodriver的并发支持
可以去看看nodriver的官方文档,确认它是否支持同一页面的并发DOM操作,有没有需要开启的配置项。有些工具默认不允许页面级并发,或者需要特殊的初始化参数才能启用。
额外调试建议
- 在
safe_select和safe_select_all里添加更详细的日志,比如进入函数、开始执行page.select_all、完成操作的时间戳,这样能精准定位到哪个步骤卡住了。 - 先简化
gather里的任务,只保留2-3个测试,如果正常再逐步添加其他任务,找到导致挂起的具体选择器。
备注:内容来源于stack exchange,提问作者Tharusha Jayasooriya

