แก้ปัญหา Puppeteer

Chrome แบบ Headless ไม่เปิดขึ้นใน Windows

นโยบาย Chrome บางรายการอาจบังคับใช้ Chrome หรือ Chromium กับส่วนขยายบางรายการ

Puppeteer ส่ง Flag --disable-extensions โดยค่าเริ่มต้น ดังนั้นจึงไม่สามารถเปิดใช้ได้เมื่อนโยบายดังกล่าวทำงานอยู่

ในการแก้ปัญหานี้ ให้ลองเรียกใช้โดยไม่มี Flag:

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

บริบท: ปัญหา 3681

Chrome แบบ Headless ไม่เปิดตัวบน UNIX

ตรวจสอบว่าได้ติดตั้งทรัพยากร Dependency ที่จำเป็นทั้งหมดแล้ว คุณสามารถเรียกใช้ ldd chrome | grep not บนเครื่อง Linux ได้เพื่อตรวจสอบว่าทรัพยากร Dependency ใดหายไป

ทรัพยากร 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

ทรัพยากร Dependency ของ 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 แบบ Headless ปิดใช้การประสาน 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 จริงๆ คุณสามารถเปิด Chrome ด้วยอาร์กิวเมนต์ --no-sandbox ได้ดังนี้

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

การกำหนดค่าแซนด์บ็อกซ์ใน Chromium ทำได้ 2 วิธี

การโคลนเนมสเปซของ Sser ใช้ได้กับเคอร์เนลสมัยใหม่เท่านั้น โดยทั่วไปเนมสเปซของผู้ใช้ที่ไม่มีสิทธิ์มักจะเปิดใช้ได้ แต่สามารถเปิดพื้นที่การโจมตีเคอร์เนลได้มากขึ้นสำหรับกระบวนการที่ไม่ใช่รูท (ไม่ใช่แซนด์บ็อกซ์) เพื่อยกระดับสิทธิ์เคอร์เนล

sudo sysctl -w kernel.unprivileged_userns_clone=1

[ทางเลือก] ตั้งค่าแซนด์บ็อกซ์ของ setuid

แซนด์บ็อกซ์ของ setuid เป็นไฟล์ปฏิบัติการแบบสแตนด์อโลนและอยู่ถัดจาก Chromium ที่ Puppeteer ดาวน์โหลด คุณจะนำไฟล์ดำเนินการของแซนด์บ็อกซ์เดียวกันมาใช้ซ้ำสำหรับ 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

คุณอาจต้องส่งออกตัวแปร env CHROME_DEVEL_SANDBOX โดยค่าเริ่มต้น ในกรณีนี้ ให้เพิ่มค่าต่อไปนี้ลงใน ~/.bashrc หรือ .zshenv

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

วิ่งหุ่นเชิดใน Travis CI

เราทำการทดสอบ Puppeteer บน Travis CI จนถึงเวอร์ชัน 6.0.0 หลังจากนั้นเราก็ย้ายข้อมูลไปยัง GitHub Actions คุณดู .travis.yml (v5.5.0) เป็นข้อมูลอ้างอิงได้

ตัวอย่างของแนวทางปฏิบัติแนะนำมีดังนี้

  • ควรเปิดบริการ xvfb เพื่อเรียกใช้ Chromium ในโหมดที่ไม่มีส่วนหัว
  • ทำงานบน Xenial Linux บน Travis ได้โดยค่าเริ่มต้น
  • เรียกใช้ npm install โดยค่าเริ่มต้น
  • node_modules ถูกแคชไว้โดยค่าเริ่มต้น

.travis.yml อาจมีลักษณะเช่นนี้

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

เรียกใช้ Puppeteer ใน CircleCI

  1. เริ่มต้นด้วยอิมเมจ NodeJS ในการกำหนดค่า yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. ทรัพยากร Dependency อย่าง libXtst6 อาจต้องติดตั้งด้วย apt-get ดังนั้นให้ใช้ Threetreeslight/puppeteer orb (instructions) หรือวางส่วนต่างๆ ของแหล่งที่มาลงในการกำหนดค่าของคุณเอง
  3. สุดท้าย หากใช้ Puppeteer ผ่าน Jest คุณอาจพบข้อผิดพลาดในการสร้างโปรเซสย่อย: 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

การทำให้ Chrome แบบไม่มีส่วนหัวทำงานใน Docker อาจเป็นเรื่องยาก Chromium ที่รวมอยู่ในชุดที่ Puppeteer ติดตั้งไม่มีทรัพยากร Dependency ของไลบรารีที่ใช้ร่วมกันที่จำเป็น

ในการแก้ไขปัญหา คุณจะต้องติดตั้งทรัพยากร Dependency ที่ขาดหายไปและแพ็กเกจ Chromium ล่าสุดใน 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"]

สร้างคอนเทนเนอร์ด้วยคำสั่งต่อไปนี้

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 ที่แสดงวิธีเรียกใช้ Dockerfile นี้จากเว็บเซิร์ฟเวอร์ที่ทำงานบน App Engine Flex (Node)

วิ่งบนเทือกเขาแอลป์

แพ็กเกจ Chromium ใหม่ล่าสุด ที่รองรับใน Alpine คือ 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 ที่ 64 MB ซึ่งโดยปกติจะมีขนาดเล็กเกินไปสำหรับ 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 เป็นผู้ใช้ที่ไม่มีสิทธิ์ จึงอาจไม่มีสิทธิ์ที่จำเป็นทั้งหมด

dumb-init ก็คุ้มค่าที่จะดูนะคะ หากคุณ ประสบกับกระบวนการเกี่ยวกับซอมบี้มากมายใน Chrome ที่ยังรออยู่ กระบวนการใช้ PID=1 มีการดูแลเป็นพิเศษ ซึ่งทำให้สิ้นสุด Chrome อย่างถูกต้องได้ยากในบางกรณี (เช่น เมื่อใช้ Docker)

วิ่งเชิดหุ่นกระบอกบนก้อนเมฆ

ใน Google App Engine

รันไทม์ Node.js ของสภาพแวดล้อมมาตรฐานของ App Engine มาพร้อมกับแพ็กเกจระบบทั้งหมดที่จำเป็นต่อการเรียกใช้ Chrome แบบ Headless

หากต้องการใช้ puppeteer ให้ระบุโมดูลนี้เป็นทรัพยากร Dependency ใน package.json และทำให้ใช้งาน Google App Engine ได้ อ่านเพิ่มเติมเกี่ยวกับการใช้ puppeteer บน App Engine โดยทำตามบทแนะนำอย่างเป็นทางการ

บน Google Cloud Function

รันไทม์ Node.js 10 ของ Google Cloud Functions มาพร้อมกับแพ็กเกจระบบทั้งหมดที่จำเป็นต่อการเรียกใช้ Chrome แบบ Headless

หากต้องการใช้ puppeteer ให้ระบุโมดูลเป็นทรัพยากร Dependency ใน package.json และทำให้ฟังก์ชันใช้งานได้ใน Google Cloud Functions โดยใช้รันไทม์ nodejs10

เรียกใช้ Puppeteer บน Google Cloud Run

รันไทม์ Node.js เริ่มต้นของ Google Cloud Run ไม่ได้มาพร้อมกับแพ็กเกจระบบที่จำเป็นต่อการเรียกใช้ Chrome แบบ Headless ตั้งค่า Dockerfile ของคุณเองและรวมทรัพยากร Dependency ที่ขาดหายไป

บน Heroku

การเรียกใช้ Puppeteer บน Heroku ต้องใช้ทรัพยากร Dependency เพิ่มเติมบางอย่างที่ไม่มีให้ในกล่อง Linux ที่ Heroku สร้างให้คุณ หากต้องการเพิ่มทรัพยากร Dependency ในการทำให้ใช้งานได้ ให้เพิ่มชุดบิลด์ Puppeteer Heroku ลงในรายการชุดบิลด์สำหรับแอปในส่วนการตั้งค่า > Buildpack

URL สำหรับชุดบิลด์คือ https://github.com/jontewks/puppeteer-heroku-buildpack

ตรวจสอบว่าคุณใช้โหมด '--no-sandbox' เมื่อเปิด Puppeteer ซึ่งทำได้โดยการส่งต่อเป็นอาร์กิวเมนต์ไปยังการเรียก .launch() ของคุณ: puppeteer.launch({ args: ['--no-sandbox'] });

เมื่อคุณคลิกเพิ่มชุดการสร้าง ให้วาง URL นั้นลงในอินพุต แล้วคลิกบันทึก ในการปรับใช้ครั้งถัดไป แอปของคุณจะติดตั้งทรัพยากร Dependency ที่ Puppeteer ต้องการเรียกใช้ด้วย

หากต้องการแสดงผลอักขระภาษาจีน ญี่ปุ่น หรือเกาหลี คุณอาจต้องใช้ชุดบิลด์ที่มีไฟล์แบบอักษรเพิ่มเติม เช่น https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

นอกจากนี้ยังมีคำแนะนำจาก @timleland อีก 1 รายการที่มีโปรเจ็กต์ตัวอย่าง

บน AWS Lambda

ขีดจำกัดของแพ็กเกจการติดตั้งใช้งาน AWS Lambda อยู่ที่ประมาณ 50 MB สิ่งนี้นำมาซึ่งความท้าทายสำหรับการวิ่ง Chrome แบบ Headless (ซึ่งแน่นอนว่าเป็น Puppeteer) ใน Lambda ชุมชนได้รวบรวมแหล่งข้อมูล จำนวนหนึ่งที่ช่วยแก้ปัญหานี้

อินสแตนซ์ AWS EC2 ที่เรียกใช้ Amazon-Linux

หากคุณมีอินสแตนซ์ EC2 ที่เรียกใช้ amazon-linux ในไปป์ไลน์ CI/CD และต้องการเรียกใช้การทดสอบ Puppeteer ใน amazon-linux ให้ทำตามขั้นตอนต่อไปนี้

  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 และติดตั้ง Chromium ต่อไปโดยเป็นส่วนหนึ่งของ npm install Puppeteer ไม่สามารถเปิด Chromium ได้เนื่องจาก libatk-1.0.so.0 และแพ็กเกจอื่นๆ ไม่พร้อมใช้งาน

ปัญหาการเปลี่ยนรูปแบบโค้ด

หากคุณกำลังใช้ตัวเปลี่ยนรูปแบบ JavaScript เช่น Babel หรือ TypeScript การเรียกใช้ evaluate() ด้วยฟังก์ชันอะซิงโครนัสอาจไม่ทำงาน เนื่องจากในขณะที่ puppeteer ใช้ Function.prototype.toString() ในการเรียงลําดับฟังก์ชันในขณะที่ตัวแปลงสัญญาณอาจเปลี่ยนโค้ดเอาต์พุตในลักษณะที่เข้ากันไม่ได้กับ puppeteer

วิธีแก้ปัญหานี้คือการบอกกับตัวแปลงรหัสว่าอย่ายุ่งกับโค้ด เช่น กำหนดค่า TypeScript ให้ใช้ ecma เวอร์ชันล่าสุด ("target": "es2018") วิธีแก้ปัญหาเฉพาะหน้าคือการใช้เทมเพลตสตริง แทนฟังก์ชันต่างๆ ดังนี้

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