使用Parallel.ForEach搭配Puppeteer时无法打开URL的问题求助
Puppeteer并行爬坑:Parallel.ForEach失效与性能优化方案
问题根源
Parallel.ForEach是为CPU密集型任务设计的多线程并行工具,它完全不兼容异步委托——你传入的async lambda会被当作async void执行,这会导致异步操作脱离生命周期管理,加上Puppeteer的Page实例并非线程安全,多线程环境下直接操作必然会出现GoToAsync失效、数据获取失败等问题。
正确的异步并行方案
改用Task.WhenAll实现异步IO任务的并行,同时用SemaphoreSlim控制并发数,避免无限制创建Page导致浏览器资源耗尽:
// 限制最大并发数,根据机器性能调整,建议5-10之间 var concurrencySemaphore = new SemaphoreSlim(8); var taskList = Urls.Select(async url => { await concurrencySemaphore.WaitAsync(); try { Uri uri = new Uri(url); var myDomain = uri.Host; var objectDomain = domains.FirstOrDefault(item => item.Domain == myDomain); // 容错:跳过无配置的域名 if (objectDomain == null) { Console.WriteLine($"域名 {myDomain} 无配置,跳过URL:{url}"); return; } var priceSelector = objectDomain.Price; var oldPriceSelector = objectDomain.oldPrice; var page = await browser.NewPageAsync(); try { // 性能优化:禁用图片、CSS等非必要资源加载 await page.SetRequestInterceptionAsync(true); page.Request += (_, requestArgs) => { var resourceType = requestArgs.Request.ResourceType; if (resourceType is ResourceType.Image or ResourceType.StyleSheet or ResourceType.Font) { _ = requestArgs.Request.AbortAsync(); } else { _ = requestArgs.Request.ContinueAsync(); } }; // 设置导航超时,避免单个请求卡住 await page.GoToAsync(url, new NavigationOptions { Timeout = 30000 }); var priceNode = await page.QuerySelectorAsync($".{priceSelector}"); var oldPriceNode = await page.QuerySelectorAsync($".{oldPriceSelector}"); // 提取数据示例 if (priceNode != null) { var currentPrice = await priceNode.GetTextContentAsync(); // 处理价格数据,比如存入数据库或集合 } if (oldPriceNode != null) { var originalPrice = await oldPriceNode.GetTextContentAsync(); // 处理原价数据 } } catch (Exception ex) { Console.WriteLine($"处理URL {url} 失败:{ex.Message}"); } finally { // 必须关闭Page释放资源 await page.CloseAsync(); concurrencySemaphore.Release(); } } catch (Exception ex) { Console.WriteLine($"初始化URL {url} 时出错:{ex.Message}"); concurrencySemaphore.Release(); } }); // 等待所有异步任务完成 await Task.WhenAll(taskList);
额外性能优化点
- 复用Page(同域名场景):如果多个URL属于同一域名,可以复用单个Page,通过
page.GoToAsync跳转不同地址,减少Page创建销毁的开销。 - 启用浏览器缓存:在启动Browser时配置缓存路径,避免重复请求静态资源:
var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true, UserDataDir = "./browser-cache" // 指定缓存目录 }); - 调整导航参数:设置
WaitUntil = WaitUntilNavigation.Networkidle2,在网络请求基本停止时就结束导航,无需等待完全加载:await page.GoToAsync(url, new NavigationOptions { Timeout = 30000, WaitUntil = WaitUntilNavigation.Networkidle2 }); - 禁用JavaScript(如果允许):如果目标页面不需要JS渲染就能获取数据,直接禁用JS可以大幅提升加载速度:
await page.SetJavaScriptEnabledAsync(false);
内容的提问来源于stack exchange,提问作者Long
相关产品推荐
相关产品推荐

