我想用C#和Selenium和ChromeDriver截取一整页的屏幕截图。在这里:https://stackoverflow.com/a/45201692/5400125 我找到了一个如何在 Java 中执行此操作的示例。我正在尝试在 C# 中实现这一点,但是在第一次调用 sendEvaluate 时加载页面后出现异常:
OpenQA.Selenium.WebDriverException: 'no like session (Driver info: chromedriver=2.41.578737 (49da6702b16031c40d63e5618de03a32ff6c197e(,platform=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
这段代码对我来说很好用,可以创建一个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:pathtodirectoryofchromedriver", options);
driver.Url = "https://stackoverflow.com/questions";
Screenshot screenshot = driver.GetFullPageScreenshot();
screenshot.SaveAsFile(@"C:desiredscreenshotpathFullPageScreenshot.png");
这是我获取全屏屏幕截图的示例:
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();
我已经更新了 JimEvans 解决方案,通过拍摄多个屏幕截图并将它们拼接在一起,也可以处理长度超过 8192/16384px 的页面。
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("rn", 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;
}
}