Puppeteer 截圖及相關問題


Puppeteer 是 Headless Chrome 的 Node.js 封裝。通過它可方便地對頁面進行截圖,或者保存成 PDF。

鏡像的設置

因為其使用了 Chromium,其源在 Google 域上,最好設置一下 npm 從國內鏡像安裝,可解決無法安裝的問題。

推薦在項目中放置 .npmrc.yarnrc 文件來進行鏡像的設置,這樣設置只針對項目生效,不影響其他項目,同時其他人不用重復在本地設置。

這是一個整理好的 .npmrc 文件,如果使用的是 yarn,對應的 .yarnrc 文件。也可通過如下命令從 GitHub gist 下載到項目中,

# .npmrc
$ npx pkgrc

# .yarnc
$ npx pkgrc yarn

截取頁面

使用 page.screenshot() API 進行截圖的示例:

const puppeteer = require("puppeteer");

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "screenshot.png" });
await browser.close();
});

實際應用中,你需要加上等待時間,以保證頁面已經完全加載,否則截取出來的畫面是頁面半成品的樣子。

通過 page.waitFor() 可讓頁面等待指定時間,

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto('https://example.com');

// 等待一秒鍾

+ await page.waitFor(1000);

await page.screenshot({path: 'screenshot.png'});
await browser.close();
});

但這里無論你指定的時長是多少,都是比較主觀的值。頁面實際加載情況受很多因素影響,機器性能,網絡好壞等。即頁面加載完成是個無法預期的時長,所以這種方式不靠譜。我們應該使用另一個更加有保障的方式,在調用 page.goto() 時,可指定 waitUntil 參數。

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto('https://example.com’,{
+ waitUtil: 'networkidle2'
});
await page.screenshot({path: 'screenshot.png'});
await browser.close();
});

networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.
-- 來自 puppeteer 文檔中關於 waitUtil 參數的描述

networkidle2 會一直等待,直到頁面加載后同時沒有存在 2 個以上的資源請求,這個種狀態持續至少 500 ms。

此時再進行截圖,是比較保險的了。

截圖時還有個實用的參數 fullPage,一般情況下也會搭配着使用,對整個頁面進行截取。如果頁面過長,超出了當前視窗(viewport),它會自動截取超出的部分,即截取結果是長圖。這應該是大部分情況下所期望的。

await page.screenshot({ path: "screenshot.png", fullPage: true });

注意,其與 clip 參數互斥,即,如果手動指定了 clip 參數對頁面進行范圍的限定,則不能再指定 fullPage 參數。

// 💥 拋錯!
await page.screenshot({
  path: "screenshot.png",
  fullPage: true,
  clip: {
    x: 0,
    y: 0,
    width: 400,
    height: 400
  }
});

針對頁面中某個元素進行截取

如果你使用過 Chrome DevTool 中的截圖命令,或許知道,其中有一個針對元素進行截取的命令。

Chrome DevTool 中對元素進行截圖的命令

Chrome DevTool 中對元素進行截圖的命令

所以,除了對整個頁面進行截取,Chrome 還支持對頁面某個元素進行截取。通過 elementHandle .screenshot() 可針對具體元素進行截取。

這就很實用了,能夠滿足大部分自定義的需求。大多數情況下,我們只對 body 部分感興趣,通過只對 body 進行截取,就不用指定長寬而且自動排除掉 body 外多余的留白等。

const puppeteer = require("puppeteer");

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto("https://example.com", {
waitUtil: "networkidle2"
});
const element = await page.$("body");
await element.screenshot({
path: "screenshot.png"
});
});

其參數與 page.screenshot() 一樣。需要注意的是,雖說一樣,但其中是不能使用 fullPage 參數的。因為針對元素進行圖片截取已經表明是局部截圖了,與 fullPage 截取整個頁面是沖突的,但它還是會自動滾動以截取完整的這個元素, fullPage 的優點沒有丟掉。

數據的返回

生成的圖片可直接返回,也可保存成文件后返回文件地址。

其中,截圖方法 page.screenshot([options]) 的返回是 <Promise<string|Buffer>>,即生成的可能是 buffer 數據,也可以是base64 形式的字符串數據,默認為 Buffer 內容,通過設置 encoding 參數為 base64 便可得到字符串形式的截圖數據。

以 Koa 為例,binary 形式的 buffer 數據直接賦值給 ctx.body 進行返回,通過 response.attachment 可設置返回的文件名。

app.use(async ctx => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const buffer = await page.screenshot();
  await browser.close();
  ctx.response.attachment("screenshot.png");
  ctx.body = buffer;
});

字符串形式時,需要注意拿到的並不是標准的圖片 base64 格式,它只包含了數據部分,並沒有文件類型部分,即 data:image/png;base64,所以需要手動拼接后才是正確可展示的圖片。

app.use(async ctx => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const base64 = await page.screenshot({ encoding: "base64" });
  await browser.close();
  ctx.body = `<img src="data:image/png;base64,${base64}"/>`;
});

如果你是以異步接口形式返回到前端,只需要將 "data:image/png;base64,${base64}" 這部分作為數據返回即可。

當然,字符串形式下,仍然是可以返回成文件下載的形式的,

app.use(async ctx => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const base64 = await page.screenshot({ encoding: "base64" });
  await browser.close();
  ctx.response.attachment("screenshot.png");
  const image = new Buffer(base64, "base64");
  ctx.body = image;
});

PDF 的生成

通過 page.pdf([options]) 可將頁面截取成 PDF 格式。

const puppeteer = require("puppeteer");

async function run() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.emulateMedia("screen");
await page.goto("https://www.google.com/chromebook/");
await page.pdf({
path: "puppeteer.pdf",
format: "A4"
});

await browser.close();
}

run();

一般 PDF 用於打印,所以默認以 print 媒體查詢 (media query)的樣式來截取。這里調用 page.emulateMedia("screen") 顯式指定環境為 screen 而不是 print 是為了得到更加接近於頁面在瀏覽器中展示的效果。

需要注意的是,如果頁面中使用了背景圖片,上面代碼截取出來是看不到的。

截圖 PDF 時背景圖片未顯示

截圖 PDF 時背景圖片未顯示

需要設置截取時的 printBackground 參數為 true

  await page.pdf({
    path: "puppeteer.pdf",
    format: "A4",
+    printBackground: true
  });

修正后截圖的 PDF 背景圖片正常顯示

修正后截圖的 PDF 背景圖片正常顯示

一些問題

服務器字體文件問題

部署到全新的 Linux 環境時,大概率你會看到截來的圖片中中文無法顯示。

中文字體缺失的情況

中文字體缺失的情況


那是因為系統缺少中文字體,Chromium 無法正常渲染。你需要安裝中文字體,通過包管理工具或者手動下載安裝。

$ sudo apt-get install language-pack-zh*
$ sudo apt-get install chinese*

服務器上 Chromium 無法啟動的問題

在 Puppeteer 的 troubleshoting 文檔 中有對應的解決方案。

(node:24206) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!

一般是機器上缺少對應的依賴庫,安裝補上即可。Puppeteer 自帶的 Chromium 是非常純粹的,它不會安裝除了自身作為瀏覽器外的其他東西。

通過 ldd (List Dynamic Dependencies)命令可查看運行 Chromium 運行所需但缺少的 shared object dependencies。

查看缺少的依賴項

$ ldd node_modules/puppeteer/.local-chromium/linux-641577/chrome-linux/chrome | grep not
        libX11.so.6 => not found
        libX11-xcb.so.1 => not found
        libxcb.so.1 => not found
        libXcomposite.so.1 => not found
        libXcursor.so.1 => not found
        libXdamage.so.1 => not found
        libXext.so.6 => not found
        libXfixes.so.3 => not found
        libXi.so.6 => not found
        libXrender.so.1 => not found
        libXtst.so.6 => not found
        libgobject-2.0.so.0 => not found
        libglib-2.0.so.0 => not found
        libnss3.so => not found
        libnssutil3.so => not found
        libsmime3.so => not found
        libnspr4.so => not found
        libcups.so.2 => not found
        libdbus-1.so.3 => not found
        libXss.so.1 => not found
        libXrandr.so.2 => not found
        libgio-2.0.so.0 => not found
        libasound.so.2 => not found
        libpangocairo-1.0.so.0 => not found
        libpango-1.0.so.0 => not found
        libcairo.so.2 => not found
        libatk-1.0.so.0 => not found
        libatk-bridge-2.0.so.0 => not found
        libatspi.so.0 => not found
        libgtk-3.so.0 => not found
        libgdk-3.so.0 => not found
        libgdk_pixbuf-2.0.so.0 => not found

那么多,一個個搜索(因為這里例出的名稱不一定就是直接可用來安裝的名稱)安裝多麻煩。所以需要用其他方法。

以 Debian 系統為例。

tips: 可通過 $ cat /etc/os-release 查看系統信息從而判斷是什么系統。

$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

腳本安裝

通過 troubleshoting 頁面 Chrome headless doesn't launch 部分其列出的對應系統所需依賴中,將所有依賴復制出來組裝成如下的命令執行:

sudo apt-get install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 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 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

通過安裝 Chrome 來自動安裝

直接安裝一個非 Chromium 版本的 Chrome,它會把依賴自動安裝上。

Chrome 是基於 Chromium 的發行版,包括 google-chrome-stablegoogle-chrome-unstablegoogle-chrome-beta,安裝任意一個都行。

還是以 Debian 系統為例:

$ apt-get update && apt-get install google-chrome-unstable

如果直接執行上面的安裝,會報錯:

E: Unable to locate package google-chrome-unstable

這是安裝程序時的一個安全相關策略 ,需要先設置一下 apt-key

$ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -

然后設置 Chrome 的倉庫:

$ sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'

再次執行安裝便正常進行了。

安裝時可以看到會提示所需的依賴項:

安裝 Chrome 時的提示信息

$ sudo apt-get install google-chrome-unstable
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  adwaita-icon-theme at-spi2-core dbus dconf-gsettings-backend dconf-service fontconfig fontconfig-config fonts-liberation glib-networking
  glib-networking-common glib-networking-services gsettings-desktop-schemas gtk-update-icon-cache hicolor-icon-theme libappindicator3-1
  libasound2 libasound2-data libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0 libauthen-sasl-perl libavahi-client3
  libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcroco3 libcups2 libdatrie1 libdbus-1-3 libdbusmenu-glib4
  libdbusmenu-gtk3-4 libdconf1 libdrm-amdgpu1 libdrm-intel1 libdrm-nouveau2 libdrm-radeon1 libdrm2 libegl1-mesa libencode-locale-perl
  libepoxy0 libfile-basedir-perl libfile-desktopentry-perl libfile-listing-perl libfile-mimeinfo-perl libfont-afm-perl libfontconfig1
  libfontenc1 libgbm1 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-common libgl1-mesa-dri libgl1-mesa-glx libglapi-mesa libglib2.0-0 libglib2.0-data
  libgraphite2-3 libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libhtml-form-perl libhtml-format-perl libhtml-parser-perl
  libhtml-tagset-perl libhtml-tree-perl libhttp-cookies-perl libhttp-daemon-perl libhttp-date-perl libhttp-message-perl
  libhttp-negotiate-perl libice6 libindicator3-7 libio-html-perl libio-socket-ssl-perl libipc-system-simple-perl libjbig0 libjpeg62-turbo
  libjson-glib-1.0-0 libjson-glib-1.0-common liblcms2-2 libllvm3.9 liblwp-mediatypes-perl liblwp-protocol-https-perl libmailtools-perl
  libnet-dbus-perl libnet-http-perl libnet-smtp-ssl-perl libnet-ssleay-perl libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0
  libpangoft2-1.0-0 libpciaccess0 libpixman-1-0 libproxy1v5 librest-0.7-0 librsvg2-2 librsvg2-common libsensors4 libsm6 libsoup-gnome2.4-1
  libsoup2.4-1 libthai-data libthai0 libtie-ixhash-perl libtiff5 libtimedate-perl libtxc-dxtn-s2tc libu2f-udev liburi-perl
  libwayland-client0 libwayland-cursor0 libwayland-egl1-mesa libwayland-server0 libwww-perl libwww-robotrules-perl libx11-6 libx11-data
  libx11-protocol-perl libx11-xcb1 libxau6 libxaw7 libxcb-dri2-0 libxcb-dri3-0 libxcb-glx0 libxcb-present0 libxcb-render0 libxcb-shape0
  libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxdmcp6 libxext6 libxfixes3 libxft2 libxi6
  libxinerama1 libxkbcommon0 libxml-parser-perl libxml-twig-perl libxml-xpathengine-perl libxml2 libxmu6 libxmuu1 libxpm4 libxrandr2
  libxrender1 libxshmfence1 libxss1 libxt6 libxtst6 libxv1 libxxf86dga1 libxxf86vm1 perl-openssl-defaults sgml-base shared-mime-info
  x11-common x11-utils x11-xserver-utils xdg-user-dirs xdg-utils xkb-data xml-core

按[Y] 確認即可。

有了這些依賴,Puppeteer 中的 Chromium 便可運行了。

$ google-chrome-unstable --version
Google Chrome 75.0.3745.4 dev
$ ldd node_modules/puppeteer/.local-chromium/linux-641577/chrome-linux/chrome | grep not

google-chrome-unstable --version 正常輸出版本號表示安裝成功,再次檢查 not found 的依賴項輸出為空。

我們的目的只是安裝依賴,所以裝完后可移除 Chome。apt-get remove google-chrome-unstable 時會自動列出其依賴項,就像安裝時一樣。后續如果機器上不再需要 Chromium 了可通過 apt-get autoremove 來清理。

卸載 Chrome 時的提示信息

$ sudo apt-get remove google-chrome-unstable
...
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  adwaita-icon-theme at-spi2-core dconf-gsettings-backend dconf-service fontconfig fontconfig-config fonts-liberation glib-networking
  glib-networking-common glib-networking-services gsettings-desktop-schemas gtk-update-icon-cache hicolor-icon-theme libappindicator3-1
  libasound2 libasound2-data libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0 libauthen-sasl-perl libavahi-client3
  libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcroco3 libcups2 libdatrie1 libdbusmenu-glib4
  libdbusmenu-gtk3-4 libdconf1 libdrm-amdgpu1 libdrm-intel1 libdrm-nouveau2 libdrm-radeon1 libdrm2 libegl1-mesa libencode-locale-perl
  libepoxy0 libfile-basedir-perl libfile-desktopentry-perl libfile-listing-perl libfile-mimeinfo-perl libfont-afm-perl libfontconfig1
  libfontenc1 libgbm1 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-common libgl1-mesa-dri libgl1-mesa-glx libglapi-mesa libglib2.0-0 libglib2.0-data
  libgraphite2-3 libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libhtml-form-perl libhtml-format-perl libhtml-parser-perl
  libhtml-tagset-perl libhtml-tree-perl libhttp-cookies-perl libhttp-daemon-perl libhttp-date-perl libhttp-message-perl
  libhttp-negotiate-perl libice6 libicu57 libindicator3-7 libio-html-perl libio-socket-ssl-perl libipc-system-simple-perl libjbig0
  libjpeg62-turbo libjson-glib-1.0-0 libjson-glib-1.0-common liblcms2-2 libllvm3.9 liblwp-mediatypes-perl liblwp-protocol-https-perl
  libmailtools-perl libnet-dbus-perl libnet-http-perl libnet-smtp-ssl-perl libnet-ssleay-perl libnspr4 libnss3 libpango-1.0-0
  libpangocairo-1.0-0 libpangoft2-1.0-0 libpciaccess0 libpixman-1-0 libproxy1v5 librest-0.7-0 librsvg2-2 librsvg2-common libsensors4 libsm6
  libsoup-gnome2.4-1 libsoup2.4-1 libthai-data libthai0 libtie-ixhash-perl libtiff5 libtimedate-perl libtxc-dxtn-s2tc libu2f-udev
  liburi-perl libuv1 libwayland-client0 libwayland-cursor0 libwayland-egl1-mesa libwayland-server0 libwww-perl libwww-robotrules-perl
  libx11-6 libx11-data libx11-protocol-perl libx11-xcb1 libxau6 libxaw7 libxcb-dri2-0 libxcb-dri3-0 libxcb-glx0 libxcb-present0
  libxcb-render0 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxdmcp6 libxext6
  libxfixes3 libxft2 libxi6 libxinerama1 libxkbcommon0 libxml-parser-perl libxml-twig-perl libxml-xpathengine-perl libxml2 libxmu6 libxmuu1
  libxpm4 libxrandr2 libxrender1 libxshmfence1 libxss1 libxt6 libxtst6 libxv1 libxxf86dga1 libxxf86vm1 perl-openssl-defaults sgml-base
  shared-mime-info x11-common x11-utils x11-xserver-utils xdg-user-dirs xdg-utils xkb-data xml-core
Use 'sudo apt autoremove' to remove them.
The following packages will be REMOVED:
  google-chrome-unstable
0 upgraded, 0 newly installed, 1 to remove and 25 not upgraded.
After this operation, 213 MB disk space will be freed.
Do you want to continue? [Y/n]
(Reading database ... 71778 files and directories currently installed.)
Removing google-chrome-unstable (75.0.3745.4-1) ...
Processing triggers for mime-support (3.60) ...
Processing triggers for man-db (2.7.6.1-2) ...

sandbox 的問題

Linux 上 Puppeteer 啟動 Chromium 時可能會看到如下的錯誤提示:

[0402/152925.182431:ERROR:zygote_host_impl_linux.cc(89)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.

錯誤信息已經很明顯,所以在啟動時加上 --no-sandbox 參數即可。

const browser = await puppeteer.launch({ args: ["--no-sandbox"] });

但考慮到安全問題,Puppeteer 是強烈不建議在無沙盒環境下運行,除非加載的頁面其內容是絕對可信的。

如果需要設置在沙盒中運行,可參考文檔中的兩種方法

相關資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM