排查 Puppeteer 问题

无头 Chrome 无法在 Windows 上启动

某些 Chrome 政策可能会强制要求运行带有特定扩展程序的 Chrome 或 Chromium。

Puppeteer 默认传递 --disable-extensions 标志,因此当此类政策生效时,Puppeteer 无法启动。

如需解决此问题,请尝试在不使用该标志的情况下运行:

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 - Alpine 问题排查

无头 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 中的一个 bug 会破坏 extract-zip,Puppeteer 使用该模块将浏览器下载内容提取到正确位置。该 bug 在 Node.js v14.1.0 中已得到修复,因此请确保您使用的是该版本或更高版本。

设置 Chrome Linux 沙盒

为了保护主机环境免受不受信任的 Web 内容的影响,Chrome 使用了多层沙盒机制。为使此操作正常运行,应首先配置主机。如果没有良好的沙盒可供 Chrome 使用,Chrome 就会崩溃,并显示 No usable sandbox! 错误。

如果您绝对信任您在 Chrome 中打开的内容,则可以使用 --no-sandbox 参数启动 Chrome:

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

在 Chromium 中配置沙盒的方法有两种。

只有新型内核支持 Sser 命名空间克隆。无特权用户命名空间通常可以正常启用,但(未经过沙盒屏蔽的)非 Root 进程可能会引发更多内核攻击面,进而导致此类进程提升到内核特权。

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

我们在 Travis CI 上对 Puppeteer 进行了测试,直到 6.0.0 版为止,之后我们迁移到了 GitHub Actions。您可以参阅 .travis.yml (v5.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. libXtst6 等依赖项可能需要使用 apt-get 进行安装,因此请使用 threetreeslight/puppeteer orb(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 上运行

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 运行一个 /dev/shm 共享内存空间为 64MB 的容器。这对于 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 标准环境的 Node.js 运行时包含运行 Headless Chrome 所需的所有系统软件包。

如需使用 puppeteer,请在 package.json 中将该模块列为依赖项,并部署到 Google App Engine。如需详细了解如何在 App Engine 上使用 puppeteer,请参阅官方教程

适用于 Google Cloud Functions

Google Cloud Functions 的 Node.js 10 运行时包含运行无头 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 机箱中。如需在部署时添加依赖项,请在“Settings”>“Buildpacks”下将 Puppeteer Heroku buildpack 添加到应用的 Buildpack 列表中。

Buildpack 的网址为 https://github.com/jontewks/puppeteer-heroku-buildpack

确保在启动 Puppeteer 时使用 '--no-sandbox' 模式。为此,您可以将它作为参数传递给 .launch() 调用:puppeteer.launch({ args: ['--no-sandbox'] });

点击 add buildpack 时,将该网址粘贴到输入字段中,然后点击 save。 在下次部署时,您的应用还将安装 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 (Extra Packages for Enterprise Linux) 的一部分:

    sudo amazon-linux-extras install epel -y
    
  2. 接下来,安装 Chromium:

    sudo yum install -y chromium
    

现在,Puppeteer 可以启动 Chromium 来运行测试。如果您未启用 EPEL 并继续安装 Chromium 作为 npm install 的一部分,Puppeteer 无法启动 Chromium,因为 libatk-1.0.so.0 和其他软件包不可用。

代码转译问题

如果您使用的是 babel 或 TypeScript 等 JavaScript 转译器,则使用异步函数调用 evaluate() 可能不起作用。这是因为,虽然 puppeteer 使用 Function.prototype.toString() 对函数进行序列化,而转译器可能会以与 puppeteer 不兼容的方式更改输出代码。

此问题的一些解决方法是指示转译器不要混淆代码,例如,将 TypeScript 配置为使用最新的 ecma 版本 ("target": "es2018")。另一种解决方法是使用字符串模板,而不是函数:

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