建立一個Puppeteer微服務以部署到Google Cloud Functions

語言: CN / TW / HK

Puppeteer是對無頭Chrome瀏覽器的一個高階抽象,具有廣泛的API。這使得它能夠非常方便地實現與網頁的自動互動。

本文將向您介紹一個用例,我們將在GitHub上搜索一個關鍵詞,並獲取第一個結果的標題。

這是一個基本的例子,純粹是為了演示,甚至不用Puppeteer也能完成。因為關鍵詞可以在GitHub的頁面上的URL和頁面列表中出現,所以可以直接導航到結果。

但是,假設你在網頁上的互動並沒有反映在頁面的URL中,也沒有公共的API來獲取資料,那麼通過Puppeteer實現的自動化就會很方便。

設定Puppeteer和Node.js

讓我們在一個資料夾內初始化一個Node.js專案。在你的系統終端,導航到你想要的專案資料夾,並執行以下命令。

``` npm init -y

```

這將生成一個package.json 檔案。接下來,安裝Puppeteer的npm包。

``` npm install --save puppeteer

```

現在,建立一個名為service.mjs 的檔案。這個檔案格式允許我們使用ES模組,並將負責通過使用Puppeteer來搜刮頁面。讓我們用Puppeteer完成一個快速測試,看看它是否有效。

首先,我們啟動一個Chrome例項,並通過headless: false 引數來顯示它,而不是在沒有GUI的情況下無頭執行它。現在用newPage 方法建立一個頁面,用goto 方法導航到作為引數傳遞的URL。

``` import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: false });

const page = await browser.newPage(); await page.goto('https://www.github.com');

```

當您執行這段程式碼時,應彈出一個Chrome視窗,並在新標籤頁中導航到URL。

與Puppeteer一起使用自動化

為了讓Puppeteer與頁面互動,我們需要手動檢查頁面,並指定要針對的DOM元素。

我們需要確定選擇器,即類名、id、元素型別,或其中幾個的組合。如果我們需要高度的特殊性,我們可以用各種Puppeteer方法來使用這些選擇器。

現在,讓我們用瀏覽器來檢查www.github.com。我們需要能夠關注頁面頂部的搜尋輸入欄,然後輸入我們想要搜尋的關鍵詞。然後我們需要在鍵盤上GitEnter 按鈕。

在你喜歡的瀏覽器上開啟www.github.com--我用的是Chrome,但任何瀏覽器都可以--在頁面上點選右鍵,點選檢查。然後,在元素標籤下,你可以看到DOM樹。使用檢查窗格左上角的檢查工具,你可以點選元素,在DOM樹中突出顯示它們。

DOM Tree

我們感興趣的輸入欄位元素有幾個類名,但只針對.header-search-input ,就足夠了。為了確保我們所指的是正確的元素,我們可以在瀏覽器控制檯快速測試。點選控制檯標籤,在Document 物件上使用querySelector 方法。

``` document.querySelector('.header-search-input')

```

如果這能返回正確的元素,那麼我們就知道它能與Puppeteer一起工作。

注意,可能有幾個元素與同一個選擇器相匹配。在這種情況下,querySelector 返回第一個匹配的元素。為了引用正確的元素,你需要使用querySelectorAll ,然後從返回的元素的NodeList 中挑選正確的索引。

這裡有一件事我們應該注意。如果你調整GitHub網頁的大小,輸入欄就會變得不可見,而在漢堡包選單中可以看到。

因為除非漢堡包選單開啟,否則它是不可見的,所以我們無法關注它。為了確保輸入框是可見的,我們可以通過將defaultViewport 物件傳遞給設定,明確地設定瀏覽器視窗大小。

``` const browser = await puppeteer.launch({ headless: false, defaultViewport: { width: 1920, height: 1080 } });

```

現在是時候使用查詢來鎖定該元素了。在嘗試與該元素進行互動之前,我們必須確保它在頁面上已被渲染並準備好。Puppeteer有waitForSelector 方法,就是這個原因。

它把選擇器字串作為第一個引數,把選項物件作為第二個引數。因為我們要與該元素進行互動,即聚焦,然後在輸入框中輸入,我們需要讓它在頁面上可見,因此有了visible: true 這個選項。

``` const inputField = '.header-search-input'; await page.waitForSelector(inputField, { visible: true });

```

如前所述,我們需要聚焦於輸入欄元素,然後模擬打字。為了這些目的,Puppeteer有以下方法。

``` const keyword = 'react';

await page.focus(inputField); await page.keyboard.type(keyword);

```

到目前為止,service.mjs ,看起來如下。

``` import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: false, defaultViewport: { width: 1920, height: 1080 } });

const page = await browser.newPage(); await page.goto('https://www.github.com');

const inputField = '.header-search-input'; const keyword = 'react'

await page.waitForSelector(inputField); await page.focus(inputField); await page.keyboard.type(keyword);

```

當你執行程式碼時,你應該看到搜尋欄位被聚焦,而且它有輸入的react 關鍵字。

現在,模擬在鍵盤上按下Enter 鍵。

``` await page.keyboard.press('Enter');

```

在我們按下回車鍵後,Chrome會導航到一個新的頁面。如果我們手動搜尋關鍵詞並檢查我們被導航到的頁面,我們會發現我們感興趣的元素的選擇器是.repo-list

在這一點上,我們需要確保導航到新頁面是完整的。要做到這一點,有一個page.waitForNavigation 方法。在導航之後,我們再次需要通過使用page.waitForSelector 方法來等待該元素。

然而,如果我們只對從該元素中刮取一些資料感興趣,我們不需要等到它在視覺上可見。所以,這一次,我們可以省略{ visible: true } ,該選項預設設定為false

``` const repoList = '.repo-list';

await page.waitForNavigation(); await page.waitForSelector(repoList);

```

一旦我們知道.repo-list 選擇器在DOM樹中,那麼我們就可以通過使用page.evaluate 方法來搜刮標題了。

首先,我們通過將repoList 變數傳遞給querySelector 來選擇.repo-list 。然後我們級聯querySelectorAll ,得到所有的li 元素,並從NodeList 的元素中選擇第一個元素。

最後,我們新增另一個querySelector ,目標是.f4.text-normal 查詢,它有我們通過innerText 訪問的標題。

``` const title = await page.evaluate((repoList) => ( document .querySelector(repoList) .querySelectorAll('li')[0] .querySelector('.f4.text-normal') .innerText ), repoList);

```

現在我們可以把所有的東西都包在一個函式裡面,並把它匯出到另一個檔案中使用,在那裡我們將為Express伺服器設定一個端點來提供資料。

service.mjs 的最終版本返回一個非同步函式,該函式將關鍵字作為輸入。在該函式內部,我們使用一個try…catch 塊來捕捉並返回任何錯誤。最後,我們呼叫browser.close 來關閉我們所啟動的瀏覽器。

``` import puppeteer from 'puppeteer';

const service = async (keyword) => { const browser = await puppeteer.launch({ headless: true, defaultViewport: { width: 1920, height: 1080 } });

const inputField = '.header-search-input'; const repoList = '.repo-list';

try { const page = await browser.newPage(); await page.goto('https://www.github.com');

await page.waitForSelector(inputField);
await page.focus(inputField);
await page.keyboard.type(keyword);

await page.keyboard.press('Enter');

await page.waitForNavigation();
await page.waitForSelector(repoList);

const title = await page.evaluate((repoList) => (
  document
    .querySelector(repoList)
    .querySelectorAll('li')[0]
    .querySelector('.f4.text-normal')
    .innerText
), repoList);

await browser.close();
return title;

} catch (e) { throw e; } }

export default service;

```

建立一個Express伺服器

我們需要一個單一的端點來提供資料,在這裡我們捕獲要搜尋的關鍵詞作為一個路由引數。因為我們將路由路徑定義為/:keyword ,所以它在req.params 物件中以keyword 為鍵暴露出來。接下來,我們呼叫service 函式,將這個關鍵詞作為輸入引數傳遞給Puppeteer執行。

server.mjs 的內容如下。

``` import express from 'express'; import service from './service.mjs';

const app = express(); app.listen(5000);

app.get('/:keyword', async (req, res) => { const { keyword } = req.params; try { const response = await service(keyword); res.status(200).send(response); } catch (e) { res.status(500).send(e); } });

```

在終端中,執行node server.mjs ,啟動伺服器。在另一個終端視窗中,通過使用curl ,向端點發送一個請求。這應該返回條目標題中的字串值。

``` curl localhost:5000/react

```

請注意,這個伺服器是最基本的。在生產中,你應該保護你的端點並設定CORS,以防你需要從瀏覽器而不是伺服器傳送請求。

部署到谷歌雲功能

現在我們要把這個服務部署到無伺服器的雲函式上。雲函式和伺服器的主要區別在於,雲函式在請求時被快速呼叫,並保持一段時間以響應後續請求,而伺服器則始終處於執行狀態。

部署到谷歌雲功能是非常直接的。然而,為了成功執行Puppeteer,你應該注意一些設定。

首先,為你的雲功能分配足夠的記憶體。根據我的測試,512MB對Puppeteer來說是足夠的,但如果你遇到了與記憶體有關的問題,請分配更多。

package.json 的內容應該如下。

``` { "name": "puppeteer-example", "version": "0.0.1", "type": "module", "dependencies": { "puppeteer": "^10.2.0", "express": "^4.17.1" } }

```

我們將puppeteerexpress 作為依賴項,並設定 "型別":"模組",以便使用ES6語法。

現在建立一個名為service.js 的檔案,並將我們在service.mjs 中使用的內容填入其中。

index.js 的內容如下。

``` import express from 'express'; import service from './service.js';

const app = express();

app.get('/:keyword', async (req, res) => { const { keyword } = req.params; try { const response = await service(keyword); res.status(200).send(response); } catch (e) { res.status(500).send(e); } });

export const run = app;

```

在這裡,我們從express 包和我們的函式匯入了server.js

與我們在localhost上測試的伺服器程式碼不同,我們不需要監聽一個埠,因為這一點是自動處理的。

而且,與localhost不同,我們需要在雲端函式中匯出app 物件。將入口點設定為run 或任何你要匯出的變數名稱,如下圖所示。預設情況下,這被設定為helloWorld

Entry Point

為了方便測試我們的雲函式,讓我們把它公開。選擇雲功能(注意:它旁邊有一個複選框),然後點選頂欄選單中的許可權按鈕。這將顯示一個側板,你可以在那裡新增負責人。點選新增委託人,在新委託人欄位中搜索allUsers 。最後,選擇Cloud Function Invoker 作為角色

請注意,一旦添加了這個委託人,任何擁有觸發連結的人都可以呼叫這個功能。對於測試來說,這很好,但要確保為你的雲功能實現認證,以避免不受歡迎的呼叫,這將反映在賬單上。

Add Principals

現在點選你的函式,檢視函式的細節。導航到觸發器標籤,在那裡你會找到觸發器的URL。點選這個連結來呼叫這個函式,它返回列表中第一個資源庫的標題。現在你可以在你的應用程式中使用這個連結來獲取資料。

總結

我們已經介紹瞭如何使用Puppeteer來自動實現與網頁的基本互動,並通過Node伺服器上的Express框架來刮取內容,為其服務。然後,我們將其部署在Google Cloud Functions上,以使其成為一個微服務,然後可以在另一個應用程式中整合和使用。

建立Puppeteer微服務以部署到Google Cloud Functions》一文出現在LogRocket部落格上。