如何在C#中使用Chromium headless(无头模式)浏览器

CEFSharp

在前面的文章中,我介绍过CEFSharp

CEF 全称是Chromium Embedded Framework(Chromium嵌入式框架),是个基于Google Chromium项目的开源Web browser控件,支持Windows, Linux, Mac平台。

CEFSharp就是CEF的C#移植版本。

CEFSharp提供了一个headless(无头模式)版本。

所谓无头模式(Headless Mode),就是浏览器不显示 GUI 界面,后台完整运行渲染引擎与 JS 引擎,适合服务器 / 自动化场景。

如何使用CEFSharp的无头模式

这里提供了一个CefSharp.OffScreen包,基础使用步骤如下:

1、创建一个控制台项目,nuget安装CefSharp.OffScreen包

注意:截止最新稳定版 147.0.100,CEFSharp.OffScreen.NETCore包仅支持.NET 6.0-windows版本,后续 不知道会不会支持.NET 6.0以上版本

2、项目属性 → 32 位 / 64 位一致

3、 设置为 STA 线程

复制代码
1  class Program
2  {
3      [STAThread]
4      static async Task Main(string[] args)
5      {
6 
7      }
8 }

4、基础使用代码如下

复制代码
 1 // 全局浏览器实例
 2 private static ChromiumWebBrowser _browser;
 3 
 4 //需要指定STA线程
 5 [STAThread] 
 6 static void Main(string[] args)
 7 {
 8     //CEF全局配置
 9     var cefSettings = new CefSettings
10     {
11         //缓存路径
12         CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache"),
13 
14         //自定义UA
15         UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36"
16     };
17 
18     //最优离屏渲染参数
19     cefSettings.SetOffScreenRenderingBestPerformanceArgs();
20 
21     //初始化CEF
22     if (!Cef.Initialize(cefSettings, true))
23     {
24         Console.WriteLine("CEF初始化失败");
25         return;
26     }
27 
28     try
29     {
30         _browser = new ChromiumWebBrowser();
31 
32         //加载网页
33         _browser.Load("https://www.baidu.com");
34 
35         //等待加载完成
36         LoadPageAsync(_browser).Wait();
37 
38     }
39     catch (Exception ex)
40     {
41         Console.WriteLine(ex.Message);
42     }
43     finally
44     {
45         //释放资源
46         _browser?.Dispose();
47         Cef.Shutdown();
48 
49         Console.WriteLine("请输入任意键退出");
50         Console.ReadLine();
51     }
52 }

这里我们需要封装一个等待函数来等待网页执行完成

复制代码
 1  /// <summary>
 2  /// 等待页面加载完成
 3  /// </summary>
 4  /// <param name="browser"></param>
 5  /// <returns></returns>
 6  public static Task LoadPageAsync(IWebBrowser browser)
 7  {
 8      var tcs = new TaskCompletionSource<bool>();
 9      EventHandler<LoadingStateChangedEventArgs> handler = null;
10      handler = (sender, args) =>
11      {
12          //判断没有在加载,则代表整个页面加载完成了
13          if (!args.IsLoading)
14          {
15              browser.LoadingStateChanged -= handler;
16              tcs.TrySetResult(true);
17          }
18      };
19      //处理加载状态更改事件
20      browser.LoadingStateChanged += handler;
21      return tcs.Task;
22  }

运行效果:

常用事件

复制代码
1      // 渲染画面回调(每一帧都会触发)
2      _browser.Paint += _browser_Paint;
3 
4      // JS控制台日志捕获
5      _browser.ConsoleMessage += _browser_ConsoleMessage;
6 
7      _browser.FrameLoadEnd += _browser_FrameLoadEnd;

事件处理程序

复制代码
 1  /// <summary>
 2  /// 浏览器完成页面框架加载时触发的事件处理程序。
 3  /// 页面可能同时存在多个框架正在加载;
 4  /// 主框架加载完成后,子框架仍可能发起或继续加载流程。
 5  /// 无论请求是否加载成功,该方法都会对所有框架执行回调。
 6  /// 请注意:此事件在 CEF 界面线程触发,默认情况下该线程与应用程序界面主线程不是同一个线程。
 7  /// 不要在该线程中进行任何耗时阻塞操作,否则浏览器会无响应甚至卡死。
 8  /// 如果需要操作界面控件,必须通过Invoke/Dispatch切换至应用 UI 主线程。
 9  /// </summary>
10  /// <param name="sender"></param>
11  /// <param name="e"></param>
12  /// <see cref="https://cefsharp.github.io/api/118.6.x/html/E_CefSharp_IChromiumWebBrowserBase_FrameLoadEnd.htm"/>
13  private static async void _browser_FrameLoadEnd(object? sender, FrameLoadEndEventArgs e)
14  {
15      //参考:https://www.cnblogs.com/zhaotianff/p/9556270.html
16      var html = await e.Browser.GetSourceAsync();
17      Console.WriteLine(html);
18      Console.WriteLine("frame 加载完成");
19  }
20 
21  /// <summary>
22  /// 用于接收网页发送的 JavaScript 控制台消息的事件处理程序。
23  /// 需要重点注意:该事件在 CEF 界面线程触发,默认情况下该线程与你的应用程序界面线程并非同一线程。
24  /// 切勿在此线程中执行任何耗时阻塞操作,否则浏览器会失去响应甚至卡死。
25  /// 若要操作界面控件,你必须通过调用Invoke/Dispatch切换至应用主线程。
26  /// </summary>
27  /// <param name="sender"></param>
28  /// <param name="e"></param>
29  /// <see cref="https://cefsharp.github.io/api/118.6.x/html/E_CefSharp_IChromiumWebBrowserBase_ConsoleMessage.htm"/>
30  private static void _browser_ConsoleMessage(object? sender, ConsoleMessageEventArgs e)
31  {
32      
33  }
34 
35  /// <summary>
36  /// 
37  /// </summary>
38  /// <param name="sender"></param>
39  /// <param name="e"></param>
40  /// <exception cref="NotImplementedException"></exception>
41  private static void _browser_Paint(object? sender, OnPaintEventArgs e)
42  {
43      // 该回调在 CEF 界面线程触发,默认情况下该线程与应用程序主线程并非同一线程。
44      // 页面元素需要绘制时触发此方法
45 
46      //BGRA 4字节/像素
47      int stride = e.Width * 4; 
48 
49      //创建Bitmap:CEF像素格式 BGRA
50      Bitmap bmp = new Bitmap(e.Width, e.Height, stride, System.Drawing.Imaging.PixelFormat.Format32bppArgb, e.BufferHandle);
51 
52      bmp.Save(DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss_ffff") + ".jpg");
53 
54      bmp.Dispose();
55  }

截图效果

常用方法

复制代码
 1     // 执行JS获取返回值
 2     var jsResult = await _browser.EvaluateScriptAsync("document.title");
 3     if (jsResult.Success)
 4     {
 5         Console.WriteLine("网页标题:" + jsResult.Result);
 6     }
 7 
 8     var host = _browser.GetBrowser().GetHost();
 9 
10     // 点击页面元素(坐标点击)
11     // x,y 网页相对坐标
12     //按下
13     host.SendMouseClickEvent(120, 120, MouseButtonType.Left, false, 1, CefEventFlags.None);
14     //等待
15     await Task.Delay(200);
16     //松开
17     host.SendMouseClickEvent(120, 120, MouseButtonType.Left, true, 1, CefEventFlags.None);
18 
19     //输入键盘内容
20     KeyEvent keyEvent = new KeyEvent();
21     keyEvent.WindowsKeyCode = (int)Keys.H;
22     keyEvent.Type = KeyEventType.KeyDown;
23     //按下
24     host.SendKeyEvent(keyEvent);
25     //松开
26     keyEvent.Type = KeyEventType.KeyUp;
27     host.SendKeyEvent(keyEvent);
28     
29     // 网页后退/前进/刷新
30     //_browser.Back();
31     //_browser.Forward();
32     //_browser.Reload();
33 
34     //导出网页为PDF
35     var pdfSettings = new PdfPrintSettings
36     {
37         Landscape = false,
38         PrintBackground = true
39     };
40     await _browser.PrintToPdfAsync("网页导出.pdf", pdfSettings);
41     Console.WriteLine("PDF导出完成");
42 
43     //设置Cookie
44     //var cookieManager = Cef.GetGlobalCookieManager();
45     //cookieManager.SetCookie("https://www.baidu.com", new Cookie
46     //{
47     //    Name = "testCookie",
48     //    Value = "123456",
49     //    Domain = ".baidu.com"
50     //});
51 
52     //获取当前URL
53     Console.WriteLine("当前地址:" + _browser.Address);

参考资料:

General Usage · cefsharp/CefSharp Wiki · GitHub

C#使用Puppeteer - zhaotianff - 博客园

示例代码

cnblog-demo-code/CEFSharpOffScreenDemo at main · zhaotianff/cnblog-demo-code · GitHub