Risolvere i problemi relativi a Puppeteer

Il Chrome headless non si avvia su Windows

Alcuni criteri di Chrome potrebbero applicare l'esecuzione di Chrome o Chromium con determinate estensioni.

Puppeteer passa il flag --disable-extensions per impostazione predefinita, quindi non viene avviato quando questi criteri sono attivi.

Per risolvere il problema, prova a eseguire il comando senza il flag:

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

Contesto: problema 3681.

Il Chrome headless non viene lanciato su UNIX

Assicurati che siano installate tutte le dipendenze necessarie. Puoi eseguire ldd chrome | grep not su una macchina Linux per verificare quali dipendenze mancano.

Dipendenze 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

Dipendenze 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

Dopo aver installato le dipendenze devi aggiornare la libreria nss utilizzando questo comando

yum update nss -y

Dai un'occhiata alle discussioni:

  • #290 - Risoluzione dei problemi relativi a Debian
  • #391. Risoluzione dei problemi di CentOS
  • #379 - Risoluzione dei problemi relativi ad Alpi

Chrome headless disattiva il compositing GPU

Chrome e Chromium richiedono --use-gl=egl per attivare l'accelerazione GPU in modalità headless.

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

Chrome è stato scaricato, ma non si avvia su Node.js

Se quando provi ad avviare Chromium viene visualizzato un errore simile al seguente:

(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

Significa che il browser è stato scaricato ma non è stato possibile estrarlo correttamente. La causa più comune è un bug in Node.js v14.0.0 che ha infranto extract-zip, il modulo utilizzato da Puppeteer per estrarre i download del browser nella posizione corretta. Il bug è stato corretto in Node.js v14.1.0, quindi assicurati di eseguire questa versione o una versione successiva.

Configurare una sandbox per Chrome Linux

Per proteggere l'ambiente host da contenuti web non attendibili, Chrome utilizza più livelli di sandboxing. Affinché funzioni correttamente, è necessario prima configurare l'host. Se non esiste una sandbox valida per Chrome, si arresta in modo anomalo con l'errore No usable sandbox!.

Se ti fidi totalmente dei contenuti che apri in Chrome, puoi avviare Chrome con l'argomento --no-sandbox:

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

Esistono due modi per configurare una sandbox in Chromium.

La clonazione dello spazio dei nomi CSS è supportata solo dai kernel moderni. Gli spazi dei nomi degli utenti senza privilegi sono generalmente abilitati, ma possono creare una maggiore superficie di attacco del kernel (senza sandbox) per aumentare i privilegi del kernel.

sudo sysctl -w kernel.unprivileged_userns_clone=1

[alternativa] Configura la sandbox di Setuid

La sandbox di setuid è un file eseguibile autonomo che si trova accanto al dispositivo Chromium scaricato da Puppeteer. Puoi riutilizzare lo stesso eseguibile sandbox per diverse versioni di Chromium, pertanto le seguenti operazioni possono essere eseguite una sola volta per ogni ambiente host:

# 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

Potresti voler esportare la variabile env CHROME_DEVEL_SANDBOX per impostazione predefinita. In questo caso, aggiungi quanto segue a ~/.bashrc o .zshenv:

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

Esegui Puppeteer su Travis CI

Abbiamo eseguito i nostri test per Puppeteer su Travis CI fino alla versione 6.0.0, dopodiché abbiamo eseguito la migrazione a GitHub Actions. Puoi vedere .travis.yml (v5.5.0) come riferimento.

Di seguito sono riportate alcune best practice.

  • Per eseguire Chromium in modalità non headless, devi avviare il servizio xvfb
  • Viene eseguito su Xenial Linux su Travis per impostazione predefinita
  • Esegue npm install per impostazione predefinita
  • node_modules è memorizzato nella cache per impostazione predefinita

.travis.yml potrebbe avere il seguente aspetto:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

Esegui Puppeteer su CircleCI

  1. Inizia con un'immagine NodeJS nella configurazione. yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. Probabilmente è necessario che le dipendenze come libXtst6 debbano essere installate con apt-get, quindi utilizza l'orb Threetreeslight/puppeteer (instructions) o incolla parti della sua origine nella tua configurazione.
  3. Infine, se utilizzi Puppeteer tramite Jest, potresti riscontrare un errore durante la generazione di processi secondari: shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) Ciò è probabilmente causato dal fatto che Jest rileva automaticamente il numero di processi sull'intera macchina (36) anziché il numero consentito al tuo container (2). Per risolvere il problema, imposta jest --maxWorkers=2 nel comando di test.

Esegui Puppeteer in Docker

L'avvio e l'esecuzione di Chrome headless in Docker può essere complicato. Il Chromium in bundle installato da Puppeteer non ha le dipendenze di libreria condivisa necessarie.

Per risolvere il problema, devi installare le dipendenze mancanti e il pacchetto Chromium più recente nel 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"]

Crea il container:

docker build -t puppeteer-chrome-linux .

Esegui il container passando node -e "<yourscript.js content as a string>" come comando:

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

Puoi trovare un esempio completo all'indirizzo https://github.com/ebidel/try-puppeteer che mostra come eseguire questo Dockerfile da un server web in esecuzione su App Engine Flex (Node).

Corsa su alpino

Il più recente pacchetto di Chromium supportato su Alpine è 100, che corrisponde a Puppeteer v13.5.0.

Dockerfile di esempio:

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

...

Best practice con Docker

Per impostazione predefinita, Docker esegue un container con uno spazio di memoria condiviso di /dev/shm pari a 64 MB. In genere questa dimensione è in genere troppo piccola per Chrome e causa l'arresto anomalo di Chrome in caso di rendering di pagine di grandi dimensioni. Per risolvere il problema, esegui il container con docker run --shm-size=1gb per aumentare le dimensioni di /dev/shm. A partire da Chrome 65, questo passaggio non è più necessario. Avvia invece il browser con il flag --disable-dev-shm-usage:

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

I file di memoria condivisi verranno scritti in /tmp anziché in /dev/shm. Visita la pagina crbug.com/736452.

Noti altri errori strani quando avvii Chrome? Prova a eseguire il container con docker run --cap-add=SYS_ADMIN durante lo sviluppo locale. Poiché il Dockerfile aggiunge un utente pptr come utente senza privilegi, potrebbe non disporre di tutti i privilegi necessari.

dumb-init vale la pena provare se stai sperimentando molti processi di zombi che Chrome non riesce a guardare. Per i processi con PID=1 viene applicato un trattamento speciale, che rende difficile terminare correttamente Chrome in alcuni casi (ad esempio con Docker).

Esegui Puppeteer nel cloud

Su Google App Engine

Il runtime Node.js dell'ambiente standard di App Engine include tutti i pacchetti di sistema necessari per eseguire Chrome senza testa.

Per utilizzare puppeteer, elenca il modulo come dipendenza in package.json ed esegui il deployment in Google App Engine. Scopri di più sull'utilizzo di puppeteer in App Engine seguendo il tutorial ufficiale.

Su Google Cloud Functions

Il runtime Node.js 10 di Google Cloud Functions include tutti i pacchetti di sistema necessari per eseguire Chrome headless.

Per utilizzare puppeteer, elenca il modulo come dipendenza in package.json ed esegui il deployment della funzione in Google Cloud Functions utilizzando il runtime nodejs10.

Esegui Puppeteer su Google Cloud Run

Il runtime Node.js predefinito di Google Cloud Run non include i pacchetti di sistema necessari per eseguire Chrome headless. Configura il tuo Dockerfile e includi le dipendenze mancanti.

Su Heroku

L'esecuzione di Puppeteer su Heroku richiede alcune dipendenze aggiuntive che non sono incluse nella confezione Linux che Heroku avvia per te. Per aggiungere le dipendenze al deployment, aggiungi il buildpack Puppeteer Heroku all'elenco di buildpack per la tua app in Impostazioni > Buildpack.

L'URL del buildpack è https://github.com/jontewks/puppeteer-heroku-buildpack

Assicurati di utilizzare la modalità '--no-sandbox' quando avvii Puppeteer. Puoi farlo passandolo come argomento alla tua chiamata .launch(): puppeteer.launch({ args: ['--no-sandbox'] });.

Quando fai clic su add buildpack, incolla l'URL nell'input e fai clic su salva. Al successivo deployment, l'app installerà anche le dipendenze necessarie per l'esecuzione di Puppeteer.

Se devi eseguire il rendering di caratteri cinesi, giapponesi o coreani, potresti dover utilizzare un buildpack con file di caratteri aggiuntivi come https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

C'è anche un'altra guida di @timleland che include un progetto di esempio.

Su AWS Lambda

AWS Lambda limita le dimensioni dei pacchetti di deployment a circa 50 MB. Ciò rappresenta una sfida per l'esecuzione di Chrome (e quindi Puppeteer) headless su Lambda. La community ha raccolto alcune risorse per risolvere questi problemi:

Istanza AWS EC2 che esegue Amazon-Linux

Se hai un'istanza EC2 che esegue amazon-linux nella pipeline CI/CD e vuoi eseguire i test Puppeteer in amazon-linux, segui questi passaggi.

  1. Per installare Chromium, devi prima abilitare amazon-linux-extras, che fa parte di EPEL (Extra Packages for Enterprise Linux):

    sudo amazon-linux-extras install epel -y
    
  2. Dopodiché, installa Chromium:

    sudo yum install -y chromium
    

Ora Puppeteer può avviare Chromium per eseguire i tuoi test. Se non abiliti EPEL e continui a installare Chromium come parte di npm install, Puppeteer non può avviare Chromium perché libatk-1.0.so.0 e molti altri pacchetti non sono disponibili.

Problemi di traspilazione del codice

Se utilizzi un transpiler JavaScript come babel o TypeScript, la chiamata a evaluate() con una funzione asincrona potrebbe non funzionare. Questo perché puppeteer usa Function.prototype.toString() per serializzare le funzioni, mentre i transpiler potrebbero modificare il codice di output in modo che non sia compatibile con puppeteer.

Alcune soluzioni alternative a questo problema potrebbero essere indicare al transpiler di non confondere il codice, ad esempio configurare TypeScript per utilizzare la versione ecma più recente ("target": "es2018"). Un'altra soluzione alternativa potrebbe essere utilizzare modelli di stringa anziché funzioni:

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