C# 網頁轉圖檔 WebAPI - 青春版
0 |
早先介紹過用 Puppeteer Sharp + Chromium 寫出 C# 網頁擷圖服務。同事反映會遇到 Chrom 無故卡住無回應的現象,依過去經驗,這類 Chrome 層級的疑難排除非我能力所及,解決只能靠運氣,只能祝福原力與他同在。。
回家沒事亂爬文找替代方案,發現一個有趣解法 - 使用 System.Web.Forms.WebBrowser 抓圖!
聲明在先:程式碼是由 Stackoverflow + Google 爬文拼湊改寫,對理論未深入理解,也存在不少已知問題,與其說是解決方案,不如說是實驗。如要拿來用於正式環境,請自行調整評估。
核心程式如下,置於 ASP.NET Web 內,專案參照 System.Web.Forms,故可建立 System.Windows.Forms.WebBrowser 物件,Navigate() 連上指定網址,在 DocumentCompleted 事件呼叫 DrawToBitmap() 方法匯出圖檔 參考,非同步轉同步是靠 AutoResetEvent 搞定。但 WebBrowser 畢竟是桌面程式專用元件,存在特殊限制如必須在 Single-Threaded Apartment 模式下執行。我在 Stackoverflow 找到自開 Thread 設定 SetApartmentState(ApartmentState.STA) 配合 Application.Start()、Application.ExitThread() 的奧妙解法,感覺可能有副作用,但初步實測可行。另外,程式透過 Registry HKEY_CURRENT_USER\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION
參考 指定 WebBrowser 的 IE 相容模式為 IE11 Edge。 圖檔長寬部分則使用 WebBrowser.Document.InvokeScript() 技巧自動偵測 document.body.clientWidth/ clientHeight。
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using System.Windows.Forms;
namespace MyWeb.Models
{
public class WebSnapTool
{
static WebSnapTool()
{
var appName = Process.GetCurrentProcess().MainModule.ModuleName;
long webBrowserEmuVer = 11001; //指定 IE 相容模式 - IE11 Edge Mode
Registry.SetValue(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION",
appName, webBrowserEmuVer, RegistryValueKind.DWord);
Registry.SetValue(@"HKEY_CURRENT_USER\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION",
appName, webBrowserEmuVer, RegistryValueKind.DWord);
}
public static byte[] Snapshot(string url, int width = 1024, int height = 768, int delaySecs = 0, bool autoSize = true)
{
var sync = new AutoResetEvent(false);
byte[] png = null;
//STA
var thread = new Thread(() =>
{
var threadSync = new AutoResetEvent(false);
using (var browser = new System.Windows.Forms.WebBrowser()
{
Width = width, Height = height
})
{
Func<string, string> Eval = (script) =>
{
return browser.Document.InvokeScript("eval", new string[] {
"(function() { return " + script + "; })()" }).ToString();
};
browser.DocumentCompleted += delegate
{
Thread.Sleep(delaySecs * 1000);
if (autoSize)
{
browser.Width = int.Parse(Eval("document.body.clientWidth"));
browser.Height = int.Parse(Eval("document.body.clientHeight"));
}
using (var pic = new Bitmap(browser.Width, browser.Height))
{
browser.DrawToBitmap(pic, new Rectangle(0, 0, pic.Width, pic.Height));
using (var ms = new MemoryStream())
{
pic.Save(ms, ImageFormat.Png);
png = ms.ToArray();
Application.ExitThread();
}
}
};
browser.ScrollBarsEnabled = false;
browser.Navigate(url);
Application.Run();
sync.Set();
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
//20秒無法產生即放棄
var succ = sync.WaitOne(20*1000);
if (thread.IsAlive)
{
thread.Abort();
}
if (succ) return png;
else throw new ApplicationException("Web snapshot failed");
}
}
}
完成後,我在 ASP.NET MVC Home/Index 傳入 URL 取得截圖:(警告:實際上不應任由使用者指定網址,以免被當成跳板)
// 注意:此為示範用途,接受使用者輸入任意網址,實務上應避免以防被當成跳板
public ActionResult Index(string url = null)
{
try
{
var png = WebSnapTool.Snapshot(url ?? "https://www.twitter.com/", delaySecs: 0);
return File(png, "image/png");
}
catch (Exception ex)
{
return Content(ex.Message);
}
}
隨手實測幾個現成網站,得到幾個還能看的樣本,如下圖有 NuGet、Google News、Facebook 登入頁、Twitter 登入頁:
但更多的狀況會得到出現一片空白、部分內容遺失、排版錯亂... 等各式問題,有時 WebBrowser 遇到 HTTPS 安全疑慮、JavaScript 錯誤,Visual Studio + IIS Express 偵錯模式還會跳出對話框要求確認,若是在 IIS 執行不可能互動操作便會卡住。稱不上是一種通用各種網頁的解決方案,但是它存在一些優點:
- 完全靠 .NET 內建元件實現網頁截圖,.NET Framework 3.5 都能跑,不限定 ASP.NET MVC / .NET Core。
- 可直接放在 IIS 上執行,不需要為了 Chromium 特性費神寫 Windows Servcie
- 基於 .NET 技術,在 .NET Process 內執行,較易掌控與偵錯
至於網頁輸出異常率過高,若應用情境擷圖對象有限,內容單純(實務上很多只是數據、圖表而已),也願意積極配合調整(盡可能用純 HTML,簡化 CSS,少用 JavaScript),再加上擷圖頻率有限,則這個出奇簡單的寫法仍很值得一試。
Example of using System.Windows.Forms.WebBrowser in ASP.NET web site to capture image of specified URL.
Comments
Be the first to post a comment