使用 Headless Chrome 进行页面渲染 从属于笔者的 Web 开发基础与工程实践系列文章,主要介绍了使用 Node.js 利用 Chrome Remote Protocol 远程控制 Headless Chrome 渲染界面的基础用法。本文涉及的参考与引用资料统一列举在这里。node
近日笔者在为 declarative-crawler 编写动态页面的蜘蛛,即在使用 declarative-crawler 爬取知乎美图 一文中介绍的 HeadlessChromeSpider 时,须要选择某个无界面浏览器以执行 JavaScript 代码来动态生成页面。以前笔者每每是使用 PhantomJS 或者 Selenium 执行动态页面渲染,而在 Chrome 59 以后 Chrome 提供了 Headless 模式,其容许在命令行中使用 Chromium 以及 Blink 渲染引擎提供的完整的现代 Web 平台特性。须要注意的是,Headless Chrome 仍然存在必定的局限,相较于 Nightmare 或 Phantom 这样的工具, Chrome 的远程接口仍然没法提供较好的开发者体验。咱们在下文介绍的代码示例中也会发现,目前咱们仍须要大量的模板代码进行控制。linux
在 Chrome 安装完毕后咱们能够利用其包体内自带的命令行工具启动:git
$ chrome --headless --remote-debugging-port=9222 https://chromium.org
笔者为了部署方便,使用 Docker 镜像来进行快速部署,若是你本地存在 Docker 环境,能够使用以下命令快速启动:github
docker run -d -p 9222:9222 justinribeiro/chrome-headless
若是是在 Mac 下本地使用的话咱们还能够建立命令别名:chrome
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 环境下咱们能够使用 deb 进行安装:docker
# 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 命令行也支持丰富的命令行参数,--dump-dom
参数能够将 document.body.innerHTML
打印到标准输出中:npm
chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/
而 --print-to-pdf
标识则会将网页输出位 PDF:ubuntu
chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/
初次以外,咱们也能够使用 --screenshot
参数来获取页面截图:浏览器
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/
若是咱们须要更复杂的截图策略,譬如进行完整页面截图则须要利用代码进行远程控制。网络
在上文中咱们介绍了如何利用命令行来手动启动 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 的远程封装,咱们能够参考协议文档了解详细的功能与参数。使用 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 提供的命令行功能,譬如咱们能够在命令行中访问某个界面而且记录全部的网络请求:
$ 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' }
咱们也能够直接查看内置的接口文档:
>>> 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); });