【问题标题】:C# - Selenium full page screenshotC# - Selenium 整页截图
【发布时间】:2019-02-02 05:18:33
【问题描述】:

我想使用带有 Selenium 和 ChromeDriver 的 C# 截取整页屏幕截图。在这里:https://stackoverflow.com/a/45201692/5400125 我找到了一个如何在 Java 中执行此操作的示例。我试图在 C# 中实现这一点,但是在第一次调用 sendEvaluate 时加载页面后出现异常:

OpenQA.Selenium.WebDriverException: '没有这样的会话(驱动程序信息:chromedriver=2.41.578737 (49da6702b16031c40d63e5618de03a32ff6c197e),平台=Windows NT 10.0.17134 x86_64)'

public class ChromeDriverEx : ChromeDriver
{
    public ChromeDriverEx(string chromeDriverDirectory, ChromeOptions options)
        : base(chromeDriverDirectory, options, RemoteWebDriver.DefaultCommandTimeout)
    {
        var addCmd = this.GetType().BaseType
            .GetMethod("AddCustomChromeCommand", BindingFlags.NonPublic | BindingFlags.Instance);
        addCmd.Invoke(this,
            new object[] {"sendCommand", "POST", "/session/:sessionId/chromium/send_command_and_get_result"});
    }

    public void GetFullScreenshot()
    {
        Object metrics = sendEvaluate(
            @"({" +
            "width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0," +
            "height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0," +
            "deviceScaleFactor: window.devicePixelRatio || 1," +
            "mobile: typeof window.orientation !== 'undefined'" +
            "})");
    }

    private object sendEvaluate(string script)
    {
        var response = sendCommand("Runtime.evaulate",
            new Dictionary<string, object> {{"returnByValue", true}, {"expression", script}});
        return response;
    }

    private object sendCommand(string cmd, object param)
    {
        var r = this.Execute("sendCommand", new Dictionary<string, object> {{"cmd", cmd}, {"params", param}});
        return r.Value;
    }
}

我这样称呼它:

 var opts = new ChromeOptions();
 opts.AddAdditionalCapability("useAutomationExtension", false);
 opts.AddArgument("disable-infobars");
 var driver = new ChromeDriverEx(".", opts);
 driver.Navigate().GoToUrl("https://stackoverflow.com/questions");
 driver.GetFullScreenshot();

我正在使用 Chrome 68 和 ChromeDriver 2.41

【问题讨论】:

  • 不应该是"/session/{sessionId}/chromium/send_command" 基于此处类似的行:github.com/SeleniumHQ/selenium/blob/master/dotnet/src/webdriver/…
  • @KirilS。不确定,我对硒等了解不多。但它不会改变任何东西,同样的例外。
  • 好吧,异常“没有这样的会话”告诉我会话有问题...尝试添加更多日志以获得更详细的信息 (stackoverflow.com/questions/13204820/…)
  • 只是要指出,这是在 .NET 绑定中添加新命令的一种令人难以置信的迂回方式。完全不需要使用反射,因为绑定中已经有扩展点。

标签: c# selenium selenium-webdriver selenium-chromedriver


【解决方案1】:

我更新了 JimEvans 解决方案,通过拍摄多个屏幕截图并将它们拼接在一起,也可以处理超过 8192/16384 像素的页面。

16384px 的限制源于合成器使用的最大纹理尺寸。

    public class ChromeDriverEx : ChromeDriver
{
    private const string SendChromeCommandWithResult = "sendChromeCommandWithResponse";
    private const string SendChromeCommandWithResultUrlTemplate = "/session/{sessionId}/chromium/send_command_and_get_result";

    public ChromeDriverEx(ChromeDriverService service, ChromeOptions options)
        : base(service, options)
    {
        CommandInfo commandInfoToAdd = new CommandInfo(CommandInfo.PostCommand, SendChromeCommandWithResultUrlTemplate);
        this.CommandExecutor.CommandInfoRepository.TryAddCommand(SendChromeCommandWithResult, commandInfoToAdd);
    }

    public OpenQA.Selenium.Screenshot GetFullPageScreenshot()
    {
        // Evaluate this only to get the object that the
        // Emulation.setDeviceMetricsOverride command will expect.
        // Note that we can use the already existing ExecuteChromeCommand
        // method to set and clear the device metrics, because there's no
        // return value that we care about.
        string metricsScript = @"({
        width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0,
        height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0,
        deviceScaleFactor: window.devicePixelRatio || 1,,
        mobile: typeof window.orientation !== 'undefined'
        })";
        Dictionary<string, object> metrics = this.EvaluateDevToolsScript(metricsScript);
        this.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);
        Thread.Sleep(1000);
        Dictionary<string, object> parameters = new Dictionary<string, object>
        {
            ["format"] = "png",
            ["fromSurface"] = true
        };
        var fullHeight = int.Parse(metrics["height"]?.ToString() ?? "0");
        var splitSSAt = 8192;

        if (fullHeight > splitSSAt)
        {
            var currentHeight = splitSSAt;
            var startHeight = 0;
            List<Bitmap> bitmaps = new List<Bitmap>();

            while (fullHeight > 0)
            {
                if (currentHeight > fullHeight)
                {
                    currentHeight = fullHeight;
                }
                parameters["clip"] = new Dictionary<string, object>
                {
                    ["x"] = 0,
                    ["y"] = startHeight,
                    ["width"] = metrics["width"],
                    ["height"] = currentHeight,
                    ["scale"] = 1,
                };

                object splitScreenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
                Dictionary<string, object> splitScreenshotResult = splitScreenshotObject as Dictionary<string, object>;
                Byte[] bitmapData = Convert.FromBase64String(FixBase64ForImage(splitScreenshotResult["data"] as string));
                MemoryStream streamBitmap = new System.IO.MemoryStream(bitmapData);
                bitmaps.Add(new Bitmap((Bitmap)Image.FromStream(streamBitmap)));
                fullHeight -= splitSSAt;
                startHeight += splitSSAt;
            }

            using var ms = new MemoryStream();
            using var bitmap = new Bitmap(MergeImages(bitmaps));
            bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            var base64 = Convert.ToBase64String(ms.GetBuffer()); //Get Base64
            return new OpenQA.Selenium.Screenshot(base64);
        }
        object screenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
        Dictionary<string, object> screenshotResult = screenshotObject as Dictionary<string, object>;
        string screenshotData = screenshotResult["data"] as string;

        this.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, object>());

        var screenshot = new OpenQA.Selenium.Screenshot(screenshotData);
        return screenshot;
    }

    public string FixBase64ForImage(string image)
    {
        StringBuilder sbText = new StringBuilder(image, image.Length);
        sbText.Replace("\r\n", String.Empty); sbText.Replace(" ", string.Empty);
        return sbText.ToString();
    }

    private Bitmap MergeImages(IEnumerable<Bitmap> images)
    {
        var enumerable = images as IList<Bitmap> ?? images.ToList();

        var width = 0;
        var height = 0;

        foreach (var image in enumerable)
        {
            width = image.Width;
            height += image.Height;
        }

        var bitmap = new Bitmap(width, height);
        using (var g = Graphics.FromImage(bitmap))
        {
            var localHeight = 0;
            foreach (var image in enumerable)
            {
                g.DrawImage(image, 0, localHeight);
                localHeight += image.Height;
            }
        }
        return bitmap;
    }


    public object ExecuteChromeCommandWithResult(string commandName, Dictionary<string, object> commandParameters)
    {
        if (commandName == null)
        {
            throw new ArgumentNullException("commandName", "commandName must not be null");
        }

        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["cmd"] = commandName;
        parameters["params"] = commandParameters;
        Response response = this.Execute(SendChromeCommandWithResult, parameters);
        return response.Value;
    }

    private Dictionary<string, object> EvaluateDevToolsScript(string scriptToEvaluate)
    {
        // This code is predicated on knowing the structure of the returned
        // object as the result. In this case, we know that the object returned
        // has a "result" property which contains the actual value of the evaluated
        // script, and we expect the value of that "result" property to be an object
        // with a "value" property. Moreover, we are assuming the result will be
        // an "object" type (which translates to a C# Dictionary<string, object>).
        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["returnByValue"] = true;
        parameters["expression"] = scriptToEvaluate;
        object evaluateResultObject = this.ExecuteChromeCommandWithResult("Runtime.evaluate", parameters);
        Dictionary<string, object> evaluateResultDictionary = evaluateResultObject as Dictionary<string, object>;
        Dictionary<string, object> evaluateResult = evaluateResultDictionary["result"] as Dictionary<string, object>;

        // If we wanted to make this actually robust, we'd check the "type" property
        // of the result object before blindly casting to a dictionary.
        Dictionary<string, object> evaluateValue = evaluateResult["value"] as Dictionary<string, object>;
        return evaluateValue;
    }
}

【讨论】:

    【解决方案2】:

    这是我获取全屏截图的示例:

                string _currentPath = Path.GetDirectoryName(Assembly.GetAssembly(typeof(One of your objects)).Location) + @"\Attachs\";
                var filePath = _currentPath + sSName;
    
                if (!Directory.Exists(_currentPath))
                    Directory.CreateDirectory(_currentPath);
    
                Dictionary<string, Object> metrics = new Dictionary<string, Object>();
                metrics["width"] = _driver.ExecuteScript("return Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)");
                metrics["height"] = _driver.ExecuteScript("return Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)");
                metrics["deviceScaleFactor"] = (double)_driver.ExecuteScript("return window.devicePixelRatio");
                metrics["mobile"] = _driver.ExecuteScript("return typeof window.orientation !== 'undefined'");
                _driver.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);
    
                _driver.GetScreenshot().SaveAsFile(filePath, ScreenshotImageFormat.Png);
    
                _driver.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, Object>());
                _driver.Close();
    

    【讨论】:

      【解决方案3】:

      这段代码对我来说很好,可以创建ChromeDriver 的子类。请注意,下面的代码故意以非常非常冗长的风格编写,以便清楚地说明解决方案的每一部分。它可以很容易地写得更简洁,这取决于一个人的编码风格和对健壮错误处理的要求。此外,在未来的版本中,无需创建用于执行返回结果的 DevTools 命令的方法;这种方法已经成为 .NET 绑定的一部分。

      public class ChromeDriverEx : ChromeDriver
      {
          private const string SendChromeCommandWithResult = "sendChromeCommandWithResponse";
          private const string SendChromeCommandWithResultUrlTemplate = "/session/{sessionId}/chromium/send_command_and_get_result";
      
          public ChromeDriverEx(string chromeDriverDirectory, ChromeOptions options)
              : base(chromeDriverDirectory, options)
          {
              CommandInfo commandInfoToAdd = new CommandInfo(CommandInfo.PostCommand, SendChromeCommandWithResultUrlTemplate);
              this.CommandExecutor.CommandInfoRepository.TryAddCommand(SendChromeCommandWithResult, commandInfoToAdd);
          }
      
          public Screenshot GetFullPageScreenshot()
          {
              // Evaluate this only to get the object that the
              // Emulation.setDeviceMetricsOverride command will expect.
              // Note that we can use the already existing ExecuteChromeCommand
              // method to set and clear the device metrics, because there's no
              // return value that we care about.
              string metricsScript = @"({
      width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0,
      height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0,
      deviceScaleFactor: window.devicePixelRatio || 1,
      mobile: typeof window.orientation !== 'undefined'
      })";
              Dictionary<string, object> metrics = this.EvaluateDevToolsScript(metricsScript);
              this.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);
      
              Dictionary<string, object> parameters = new Dictionary<string, object>();
              parameters["format"] = "png";
              parameters["fromSurface"] = true;
              object screenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
              Dictionary<string, object> screenshotResult = screenshotObject as Dictionary<string, object>;
              string screenshotData = screenshotResult["data"] as string;
      
              this.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, object>());
      
              Screenshot screenshot = new Screenshot(screenshotData);
              return screenshot;
          }
      
          public object ExecuteChromeCommandWithResult(string commandName, Dictionary<string, object> commandParameters)
          {
              if (commandName == null)
              {
                  throw new ArgumentNullException("commandName", "commandName must not be null");
              }
      
              Dictionary<string, object> parameters = new Dictionary<string, object>();
              parameters["cmd"] = commandName;
              parameters["params"] = commandParameters;
              Response response = this.Execute(SendChromeCommandWithResult, parameters);
              return response.Value;
          }
      
          private Dictionary<string, object> EvaluateDevToolsScript(string scriptToEvaluate)
          {
              // This code is predicated on knowing the structure of the returned
              // object as the result. In this case, we know that the object returned
              // has a "result" property which contains the actual value of the evaluated
              // script, and we expect the value of that "result" property to be an object
              // with a "value" property. Moreover, we are assuming the result will be
              // an "object" type (which translates to a C# Dictionary<string, object>).
              Dictionary<string, object> parameters = new Dictionary<string, object>();
              parameters["returnByValue"] = true;
              parameters["expression"] = scriptToEvaluate;
              object evaluateResultObject = this.ExecuteChromeCommandWithResult("Runtime.evaluate", parameters);
              Dictionary<string, object> evaluateResultDictionary = evaluateResultObject as Dictionary<string, object>;
              Dictionary<string, object> evaluateResult = evaluateResultDictionary["result"] as Dictionary<string, object>;
      
              // If we wanted to make this actually robust, we'd check the "type" property
              // of the result object before blindly casting to a dictionary.
              Dictionary<string, object> evaluateValue = evaluateResult["value"] as Dictionary<string, object>;
              return evaluateValue;
          }
      }
      

      您可以将此代码与以下内容一起使用:

      ChromeOptions options = new ChromeOptions();
      ChromeDriverEx driver = new ChromeDriverEx(@"C:\path\to\directory\of\chromedriver", options);
      driver.Url = "https://stackoverflow.com/questions";
      
      Screenshot screenshot = driver.GetFullPageScreenshot();
      screenshot.SaveAsFile(@"C:\desired\screenshot\path\FullPageScreenshot.png");
      

      【讨论】:

        猜你喜欢
        • 2023-03-09
        • 1970-01-01
        • 2017-12-29
        • 2021-07-12
        • 2017-06-02
        • 1970-01-01
        • 1970-01-01
        • 2017-12-25
        相关资源
        最近更新 更多