Rozwiązywanie problemów z Puppeteer

Chrome bez interfejsu graficznego nie uruchamia się w systemie Windows

Niektóre zasady Chrome mogą wymuszać korzystanie z Chrome lub Chromium z określonymi rozszerzeniami.

Puppeteer przekazuje domyślnie flagę --disable-extensions, dlatego nie uruchamia się, gdy takie zasady są aktywne.

Aby obejść ten problem, spróbuj uruchomić tryb bez flagi:

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

Kontekst: problem 3681.

Chrome bez interfejsu graficznego nie uruchamia się w systemie UNIX

Zainstaluj wszystkie niezbędne zależności. Możesz uruchomić ldd chrome | grep not na komputerze z Linuksem, aby sprawdzić brakujące zależności.

Zależności 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

Zależności 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

Po zainstalowaniu zależności musisz zaktualizować bibliotekę nss za pomocą tego polecenia

yum update nss -y

Sprawdź dyskusje:

  • #290 – Rozwiązywanie problemów z Debianem
  • #391 – Rozwiązywanie problemów z CentOS
  • #379 – Rozwiązywanie problemów wg Alpejskich

Chrome bez interfejsu graficznego wyłącza komponowanie GPU

Chrome i Chromium wymagają --use-gl=egl, aby umożliwić przyspieszenie GPU w trybie bez interfejsu graficznego.

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

Przeglądarka Chrome została pobrana, ale nie uruchamia się w środowisku Node.js

Jeśli podczas próby uruchomienia Chromium widzisz błąd podobny do tego:

(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

Oznacza to, że przeglądarka została pobrana, ale nie udało się jej prawidłowo wyodrębnić. Najczęstszą przyczyną jest błąd w Node.js w wersji 14.0.0, w którym zepsuł się extract-zip – moduł Puppeteer służący do wyodrębniania pobranych plików przeglądarki we właściwe miejsce. Błąd został naprawiony w Node.js w wersji 14.1.0, więc upewnij się, że korzystasz z tej wersji lub nowszej.

Konfigurowanie piaskownicy w Chrome Linux

Aby chronić środowisko hosta przed niezaufanymi treściami z internetu, Chrome stosuje kilka warstw piaskownicy. Aby to działało prawidłowo, najpierw należy skonfigurować hosta. Jeśli nie ma odpowiedniej piaskownicy do użycia w Chrome, przeglądarka ulega awarii i wyświetla błąd No usable sandbox!.

Jeśli całkowicie ufasz treściom, które otwierasz w Chrome, możesz uruchomić Chrome, używając argumentu --no-sandbox:

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

Piaskownicę w Chromium możesz skonfigurować na 2 sposoby.

Klonowanie przestrzeni nazw sser jest obsługiwane tylko przez nowoczesne jądra. Przestrzenie nazw użytkowników bez uprawnień można zwykle włączyć, ale mogą zwiększyć ryzyko ataku jądra dla procesów innych niż root (poza piaskownicą), aby podnieść poziom uprawnień jądra.

sudo sysctl -w kernel.unprivileged_userns_clone=1

[alternative] Skonfiguruj piaskownicę identyfikatora setuid

Piaskownica setuid to samodzielny plik wykonywalny, który znajduje się obok Chromium, który jest pobierany przez Puppeteer. Ten sam plik wykonywalny piaskownicy można używać w różnych wersjach Chromium, więc te czynności można wykonać tylko raz w każdym środowisku hosta:

# 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

Warto domyślnie wyeksportować zmienną środowiskową CHROME_DEVEL_SANDBOX. W takim przypadku do ~/.bashrc lub .zshenv dodaj ten kod:

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

Uruchom Puppeteer w Travis CI

Testy platformy Puppeteer prowadziliśmy w travis CI do wersji 6.0.0, a potem przenieśliśmy się na Działania GitHub. Więcej informacji znajdziesz w .travis.yml (v5.5.0).

Poniżej przedstawiamy kilka sprawdzonych metod:

  • Aby uruchamiać Chromium w trybie bez interfejsu graficznego, należy uruchomić usługę xvfb
  • Domyślnie działa w systemie Xenial Linux w systemie Travis
  • Domyślnie działa: npm install
  • Aplikacja node_modules jest domyślnie przechowywana w pamięci podręcznej

.travis.yml może wyglądać tak:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

Uruchom Puppeteer w CircleCI

  1. Zacznij od obrazu NodeJS w konfiguracji. yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. Zależności takie jak libXtst6 prawdopodobnie wymagają instalacji apt-get, użyj więc obiektu threetreeslight/puppeteer (instructions) lub wklej fragmenty źródła we własnej konfiguracji.
  3. Jeśli używasz Puppeteer przez Jest, możesz napotkać błąd podczas rozpoczynania procesów podrzędnych: shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) Prawdopodobnie jest to spowodowane automatycznym wykrywaniem liczby procesów na całym komputerze (36), a nie liczbie dozwolonych w kontenerze (2). Aby rozwiązać ten problem, ustaw wartość jest --maxWorkers=2 w poleceniu testowym.

Uruchamianie Puppeteer w Dockerze

Skonfigurowanie i uruchomienie bez interfejsu graficznego Chrome w Docku może być trudne. W pakiecie Chromium instalowanym przez Puppeteer brakuje wymaganych zależności od zasobów wspólnych.

Aby rozwiązać ten problem, musisz zainstalować w pliku Dockerfile brakujące zależności i najnowszy pakiet 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"]

Utwórz kontener:

docker build -t puppeteer-chrome-linux .

Uruchom kontener, przekazując node -e "<yourscript.js content as a string>" jako polecenie:

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

Pełny przykład znajdziesz na https://github.com/ebidel/try-puppeteer. Dowiesz się z niego, jak uruchomić plik Dockerfile na serwerze WWW działającym w App Engine Flex (Node).

Bieganie po Alpach

Najnowszy pakiet Chromium obsługiwany w Alpine to 100, co odpowiada wersji Puppeteer 13.5.0.

Przykładowy plik 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

...

Sprawdzone metody korzystania z Dockera

Domyślnie Docker uruchamia kontener z 64 MB pamięci współdzielonej w usłudze /dev/shm. Jest on zwykle za mały dla Chrome i będzie powodować awarie Chrome podczas renderowania dużych stron. Aby rozwiązać ten problem, uruchom kontener za pomocą funkcji docker run --shm-size=1gb, aby zwiększyć rozmiar elementu /dev/shm. Od wersji Chrome 65 nie jest to już konieczne. Zamiast tego uruchom przeglądarkę z flagą --disable-dev-shm-usage:

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

Spowoduje to zapisanie plików pamięci udostępnionej w folderze /tmp zamiast /dev/shm. Wejdź na crbug.com/736452.

Czy podczas uruchamiania Chrome widzisz inne dziwne błędy? Jeśli programujesz lokalnie, spróbuj uruchomić kontener za pomocą narzędzia docker run --cap-add=SYS_ADMIN. Dockerfile dodaje użytkownika pptr jako użytkownika bez uprawnień, więc może nie mieć wszystkich niezbędnych uprawnień.

Jeśli masz do czynienia z wieloma procesami zombie w Chrome, warto sprawdzić dumb-init. Procesy z dyrektywą PID=1 są objęte specjalnym traktowaniem, które w niektórych przypadkach utrudniają prawidłowe zakończenie działania Chrome (np. za pomocą Dockera).

Uruchom Puppeteer w chmurze

W Google App Engine

Środowisko wykonawcze Node.js w standardowym środowisku App Engine zawiera wszystkie pakiety systemowe potrzebne do uruchamiania Chrome bez interfejsu graficznego.

Aby użyć funkcji puppeteer, wymień moduł jako zależność w package.json i wdróż ją w Google App Engine. Więcej informacji o korzystaniu z puppeteer w App Engine znajdziesz w oficjalnym samouczku.

W Google Cloud Functions

Środowisko wykonawcze Node.js 10 Google Cloud Functions zawiera wszystkie pakiety systemowe potrzebne do uruchamiania Chrome bez interfejsu graficznego.

Aby użyć funkcji puppeteer, wymień ten moduł jako zależność w package.json i wdróż swoją funkcję w Google Cloud Functions za pomocą środowiska wykonawczego nodejs10.

Uruchom Puppeteer w Google Cloud Run

Domyślne środowisko wykonawcze Node.js Google Cloud Run nie zawiera pakietów systemowych niezbędnych do uruchamiania Chrome bez interfejsu graficznego. Skonfiguruj własny Dockerfile i uwzględnij brakujące zależności.

W Heroku

Uruchomienie Puppeteer na Heroku wymaga dodatkowych zależności, których nie znajdziesz w pudełku z systemem Linux, który tworzy dla Ciebie Heroku. Aby dodać zależności dotyczące wdrożenia, dodaj pakiet kompilacji Puppeteer Heroku do listy pakietów kompilacji dla aplikacji w sekcji Ustawienia > Pakiety kompilacji.

Adres URL pakietu kompilacji to https://github.com/jontewks/puppeteer-heroku-buildpack

Upewnij się, że podczas uruchamiania Puppeteer używasz trybu '--no-sandbox'. Aby to zrobić, przekaż go jako argument do wywołania .launch(): puppeteer.launch({ args: ['--no-sandbox'] });.

Gdy klikniesz Dodaj pakiet kompilacji, wklej ten adres URL w polu wejściowym i kliknij Zapisz. Podczas następnego wdrożenia aplikacja zainstaluje również zależności, które musi uruchomić Puppeteer.

Jeśli chcesz renderować znaki chińskie, japońskie lub koreańskie, możesz użyć pakietu kompilacji z dodatkowymi plikami czcionek, takimi jak https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

Dostępny jest też inny przewodnik z @timleland, w którym znajdziesz przykładowy projekt.

W AWS Lambda

AWS Lambda limituje rozmiar pakietów wdrożeniowych do około 50 MB. Stanowi to wyzwanie dla uruchamiania bez interfejsu graficznego Chrome (a tym samym Puppeteer) na Lambda. Społeczność zebrała kilka zasobów, które pomagają rozwiązać te problemy:

Instancja AWS EC2 z systemem Amazon-Linux

Jeśli masz instancję EC2 z amazon-linux w swoim potoku CI/CD i chcesz uruchomić testy Puppeteer w amazon-linux, wykonaj te czynności.

  1. Aby zainstalować Chromium, musisz najpierw włączyć amazon-linux-extras, który jest częścią EPEL (Extra Packages for Enterprise Linux):

    sudo amazon-linux-extras install epel -y
    
  2. Następnie zainstaluj Chromium:

    sudo yum install -y chromium
    

Teraz Puppeteer może uruchomić Chromium i przeprowadzić testy. Jeśli nie włączysz EPEL i nadal będziesz instalować Chromium w ramach npm install, Puppeteer nie będzie mógł uruchomić Chromium z powodu niedostępności libatk-1.0.so.0 i wielu innych pakietów.

Problemy z transpilacją kodu

Jeśli korzystasz z transpilera JavaScriptu takiego jak Babel czy TypeScript, wywołanie evaluate() z funkcją asynchroniczną może nie działać. Dzieje się tak, ponieważ chociaż puppeteer używa Function.prototype.toString() do serializacji funkcji, a transpilatory mogą zmieniać kod wyjściowy w taki sposób, że jest on niezgodny z puppeteer.

Rozwiązaniem tego problemu jest poinstruowanie transpilera, aby nie zakłócił kodu. Na przykład skonfiguruj TypeScript do korzystania z najnowszej wersji ecma ("target": "es2018"). Innym obejściem problemu może być użycie szablonów ciągów tekstowych zamiast funkcji:

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