Selenium - FullPage ScreenShot
採用 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 就可以執行截圖的動作,速度不但快且也與實際畫面相符。最終是採用這個方案
實作細節
整個流程大致上會是
- 瀏覽目標網頁
- 等候網頁載入完成
- 進行截圖、添加浮水印、保存圖片
操控 Selenium 瀏覽網頁
HeadLess Mode
截圖的應用程式執行的時候,希望是採用 HeadLess 模式運作,也因為後續的截圖,CDP 指令是將瀏覽器的可見範圍進行截圖,所以在 HeadLess 模式下偵測網頁高度,並且重新調整可視範圍的寬高就很重要。
1 | // headless 模式 |
要使用
HeadLess
模式,可以透過設置ChromeOptions
並經由建構式注入給ChromeDriver
即可
瀏覽網頁
1 | // 瀏覽網頁 |
等候網頁讀取完成
等候網頁
透過 javaScript
的 document.readyState ==='complete'
來判斷是否讀取完成,在 Selenium 底下需要執行這一段程式碼,直到回傳的結果為 True,表示網頁已經讀取完畢,可以準備截圖了。
WebDriver 有實作介面 IJavaScriptExecutor
,該介面提供ExecuteScript
允許執行javaScript
,再透過WebDriverWait
所提供的方法來實作,如下範例
1 | // ChromeDriver : ChromiumDriver : WebDriver : IJavaScriptExecutor |
在完成讀取頁面後,一樣是透過javaScript
回傳網頁高度,準備等等設置 Selenium 的寬高來截圖
取得網頁高度
1 | var docHeight = driver.ExecuteScript("return Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)").ToString(); |
透過 CDP Command 將瀏覽器可視範圍進行截圖
使用 CDP 指令截圖
利用Page.captureScreenshot
這個指令做截圖,回傳的結果是圖片的 base64 編碼字串,文件可以參考這邊,在這邊需要給予設定的參數,但是實際上,若照著文件上的 clip
這個 ViewPort 物件設置參數會出錯,但直接給予 width
、height
則是可行的
1 | var screenshot = driver.ExecuteCdpCommand("Page.captureScreenshot", new Dictionary<string, object>() |
將圖片 base64 轉為 Image
1 | var img = Base64StringToImage(base64Str); |
幫圖片添加浮水印
1 | private static void ApplyWaterMark(Image bmp) |
保存圖片
1 | img.Save($"D:\\Temp\\Demo.jpg", ImageFormat.Jpeg); |
結論
測試的結果速度很不錯,但這只是一個簡單的概念驗證,實務上很有可能會有很多奇奇怪怪的情況需要處理,使用上要特別注意一下,詳細程式碼放在 Github,有需要請自取