在我的工具箱,Headless Chrome 已取代 PhatomJS 成為擷取網頁內容、自動測試及網頁擷圖/轉 PDF 的首選。 之前我都是自己寫程式呼叫 chrome.exe 傳參數執行各項任務,最近發現更方便的選擇。

Puppeteer Sharp 是 Github 上的開源專案,作者 Darío Kondratiuk 寫了一組 C# API,方便 .NET 開發人員透過 Puppeteer 協定與 Headless Chrome 溝通,操作 Chrome 完成各種任務,省去管理 Process、組裝命令列參數、寫 Socekt 搞通訊協定等苦差事。

PuppeteerSharp 已放上 NuGet,透過 puppeteersharp 關鍵字就能快速找到它:(它是 .NET Standard 2.0 程式庫,專案至少要 .NET 4.6.1+ 或 .NET Core 2.0+ 才能使用)

先說 Puppeteer Sharp 的一項貼心設計:不需在伺服器安裝 Chrome,它會自行下載 Chromium。一來省去部署手續,二來程式用自己專屬的 Chromium 版本,就不會像我一樣,跟使用者共用 Chrome,結果 因自動升級功能遭遇問題

故使用 Puppeteer Sharp 的起手式為:

await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
Browser browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
   Headless = true
});

BrowserFetcher 負責確認 Chromium 是否已存在,若無則自動從 Internet 下載,檔案預設放在執行檔同目錄下的 .load-chromium 資料夾, 容量約 320MB,故第一次執行時要多等幾秒鐘,之後就不用了。(伺服器不一定能連 Internet,部署時記得將 .load-chromium 資料夾一起複製過去)

接著透過 Puppeteer.LaunchAsync() 傳入參數,即可建立一個 Headless Chrome 進行操作。

官方網站已提供許多實用範例,像是擷取網頁畫面、轉存 PDF、在網頁加入 HTML、執行 JavaScript 函式比對結果,足夠大家發揮巧思,組裝出各樣花式應用,這裡不再贅述。

Puppeteer Sharp 是 Puppeteer note.js API 的移植版,若要進階活用需深入了解 Puppeteer 指令,以下是一些資源:

最後,來個簡單練習,我用高速公路 1968 即時路況網站當題材,寫一支排程抓取查詢北中南區的即時路況地圖畫面:

查過原始碼,左上區塊的「全部/北/中/南」按鈕,北中南分別是呼叫 region('N')、region('C') 及 region('P'),故我跳過 await page.ClickAsync("#region_a") 這種直接操作 HTML 元素的做法,改用 await page.EvaluteExpressionAsync("region('N')") 跑 JavaScript 指令操作網頁。

完整範例程式碼如下,補充說明我直接寫成註解,大家直接看 Code 吧。

using PuppeteerSharp;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace PuppetTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Test().Wait();
        }

        static async Task Test()
        {
            try
            {
                await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
                using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions()
                {
                    Headless = true //偵測時可設定false觀察網頁顯示結果(註:非Headless時不能匯出PDF)
                }))
                {
                    using (var page = await browser.NewPageAsync())
                    {
                        await page.GoToAsync("https://1968.freeway.gov.tw/");
                        //透過SetViewport控制視窗大小決定抓圖尺寸
                        await page.SetViewportAsync(new ViewPortOptions
                        {
                            Width = 1024,
                            Height = 768
                        });

                        foreach (var region in "N,C,P".Split(','))
                        {
                            //呼叫網頁程式方法切換區域
                            await page.EvaluateExpressionAsync($"region('{region}')");
                            //要等待網頁切換顯示完成再抓圖
                            //最精準做法是依程式邏輯檢查AJAX動作是否完成,此處偷懶用萬用招:「等一下」
                            Thread.Sleep(1000); 
                            //抓網頁畫面存檔
                            await page.ScreenshotAsync($"D:\\FreewayTraffic\\Snapshot{region}.png");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                throw;
            }
        }
    }
}

這樣,每次執行可取得北中南三張路況網頁擷圖:

工具箱再添好用兵器一件。

Introduce of the handy Headless Chrome integration library - Puppeteer Sharp.


Comments

# by 卡比

Thankyou 試一下

# by ccc0369

可以請教一下嗎?? await page.ClickAsync("#region_a") 是捉元件的ID 值嗎?? 以下的電話分機的input欄位, 我應該捉哪個值?? 在寫 await page.ClickAsync 或 await page.TypeAsyn, 應該捉#AppTel 嗎?? <td class="hdr">電話分機</td> <td> <input type="text" class="pf-med" data-key="AppTel" /> </td> </tr>

# by Jeffrey

to ccc0369, page.ClickAsync()、page.TypeAsync()傳入的第一個參數是CSS Selector,依你的案例可以寫成"input[data-key=AppTel]"。不過依我自己的習慣,我更喜歡自己寫 JavaScript 程式去找欄位填內容、點擊操作 DOM,用 jQuery 簡單又彈性,不必受限於 Puppeteer 現有的API,幾乎沒什麼辦不到的事(經驗裡只有上傳檔案比較頭痛)。

# by 卡比

請問一下,windows 底下我沒有遇到問題,Linux (centos) 試了一下,好像同樣需要安裝 Chromium,而安裝 Chromium 好像需要 8GB Ram,有錯嗎?是不是只要安裝好了 Chromium (於 centos 內),便可以在 cmd 底下執行 headless 的這些 dotnet core app?還是需要 GUI 呢?

# by Jeffrey

to 卡比,安裝 Chromium 耗用的應該是 HD 空間,依我的理解應該不會超過 500MB,好奇 8GB 這個數字是怎麼來的?Chromium 就像 Chrome 一樣,可以顯示 GUI 供使用者操作,Headless 是指透過參數讓相同程式以批次方式執行。Puppteteer Sharp 讓 .NET Core 程式可以叫 Chromium 以 Headless 模式執行並操作它執行任務。

# by 卡比

hi Jeffrey,8GB 這個數字是我在網路上,想找出運行 Chromium 的最低要求時找到的 ,原來沒有這回事。但記憶體不足是發生在我安裝 Chromium 的時候,我在試用 AWS 的最低方案 (512 MB),不足夠安裝,1GB 便能通過了。 Linux 底下運行的 c# web app 能夠調用 Puppteteer,找取網頁的 GUI 畫面,真有趣,謝謝分享。另外好像 Linux 內沒有裝好中文字的話,找取的畫面便沒有中文字,那些我還沒有搞清楚⋯

# by Cloud

請問,如果想抓取網頁中某個區塊,有解法嗎?

# by Cloud

找到了, "除了对整个页面进行截取,Chrome 还支持对页面某个元素进行截取。通过 elementHandle .screenshot() 可针对具体元素进行截取。"

# by 鱼塘是我的

作者您好,非常感谢您的无私分享。(一)请问iframe类型元素应该怎么处理点击那?通过page.MainFrame()取到所有frame以后不知道如何点击自己想要的url内的元素。(二)puppeteer sharp 内哪个命令等价于puppeteer 内的$$eval(selector)命令那?

# by Jeffrey

to 鱼塘是我的,我對 Puppeteer API 研究很有限,幾乎都是自己寫JavaScript/jQuery去操作DOM,用 page.EvaluateExpressionAsync() (應就是你在找的 $$eval(selector) )搞定大小事。

# by 鱼塘是我的

非常感谢您的帮助,非常喜欢您的博客。

Post a comment