排解 Puppeteer 問題

無頭 Chrome 無法在 Windows 中啟動

部分 Chrome 政策可能會強制透過特定擴充功能執行 Chrome 或 Chromium。

根據預設,Puppeteer 會傳送 --disable-extensions 標記,因此在啟用這些政策時,無法啟動。

如要解決這個問題,請嘗試在沒有旗標的情況下執行:

const browser = await puppeteer.launch({
  ignoreDefaultArgs: ['--disable-extensions'],
});

背景資訊:問題 3681

無頭 Chrome 無法在 UNIX 上啟動

請確認已安裝所有必要的依附元件。您可以在 Linux 機器上執行 ldd chrome | grep not,查看缺少的依附元件。

Debian (Ubuntu) 依附元件

ca-certificates
fonts-liberation
libappindicator3-1
libasound2
libatk-bridge2.0-0
libatk1.0-0
libc6
libcairo2
libcups2
libdbus-1-3
libexpat1
libfontconfig1
libgbm1
libgcc1
libglib2.0-0
libgtk-3-0
libnspr4
libnss3
libpango-1.0-0
libpangocairo-1.0-0
libstdc++6
libx11-6
libx11-xcb1
libxcb1
libxcomposite1
libxcursor1
libxdamage1
libxext6
libxfixes3
libxi6
libxrandr2
libxrender1
libxss1
libxtst6
lsb-release
wget
xdg-utils

CentOS 依附元件

alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
gtk3.x86_64
ipa-gothic-fonts
libXcomposite.x86_64
libXcursor.x86_64
libXdamage.x86_64
libXext.x86_64
libXi.x86_64
libXrandr.x86_64
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-fonts-cyrillic
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils

安裝依附元件後,您必須使用這個指令更新 nss 程式庫

yum update nss -y

查看討論:

  • #290 - Debian 疑難排解
  • #391 - CentOS 疑難排解
  • #379 - 阿爾卑斯山疑難排解

無頭 Chrome 會停用 GPU 合成功能

Chrome 和 Chromium 需要 --use-gl=egl 才能啟用無頭模式下的 GPU 加速功能

const browser = await puppeteer.launch({
  headless: true,
  args: ['--use-gl=egl'],
});

已下載 Chrome,但無法在 Node.js 上啟動

如果嘗試啟動 Chromium 時出現類似下方的錯誤訊息:

(node:15505) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!
spawn /Users/.../node_modules/puppeteer/.local-chromium/mac-756035/chrome-mac/Chromium.app/Contents/MacOS/Chromium ENOENT

這表示瀏覽器已下載,卻無法正確擷取。最常見的原因是 Node.js v14.0.0 錯誤造成 extract-zip 損壞,而 Puppeteer 使用模組將瀏覽器下載內容擷取到正確位置。已在 Node.js v14.1.0 中修正錯誤,因此請確保您執行的是該版本或更高版本。

設定 Chrome Linux 沙箱

為了保護主機環境不受不受信任的網路內容影響,Chrome 會使用多層沙箱機制。 主機必須先設定主機,這項功能才能正常運作。如果沒有適合 Chrome 使用的優質沙箱,就會異常終止並傳回 No usable sandbox! 錯誤。

如果您完全信任自己在 Chrome 中開啟的內容,可以使用 --no-sandbox 引數啟動 Chrome:

const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

在 Chromium 中設定沙箱的方法有 2 種。

只有新式核心支援Sser 命名空間複製。未獲授權的使用者命名空間通常可啟用,但可能會針對 (未採用沙箱機制) 的非根程序開啟更多核心攻擊途徑,提升核心權限。

sudo sysctl -w kernel.unprivileged_userns_clone=1

[替代] 設定 setuid 沙箱

setuid 沙箱可做為獨立執行檔,位於 Puppeteer 下載的 Chromium 旁。您可以為不同的 Chromium 版本重複使用相同的沙箱執行檔,因此以下程式碼在每個主機環境只能執行一次:

# cd to the downloaded instance
cd <project-dir-path>/node_modules/puppeteer/.local-chromium/linux-<revision>/chrome-linux/
sudo chown root:root chrome_sandbox
sudo chmod 4755 chrome_sandbox
# copy sandbox executable to a shared location
sudo cp -p chrome_sandbox /usr/local/sbin/chrome-devel-sandbox
# export CHROME_DEVEL_SANDBOX env variable
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

建議您預設匯出 CHROME_DEVEL_SANDBOX 環境變數。在這種情況下,請將以下內容新增至 ~/.bashrc.zshenv

export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

在 Travis CI 上執行 Puppeteer

我們於 6.0.0 版之前針對 Puppeteer 執行 Puppeteer 測試,直到 6.0.0 版為止,之後才會遷移至 GitHub Actions。如需參考,請參閱 .travis.yml (5.5.0 版)

以下列舉幾個最佳做法:

  • 必須啟動 xvfb 服務,才能在無頭模式下執行 Chromium
  • 預設在 Travis 上執行 Xenial Linux
  • 預設執行 npm install
  • 預設會快取 node_modules

.travis.yml 可能看起來會像這樣:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

在 CircleCI 上執行 Puppeteer

  1. 從設定中的 NodeJS 映像檔開始。yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. 您可能需要使用 apt-get 等依附元件 (例如 libXtst6) 安裝,因此請使用 3treeslight/puppeteer 或 Bb (instructions),或是將其來源的部分內容貼到自己的設定中。
  3. 最後,如果您在透過 Jest 使用 Puppeteer,可能會遇到產生子項程序時發生錯誤:shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) 這可能是由於 Jest 自動偵測:在整部機器 (36) 上的程序數量,而非容器允許的數量 (2)。如要修正這個問題,請在測試指令中設定 jest --maxWorkers=2

在 Docker 中執行 Puppeteer

在 Docker 中開啟無頭 Chrome 並不容易。Puppeteer 安裝的隨附的 Chromium 缺少必要的共用程式庫依附元件。

如要修正這個問題,您需要在 Dockerfile 中安裝缺少的依附元件和最新的 Chromium 套件:

FROM node:14-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-stable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm init -y &&  \
    npm i puppeteer \
    # Add user so we don't need --no-sandbox.
    # same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules \
    && chown -R pptruser:pptruser /package.json \
    && chown -R pptruser:pptruser /package-lock.json

# Run everything after as non-privileged user.
USER pptruser

CMD ["google-chrome-stable"]

建構容器:

docker build -t puppeteer-chrome-linux .

透過將 node -e "<yourscript.js content as a string>" 做為指令傳遞,以執行容器:

 docker run -i --init --rm --cap-add=SYS_ADMIN \
   --name puppeteer-chrome puppeteer-chrome-linux \
   node -e "`cat yourscript.js`"

我們在 https://github.com/ebidel/try-puppeteer 有完整範例,說明如何從在 App Engine Flex (Node) 上執行的網路伺服器執行這個 Dockerfile。

在阿爾卑斯山跑步

Alpine 支援的最新 Chromium 套件為 100,與 Puppeteer v13.5.0 相對應。

Dockerfile 範例:

FROM alpine

# Installs latest Chromium (100) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn

...

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Puppeteer v13.5.0 works with Chromium 100.
RUN yarn add puppeteer@13.5.0

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser

...

Docker 的最佳做法

根據預設,Docker 會執行具有 64 MB /dev/shm 共用記憶體空間的容器。這對 Chrome 而言通常過小,並會導致 Chrome 在轉譯大型網頁時異常終止。如要修正,請使用 docker run --shm-size=1gb 執行容器,增加 /dev/shm 的大小。自 Chrome 65 版起,現在已不再需要使用。請改為使用 --disable-dev-shm-usage 標記啟動瀏覽器:

const browser = await puppeteer.launch({
  args: ['--disable-dev-shm-usage'],
});

這會將共用記憶體檔案寫入 /tmp,而不是 /dev/shm。查看 crbug.com/736452

啟動 Chrome 時,畫面是否出現其他怪異錯誤?在本機開發時,請嘗試使用 docker run --cap-add=SYS_ADMIN 執行容器。由於 Dockerfile 將 pptr 使用者新增為非特殊權限使用者,因此可能無法擁有所有必要權限。

如果您發現多個 Chrome 程序持續執行,建議您確認 dumb-init。對使用 PID=1 的程序進行特殊處理,在某些情況下 (例如使用 Docker) 很難正確終止 Chrome。

在雲端執行 Puppeteer

在 Google App Engine 上

App Engine Standard 環境的 Node.js 執行階段隨附執行 Headless Chrome 所需的所有系統套件。

如要使用 puppeteer,請在 package.json 中將模組列為依附元件,然後部署至 Google App Engine。請參閱官方教學課程,進一步瞭解如何在 App Engine 上使用 puppeteer

在 Google Cloud Functions 上

Google Cloud Functions 的 Node.js 10 執行階段隨附執行 Headless Chrome 所需的所有系統套件。

如要使用 puppeteer,請在 package.json 中將模組列為依附元件,然後使用 nodejs10 執行階段將函式部署至 Google Cloud Functions。

在 Google Cloud Run 中執行 Puppeteer

Google Cloud Run 的預設 Node.js 執行階段不包含執行 Headless Chrome 所需的系統套件。設定自己的 Dockerfile加入缺少的依附元件

在 Heroku 上

在 Heroku 上執行 Puppeteer 需要 Heroku 為您啟動的 Linux 包裝盒中不包含某些額外的依附元件。如要在部署時新增依附元件,請依序前往「設定」>「Buildpacks」,將 Puppeteer Heroku 建構包新增至應用程式的建構包清單。

建構包的網址為 https://github.com/jontewks/puppeteer-heroku-buildpack

請務必在啟動 Puppeteer 時使用 '--no-sandbox' 模式。實作方法是將引數做為引數傳遞至 .launch() 呼叫:puppeteer.launch({ args: ['--no-sandbox'] });

點按「新增建構包」後,請將該網址貼到輸入欄位中,然後按一下「儲存」。在下次部署時,您的應用程式也會安裝 Puppeteer 必須執行的依附元件。

如要轉譯中文、日文或韓文字元,您可能需要使用 buildpack 搭配其他字型檔案,例如 https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

另外,還有一個 @timleland 提供的指南,其中包含專案範例。

在 AWS Lambda 中

AWS Lambda 的部署套件大小限制到 50 MB 以內。在 Lambda 上執行無頭 Chrome (以及 Puppeteer) 時,這會造成挑戰。這個社群彙整了幾項針對此問題提供的資源:

執行 Amazon-Linux 的 AWS EC2 執行個體

如果您的 CI/CD 管道中有執行 amazon-linux 的 EC2 執行個體,並且想要在 amazon-linux 中執行 Puppeteer 測試,請按照下列步驟操作。

  1. 如要安裝 Chromium,您必須先啟用 amazon-linux-extras,這是 EPEL (Enterprise Linux 適用的額外套件) 的一部分:

    sudo amazon-linux-extras install epel -y
    
  2. 接著,請安裝 Chromium:

    sudo yum install -y chromium
    

現在,Puppeteer 可以啟動 Chromium 來執行測試。如果您並未啟用 EPEL 並繼續安裝 npm install 的 Chromium,由於 libatk-1.0.so.0 和更多套件無法使用,Puppeteer 無法啟動 Chromium。

程式碼轉譯問題

如果您使用的是 babel 或 TypeScript 等 JavaScript 轉譯器,可能就無法使用非同步函式呼叫 evaluate()。這是因為雖然 puppeteer 使用 Function.prototype.toString() 將函式序列化,而傳輸工具可能會變更輸出程式碼,因而與 puppeteer 不相容。

要解決這個問題,其中一種解決方法是指示轉譯器不要混亂程式碼,例如將 TypeScript 設定為使用最新的 Eecma 版本 ("target": "es2018")。另一個解決方法是使用字串範本,而不是函式:

await page.evaluate(`(async() => {
   console.log('1');
})()`);