使用 Headless Chrome 进行页面渲染 从属于笔者的使用 Web 开发基础与工程实践系列文章,主要介绍了使用 Node.js 利用 Chrome Remote Protocol 远程控制 Headless Chrome 渲染界面的进行基础用法。本文涉及的页面参考与引用资料统一列举在这里。
近日笔者在为 declarative-crawler 编写动态页面的渲染蜘蛛,即在使用 declarative-crawler 爬取知乎美图 一文中介绍的使用 HeadlessChromeSpider 时,需要选择某个无界面浏览器以执行 JavaScript 代码来动态生成页面。进行之前笔者往往是页面使用 PhantomJS 或者 Selenium 执行动态页面渲染,而在 Chrome 59 之后 Chrome 提供了 Headless 模式,渲染其允许在命令行中使用 Chromium 以及 Blink 渲染引擎提供的使用完整的现代 Web 平台特性。需要注意的进行是,Headless Chrome 仍然存在一定的页面局限,站群服务器相较于 Nightmare 或 Phantom 这样的渲染工具, Chrome 的使用远程接口仍然无法提供较好的开发者体验。我们在下文介绍的进行代码示例中也会发现,目前我们仍需要大量的页面模板代码进行控制。
安装与启动
在 Chrome 安装完毕后我们可以利用其包体内自带的命令行工具启动:
$ chrome --headless --remote-debugging-port=9222 https://chromium.org笔者为了部署方便,使用 Docker 镜像来进行快速部署,如果你本地存在 Docker 环境,可以使用如下命令快速启动:
docker run -d -p 9222:9222 justinribeiro/chrome-headless如果是在 Mac 下本地使用的话我们还可以创建命令别名:
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 进行安装:
# 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 -fchrome 命令行也支持丰富的命令行参数,--dump-dom 参数可以将 document.body.innerHTML 打印到标准输出中:
chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/而 --print-to-pdf 标识则会将网页输出位 PDF:
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/如果我们需要更复杂的截图策略,云南idc服务商譬如进行完整页面截图则需要利用代码进行远程控制。
代码控制
启动
在上文中我们介绍了如何利用命令行来手动启动 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); });【本文是专栏作者“张梓雄 ”的原创文章,如需转载请通过与作者联系】
戳这里,源码库看该作者更多好文