使用Headless Chrome進行頁面渲染
使用 Headless Chrome 進行頁面渲染 從屬于筆者的 Web 開發(fā)基礎(chǔ)與工程實踐系列文章,主要介紹了使用 Node.js 利用 Chrome Remote Protocol 遠程控制 Headless Chrome 渲染界面的基礎(chǔ)用法。本文涉及的參考與引用資料統(tǒng)一列舉在這里。
近日筆者在為 declarative-crawler 編寫動態(tài)頁面的蜘蛛,即在使用 declarative-crawler 爬取知乎美圖 一文中介紹的 HeadlessChromeSpider 時,需要選擇某個無界面瀏覽器以執(zhí)行 JavaScript 代碼來動態(tài)生成頁面。之前筆者往往是使用 PhantomJS 或者 Selenium 執(zhí)行動態(tài)頁面渲染,而在 Chrome 59 之后 Chrome 提供了 Headless 模式,其允許在命令行中使用 Chromium 以及 Blink 渲染引擎提供的完整的現(xiàn)代 Web 平臺特性。需要注意的是,Headless Chrome 仍然存在一定的局限,相較于 Nightmare 或 Phantom 這樣的工具, Chrome 的遠程接口仍然無法提供較好的開發(fā)者體驗。我們在下文介紹的代碼示例中也會發(fā)現(xiàn),目前我們?nèi)孕枰罅康哪0宕a進行控制。
安裝與啟動
在 Chrome 安裝完畢后我們可以利用其包體內(nèi)自帶的命令行工具啟動:
- $ chrome --headless --remote-debugging-port=9222 https://chromium.org
筆者為了部署方便,使用 Docker 鏡像來進行快速部署,如果你本地存在 Docker 環(huán)境,可以使用如下命令快速啟動:
- docker run -d -p 9222:9222 justinribeiro/chrome-headless
如果是在 Mac 下本地使用的話我們還可以創(chuàng)建命令別名:
- alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
- alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
- alias chromium="/Applications/Chromium.app/Contents/MacOS/Chromium"
如果是在 Ubuntu 環(huán)境下我們可以使用 deb 進行安裝:
- # Install Google Chrome
- # https://askubuntu.com/questions/79280/how-to-install-chrome-browser-properly-via-command-line
- sudo apt-get install libxss1 libappindicator1 libindicator7
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome*.deb # Might show "errors", fixed by next line
- sudo apt-get install -f
chrome 命令行也支持豐富的命令行參數(shù),--dump-dom 參數(shù)可以將 document.body.innerHTML 打印到標準輸出中:
- chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/
而 --print-to-pdf 標識則會將網(wǎng)頁輸出位 PDF:
- chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/
初次之外,我們也可以使用 --screenshot 參數(shù)來獲取頁面截圖:
- chrome --headless --disable-gpu --screenshot https://www.chromestatus.com/
- # Size of a standard letterhead.
- chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://www.chromestatus.com/
- # Nexus 5x
- chrome --headless --disable-gpu --screenshot --window-size=412,732 https://www.chromestatus.com/
如果我們需要更復(fù)雜的截圖策略,譬如進行完整頁面截圖則需要利用代碼進行遠程控制。
代碼控制
啟動
在上文中我們介紹了如何利用命令行來手動啟動 Chrome,這里我們嘗試使用 Node.js 來啟動 Chrome,最簡單的方式就是使用 child_process 來啟動:
- const exec = require('child_process').exec;
- function launchHeadlessChrome(url, callback) {
- // Assuming MacOSx.
- const CHROME = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';
- exec(`${CHROME} --headless --disable-gpu --remote-debugging-port=9222 ${url}`, callback);
- }
- launchHeadlessChrome('https://www.chromestatus.com', (err, stdout, stderr) => {
- ...
- });
遠程控制
這里我們使用 chrome-remote-interface 來遠程控制 Chrome ,實際上 chrome-remote-interface 是對于 Chrome DevTools Protocol 的遠程封裝,我們可以參考協(xié)議文檔了解詳細的功能與參數(shù)。使用 npm 安裝完畢之后,我們可以用如下代碼片進行簡單控制:
- const CDP = require('chrome-remote-interface');
- CDP((client) => {
- // extract domains
- const {Network, Page} = client;
- // setup handlers
- Network.requestWillBeSent((params) => {
- console.log(params.request.url);
- });
- Page.loadEventFired(() => {
- client.close();
- });
- // enable events then start!
- Promise.all([
- Network.enable(),
- Page.enable()
- ]).then(() => {
- return Page.navigate({url: 'https://github.com'});
- }).catch((err) => {
- console.error(err);
- client.close();
- });
- }).on('error', (err) => {
- // cannot connect to the remote endpoint
- console.error(err);
- });
我們也可以使用 chrome-remote-interface 提供的命令行功能,譬如我們可以在命令行中訪問某個界面并且記錄所有的網(wǎng)絡(luò)請求:
- $ chrome-remote-interface inspect
- >>> Network.enable()
- { result: {} }
- >>> Network.requestWillBeSent(params => params.request.url)
- { 'Network.requestWillBeSent': 'params => params.request.url' }
- >>> Page.navigate({url: 'https://www.wikipedia.org'})
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/' }
- { result: { frameId: '5530.1' } }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia_wordmark.png' }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png' }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/js/index-3b68787aa6.js' }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/js/gt-ie9-c84bf66d33.js' }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/sprite-bookshelf_icons.png?16ed124e8ca7c5ce9d463e8f99b2064427366360' }
- { 'Network.requestWillBeSent': 'https://www.wikipedia.org/portal/wikipedia.org/assets/img/sprite-project-logos.png?9afc01c5efe0a8fb6512c776955e2ad3eb48fbca' }
我們也可以直接查看內(nèi)置的接口文檔:
- >>> Page.navigate
- { [Function]
- category: 'command',
- parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
- returns:
- [ { name: 'frameId',
- '$ref': 'FrameId',
- hidden: true,
- description: 'Frame id that will be navigated.' } ],
- description: 'Navigates current page to the given URL.',
- handlers: [ 'browser', 'renderer' ] }>>> Page.navigate
- { [Function]
- category: 'command',
- parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
- returns:
- [ { name: 'frameId',
- '$ref': 'FrameId',
- hidden: true,
- description: 'Frame id that will be navigated.' } ],
- description: 'Navigates current page to the given URL.',
- handlers: [ 'browser', 'renderer' ] }
我們在上文中還提到需要以代碼控制瀏覽器進行完整頁面截圖,這里需要利用 Emulation 模塊控制頁面視口縮放:
- const CDP = require('chrome-remote-interface');
- const argv = require('minimist')(process.argv.slice(2));
- const file = require('fs');
- // CLI Args
- const url = argv.url || 'https://www.google.com';
- const format = argv.format === 'jpeg' ? 'jpeg' : 'png';
- const viewportWidth = argv.viewportWidth || 1440;
- const viewportHeight = argv.viewportHeight || 900;
- const delay = argv.delay || 0;
- const userAgent = argv.userAgent;
- const fullPage = argv.full;
- // Start the Chrome Debugging Protocol
- CDP(async function(client) {
- // Extract used DevTools domains.
- const {DOM, Emulation, Network, Page, Runtime} = client;
- // Enable events on domains we are interested in.
- await Page.enable();
- await DOM.enable();
- await Network.enable();
- // If user agent override was specified, pass to Network domain
- if (userAgent) {
- await Network.setUserAgentOverride({userAgent});
- }
- // Set up viewport resolution, etc.
- const deviceMetrics = {
- width: viewportWidth,
- height: viewportHeight,
- deviceScaleFactor: 0,
- mobile: false,
- fitWindow: false,
- };
- await Emulation.setDeviceMetricsOverride(deviceMetrics);
- await Emulation.setVisibleSize({width: viewportWidth, height: viewportHeight});
- // Navigate to target page
- await Page.navigate({url});
- // Wait for page load event to take screenshot
- Page.loadEventFired(async () => {
- // If the `full` CLI option was passed, we need to measure the height of
- // the rendered page and use Emulation.setVisibleSize
- if (fullPage) {
- const {root: {nodeId: documentNodeId}} = await DOM.getDocument();
- const {nodeId: bodyNodeId} = await DOM.querySelector({
- selector: 'body',
- nodeId: documentNodeId,
- });
- const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});
- await Emulation.setVisibleSize({width: viewportWidth, height: height});
- // This forceViewport call ensures that content outside the viewport is
- // rendered, otherwise it shows up as grey. Possibly a bug?
- await Emulation.forceViewport({x: 0, y: 0, scale: 1});
- }
- setTimeout(async function() {
- const screenshot = await Page.captureScreenshot({format});
- const buffer = new Buffer(screenshot.data, 'base64');
- file.writeFile('output.png', buffer, 'base64', function(err) {
- if (err) {
- console.error(err);
- } else {
- console.log('Screenshot saved');
- }
- client.close();
- });
- }, delay);
- });
- }).on('error', err => {
- console.error('Cannot connect to browser:', err);
- });
【本文是51CTO專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請通過51CTO與作者聯(lián)系】