Khắc phục sự cố về Puppeteer

Chrome không có giao diện người dùng không chạy trên Windows

Một số chính sách của Chrome có thể thực thi việc chạy Chrome hoặc Chromium với một số Tiện ích nhất định.

Theo mặc định, Puppeteer sẽ truyền cờ --disable-extensions. Do đó, công cụ này sẽ không chạy được khi các chính sách đó đang hoạt động.

Để xử lý vấn đề này, hãy thử chạy mà không cần cờ:

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

Ngữ cảnh: vấn đề 3681.

Chrome không có giao diện người dùng không ra mắt trên UNIX

Hãy đảm bảo bạn đã cài đặt tất cả các phần phụ thuộc cần thiết. Bạn có thể chạy ldd chrome | grep not trên máy Linux để kiểm tra xem phần phụ thuộc nào bị thiếu.

Phần phụ thuộc 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

Phần phụ thuộc 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

Sau khi cài đặt phần phụ thuộc, bạn cần cập nhật thư viện nss bằng lệnh này

yum update nss -y

Xem các cuộc thảo luận:

  • #290 - Khắc phục sự cố Debian
  • #391 - Khắc phục sự cố CentOS
  • #379 - Khắc phục sự cố trên dãy núi

Chrome không có giao diện người dùng tắt tính năng tổng hợp GPU

Chrome và Chromium yêu cầu --use-gl=egl bật tính năng tăng tốc GPU ở chế độ không có giao diện người dùng.

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

Đã tải Chrome xuống nhưng không chạy được trên Node.js

Nếu bạn gặp lỗi như sau khi cố gắng khởi chạy 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

Điều này có nghĩa là trình duyệt đã được tải xuống nhưng không trích xuất được chính xác. Nguyên nhân phổ biến nhất là lỗi trong Node.js v14.0.0 đã làm hỏng extract-zip, mô-đun mà Puppeteer sử dụng để trích xuất các tệp tải xuống trình duyệt vào đúng vị trí. Lỗi này đã được khắc phục trong Node.js v14.1.0, vì vậy, hãy đảm bảo bạn đang chạy phiên bản đó trở lên.

Thiết lập hộp cát Linux của Chrome

Để bảo vệ môi trường lưu trữ khỏi nội dung không đáng tin cậy trên web, Chrome sử dụng nhiều lớp hộp cát. Để quá trình này hoạt động đúng cách, trước tiên, bạn nên định cấu hình máy chủ lưu trữ. Nếu không có hộp cát phù hợp để Chrome sử dụng, trình duyệt sẽ gặp sự cố do lỗi No usable sandbox!.

Nếu hoàn toàn tin tưởng nội dung mà bạn mở trong Chrome, bạn có thể chạy Chrome bằng đối số --no-sandbox:

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

Có 2 cách để định cấu hình hộp cát trong Chromium.

Tính năng Sao chép không gian tên Sser chỉ được hỗ trợ trên các hạt nhân hiện đại. Nhìn chung, không gian tên người dùng không có đặc quyền thì có thể bật, nhưng có thể tạo thêm nhiều khu vực tấn công nhân kernel cho các quy trình không phải quy trình gốc (không có hộp cát) để nâng cấp lên các đặc quyền nhân kernel.

sudo sysctl -w kernel.unprivileged_userns_clone=1

[thay thế] Thiết lập hộp cát setuid

Hộp cát setuid có dạng tệp thực thi độc lập và nằm bên cạnh Chromium mà Puppeteer tải xuống. Bạn có thể sử dụng lại cùng một tệp thực thi hộp cát cho nhiều phiên bản Chromium khác nhau. Vì vậy, bạn chỉ có thể thực hiện những thao tác sau đây một lần cho mỗi môi trường lưu trữ:

# 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

Theo mặc định, bạn nên xuất biến môi trường CHROME_DEVEL_SANDBOX. Trong trường hợp này, hãy thêm đoạn mã sau vào ~/.bashrc hoặc .zshenv:

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

Chạy Puppeteer trên Travis CI

Chúng tôi đã chạy thử nghiệm cho Puppeteer trên Travis CI cho đến phiên bản 6.0.0, sau đó chúng tôi chuyển sang GitHub Actions (Hành động trên GitHub). Bạn có thể tham khảo .travis.yml (phiên bản 5.5.0).

Dưới đây là một số phương pháp hay nhất:

  • Dịch vụ xvfb sẽ được chạy để chạy Chromium ở chế độ không có giao diện người dùng
  • Chạy trên Xenial Linux trên Travis theo mặc định
  • Chạy npm install theo mặc định
  • node_modules được lưu vào bộ nhớ đệm theo mặc định

.travis.yml có thể có dạng như sau:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

Chạy Puppeteer trên CircleCI

  1. Bắt đầu bằng hình ảnh NodeJS trong phần cấu hình. yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. Các phần phụ thuộc như libXtst6 có thể cần được cài đặt bằng apt-get, vì vậy, hãy sử dụng batreeslight/puppeteer (instructions) hoặc dán các phần của nguồn vào cấu hình của riêng bạn.
  3. Cuối cùng, nếu đang sử dụng Puppeteer thông qua Jest, bạn có thể gặp lỗi tạo ra lỗi các quy trình con: shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) Nguyên nhân có thể là do Jest tự động phát hiện số lượng quy trình trên toàn bộ máy (36) thay vì số được phép đối với vùng chứa (2). Để khắc phục lỗi này, hãy đặt jest --maxWorkers=2 trong lệnh kiểm thử.

Chạy Puppeteer trong Docker

Việc thiết lập và chạy Chrome không có giao diện người dùng trong Docker có thể rất phức tạp. Chromium đi kèm mà Puppeteer cài đặt bị thiếu các phần phụ thuộc thư viện chia sẻ cần thiết.

Để khắc phục, bạn cần cài đặt các phần phụ thuộc còn thiếu và gói Chromium mới nhất trong Dockerfile:

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"]

Tạo vùng chứa:

docker build -t puppeteer-chrome-linux .

Chạy vùng chứa bằng cách truyền node -e "<yourscript.js content as a string>" dưới dạng lệnh:

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

Bạn có thể xem một ví dụ đầy đủ tại https://github.com/ebidel/try-puppeteer cho thấy cách chạy Dockerfile này từ một máy chủ web chạy trên App Engine Flex (Nút).

Chạy trên dãy núi Anpơ

Gói Chromium mới nhất được hỗ trợ trên Alpine là 100, tương ứng với Puppeteer v13.5.0.

Ví dụ về 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

...

Các phương pháp hay nhất với Docker

Theo mặc định, Docker chạy một vùng chứa có dung lượng bộ nhớ dùng chung /dev/shm là 64 MB. Dung lượng này thường quá nhỏ đối với Chrome và sẽ khiến Chrome gặp sự cố khi hiển thị các trang lớn. Để khắc phục, hãy chạy vùng chứa bằng docker run --shm-size=1gb để tăng kích thước của /dev/shm. Kể từ Chrome 65, việc này không còn cần thiết nữa. Thay vào đó, hãy khởi chạy trình duyệt với cờ --disable-dev-shm-usage:

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

Thao tác này sẽ ghi các tệp bộ nhớ dùng chung vào /tmp thay vì /dev/shm. Hãy xem lại crbug.com/736452.

Bạn có thấy các lỗi lạ khác khi khởi chạy Chrome không? Hãy thử chạy vùng chứa bằng docker run --cap-add=SYS_ADMIN khi phát triển cục bộ. Vì Dockerfile thêm người dùng pptr là người dùng không có đặc quyền, nên Docker có thể không có tất cả các đặc quyền cần thiết.

dumb-init nên xem nếu bạn đang gặp phải nhiều quy trình Chrome tồn tại. Có một cách xử lý đặc biệt cho các quy trình với PID=1, khiến bạn khó chấm dứt Chrome đúng cách trong một số trường hợp (chẳng hạn như với Docker).

Chạy Puppeteer trên đám mây

Trên Google App Engine

Thời gian chạy Node.js của môi trường tiêu chuẩn App Engine đi kèm với tất cả các gói hệ thống cần thiết để chạy Chrome không có giao diện người dùng.

Để sử dụng puppeteer, hãy liệt kê mô-đun dưới dạng phần phụ thuộc trong package.json và triển khai lên Google App Engine. Tìm hiểu thêm về cách sử dụng puppeteer trên App Engine bằng cách làm theo hướng dẫn chính thức.

Trên Google Cloud Functions

Thời gian chạy Node.js 10 của Google Cloud Functions đi kèm với tất cả các gói hệ thống cần thiết để chạy Chrome không có giao diện người dùng.

Để sử dụng puppeteer, hãy liệt kê mô-đun dưới dạng phần phụ thuộc trong package.json và triển khai hàm của bạn lên Google Cloud Functions bằng thời gian chạy nodejs10.

Chạy Puppeteer trên Google Cloud Run

Môi trường thời gian chạy Node.js mặc định của Google Cloud Run không đi kèm với các gói hệ thống cần thiết để chạy Chrome không có giao diện người dùng. Thiết lập Dockerfile của riêng bạn và thêm các phần phụ thuộc còn thiếu.

Trên Heroku

Để chạy Puppeteer trên Heroku, bạn sẽ phải thêm một số phần phụ thuộc không có trong hộp Linux mà Heroku hỗ trợ cho bạn. Để thêm các phần phụ thuộc khi triển khai, hãy thêm gói bản dựng Puppeteer Heroku vào danh sách các gói bản dựng cho ứng dụng của bạn trong phần Cài đặt > Gói bản dựng.

URL của gói bản dựng là https://github.com/jontewks/puppeteer-heroku-buildpack

Nhớ dùng chế độ '--no-sandbox' khi chạy Puppeteer. Bạn có thể thực hiện việc này bằng cách truyền đối số đó dưới dạng một đối số vào lệnh gọi .launch(): puppeteer.launch({ args: ['--no-sandbox'] });.

Khi bạn nhấp vào thêm gói bản dựng, hãy dán URL đó vào mục nhập rồi nhấp vào lưu. Trong lần triển khai tiếp theo, ứng dụng của bạn cũng sẽ cài đặt các phần phụ thuộc mà Puppeteer cần để chạy.

Nếu cần hiển thị các ký tự tiếng Trung, tiếng Nhật hoặc tiếng Hàn, bạn có thể cần sử dụng gói bản dựng với các tệp phông chữ bổ sung như https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

Ngoài ra, còn có một hướng dẫn khác từ @timleland bao gồm dự án mẫu.

Trên AWS Lambda

AWS Lambda giới hạn kích thước gói triển khai ở mức ~50 MB. Điều này đặt ra những thách thức khi chạy Chrome không có giao diện người dùng (và từ đó tạo Puppeteer) trên Lambda. Cộng đồng đã tổng hợp một số tài nguyên giải quyết các vấn đề này:

Thực thể AWS EC2 chạy Amazon-Linux

Nếu bạn có một thực thể EC2 chạy Amazon-linux trong quy trình CI/CD và muốn chạy các thử nghiệm Puppeteer trong Amazon-linux, hãy làm theo các bước sau.

  1. Để cài đặt Chromium, trước tiên, bạn phải bật amazon-linux-extras. Đây là một phần của EPEL (Gói bổ sung dành cho Enterprise Linux):

    sudo amazon-linux-extras install epel -y
    
  2. Tiếp theo, hãy cài đặt Chromium:

    sudo yum install -y chromium
    

Giờ đây, Puppeteer có thể khởi chạy Chromium để chạy quy trình kiểm thử của bạn. Nếu bạn không bật EPEL và tiếp tục cài đặt Chromium trong npm install, thì Puppeteer không thể chạy Chromium do không có libatk-1.0.so.0 và nhiều gói khác.

Vấn đề về quá trình dịch mã

Nếu bạn đang sử dụng một trình chuyển mã JavaScript như babel hoặc TypeScript, thì việc gọi evaluate() bằng hàm không đồng bộ có thể không hoạt động. Điều này là do mặc dù puppeteer sử dụng Function.prototype.toString() để chuyển đổi tuần tự các hàm trong khi các trình chuyển đổi có thể thay đổi mã đầu ra theo cách không tương thích với puppeteer.

Một số phương án giải quyết vấn đề này là hướng dẫn bộ chuyển mã không làm rối mã, ví dụ: định cấu hình TypeScript để sử dụng phiên bản ecma mới nhất ("target": "es2018"). Một giải pháp khác có thể là sử dụng mẫu chuỗi thay vì các hàm:

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