快轉到主要內容

Selenium - FullPage ScreenShot

·399 字·2 分鐘
Art
作者
Art
這是我的技術筆記。

採用 Selenium + ChromeDriver 實作網頁快照功能,雖然 FirefoxDriver 好像有一個可以截完整頁面的方法可以用,但平常沒用 FireFox 也不想安裝,直接使用 Chrome 的 CDP Command 來處理截圖

大概就是因為之前的排程有時候會出問題,查了一下原因發現有很多雷,因此最後開始 Survey ScreenShot via Selenium 的技術來重新開發這個功能,說實在的這個功能其實不太困難。但是也有一些值得紀錄的地方,查詢了一下大概可以看到有幾種比較常見的解決方案,在這邊我大概嘗試了幾種,並記錄於下

解決方案
#

WebBrowser
#

這個解決方案是原版程式,也就是有時候會出問題的那個版本,說實在的這個解決方案其實我沒有考慮深入研究,依據MSDN - WebBrowser 控制項概觀說明,它會採用IE,基本上看到這一句我已經不想用了,其他的問題就不再贅述,總而言之不考慮這個方案

Selenium 3 + Noksa.WebDriver.ScreenshotsExtensions
#

這個是我一開始嘗試找到的解決方案,最終的成果雖然可以用,但實際上他截圖的概念是模擬使用者捲動 ScrollBar,然後將每一段的畫面拼接起來,最終合併成一個完整的網頁快照,這個方法有很多的弊端,例如畫面捲動的時候,網頁上浮動的元素也會跟著動,最終的快照截圖上面就都是那些浮動元素;此外也因為捲動的關係,快照一個網頁的時間會很久,在快照任務繁重的背景之下,此方案無疑是GG了

nuget:Noksa.WebDriver.ScreenshotsExtensions

html2canvas
#

這個是透過前端套件將畫面產生圖檔的方式,實際原理就是讓 Selenium 瀏覽網頁後,將 script 注入到網頁上並執行一段呼叫該套件的 javascript,最終將結果存放於全域變數 window 下面,然後再經由 selenium 取得圖片,如此就可以透過後端儲存截圖。而這個套件的缺點也非常明顯,他是基於 Virtual DOM 所產生出來的圖而不是瀏覽器畫面截圖,所以會跟實際上的不一樣,針對快照的需求來說,這屬於不可以接受的解決方案

github:html2canvas

Selenium 4 + Chrome DevTools Protocol
#

在搜尋快照的時候,發現了 Chrome DevTool 實際上也可以做截圖。經由 Selenium WebDriver 去呼叫 CDP Command 就可以執行截圖的動作,速度不但快且也與實際畫面相符。最終是採用這個方案

實作細節
#

整個流程大致上會是

  1. 瀏覽目標網頁
  2. 等候網頁載入完成
  3. 進行截圖、添加浮水印、保存圖片

操控 Selenium 瀏覽網頁
#

HeadLess Mode 截圖的應用程式執行的時候,希望是採用 HeadLess 模式運作,也因為後續的截圖,CDP 指令是將瀏覽器的可見範圍進行截圖,所以在 HeadLess 模式下偵測網頁高度,並且重新調整可視範圍的寬高就很重要。

// headless 模式
ChromeOptions options = new ChromeOptions();
options.AddArgument("headless");
options.AddArgument($"--window-size={width}x{height}");
var driver = new ChromeDriver(options);

要使用 HeadLess 模式,可以透過設置 ChromeOptions並經由建構式注入給 ChromeDriver即可

瀏覽網頁

// 瀏覽網頁
 driver.Navigate().GoToUrl("https://www.google.com.tw");

等候網頁讀取完成
#

等候網頁

透過 javaScriptdocument.readyState ==='complete'來判斷是否讀取完成,在 Selenium 底下需要執行這一段程式碼,直到回傳的結果為 True,表示網頁已經讀取完畢,可以準備截圖了。

WebDriver 有實作介面 IJavaScriptExecutor,該介面提供ExecuteScript允許執行javaScript,再透過WebDriverWait所提供的方法來實作,如下範例

// ChromeDriver : ChromiumDriver : WebDriver : IJavaScriptExecutor
IJavaScriptExecutor js = driver;
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(30));
wait.IgnoreExceptionTypes(typeof(InvalidOperationException));
// wait default 500ms
wait.Until(wd => (bool)js.ExecuteScript("return document.readyState === 'complete'"));

在完成讀取頁面後,一樣是透過javaScript回傳網頁高度,準備等等設置 Selenium 的寬高來截圖

取得網頁高度

var docHeight = driver.ExecuteScript("return Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)").ToString();
int.TryParse(docHeight, out height);

// 重新指定網頁寬高
driver.Manage().Window.Size = new Size(width, height);

透過 CDP Command 將瀏覽器可視範圍進行截圖
#

使用 CDP 指令截圖 利用Page.captureScreenshot這個指令做截圖,回傳的結果是圖片的 base64 編碼字串,文件可以參考這邊,在這邊需要給予設定的參數,但是實際上,若照著文件上的 clip這個 ViewPort 物件設置參數會出錯,但直接給予 widthheight則是可行的

var screenshot = driver.ExecuteCdpCommand("Page.captureScreenshot", new Dictionary<string, object>()
{
    // Image compression format (defaults to png).Allowed Values: jpeg, png, webp
    { "format", "jpeg" },
    // Compression quality from range [0..100] (jpeg only).
    { "quality", 70 },
    // Capture the screenshot beyond the viewport. Defaults to false
    { "captureBeyondViewport", true },
    // Capture the screenshot from the surface, rather than the view. Defaults to true.
    { "fromSurface", true },
    { "width", width },
    { "height", height },
});
var base64Str = ((Dictionary<string, object>)screenshot)["data"].ToString();

將圖片 base64 轉為 Image

var img = Base64StringToImage(base64Str);

private static Bitmap Base64StringToImage(string base64String)
{
    byte[] buffer = Convert.FromBase64String(base64String);

    MemoryStream stream = null;
    Bitmap bitmap;
    var data = (byte[])buffer.Clone();
    try
    {
        stream = new MemoryStream(data);
        stream.Position = 0;
        bitmap = new Bitmap(Image.FromStream(stream));
    }
    finally
    {
        if (stream != null)
        {
            stream.Close();
            stream.Dispose();
        }
    }

    return bitmap;
}

幫圖片添加浮水印

private static void ApplyWaterMark(Image bmp)
{
    var font = new Font("arial", 16, FontStyle.Bold);
    int x = 0;
    int y = 0;

    using (Graphics graphics = Graphics.FromImage(bmp))
    {
        var printStr = new StringBuilder();
        printStr.AppendLine($"TIME: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        printStr.AppendLine("Order: This is Fake Order Number");
        printStr.AppendLine("Product: I am Fake Product Code");

        SizeF measureStr = graphics.MeasureString(printStr.ToString(), font);
        graphics.FillRectangle(Brushes.Black, new Rectangle(x, y, (int)measureStr.Width, (int)measureStr.Height));
        graphics.DrawString(printStr.ToString(), font, Brushes.White, new PointF(x, y));
    }
}

保存圖片

img.Save($"D:\\Temp\\Demo.jpg", ImageFormat.Jpeg);

結論
#

測試的結果速度很不錯,但這只是一個簡單的概念驗證,實務上很有可能會有很多奇奇怪怪的情況需要處理,使用上要特別注意一下,詳細程式碼放在 Github,有需要請自取