原文:How to install and use Headless Chrome on OSX

这个教程会用详细的步骤教你在 macOS 上怎么去获取和运行 Headless Chrome 和怎么去使用 Chrome 团队提供的示例代码。

Headless Chrome 解决了什么样的问题?

Chrome 的 Headless 模式是一个和网站交互的新方式,但是没有一个实际的界面显示在屏幕上。这看起来像是一个微不足道的改进,但对于从 Web 上抓取内容来说是一个巨大的飞跃。现在有一些稳定的非正式的解决方案去抓取,比如 PhantomJS 和 Nightmare(构建在 Electron 之上)。这些方案都还没有消失(编辑:PhantomJS 仅有的维护者已经辞职),它们仍然是抓取 Web 非常棒的解决方案。如果你已经在自己的系统中运用了这些工具,你可以继续使它们。

但是也有这样的说法,一些用户在使用 PhantomJS 和 Nightmare 的时候遇到了麻烦。这两个工具都有警告,当它们运行在 shell-only 系统(没有实际的屏幕或者窗口管理) 时。举个例子,当你在使用 Nightmare (一个 Electron 应用)时,为了运行这个应用你需要安装一个虚拟的显示管理。另外,自从 Nightmare 使用 Electron 构建之后,它与 Chrome 有着不用的安全模式,在生产环境测试的时候可能会无法捕获一些安全问题。

什么版本的 Chrome 支持 Headless ?

Headless Chrome 已经在 Chrome 59 中发布。截止 2017 年 4 月 13 日,Chrome Canary 是唯一包含 Chrome 59 的频道。这意味着现在,如果你想要使用 Headless 你需要安装 Chrome Canary。将来 Chrome 的开发团队会把 Chrome 59 放到主要的 Chrome 版本中,你就不需要安装 Chrome Canary 了。
安装 Chrome Canary ,你可以下载或者用 homebrew 安装:

1
brew install Caskroom/versions/google-chrome-canary

我怎么找到 Headless Chrome?

很多例子在使用 Headless Chrome 时,只用了一个简单的 chrome 命令。这可以在 Linux 下很好的工作,但是在 macOS 上不行,因为命令没有被安装在你的 PATH 中。
所以你需要找到 Chrome 的路径,让我们启动我们的终端去找 Chrome Canary 安装在我们系统的哪里。

1
sudo find / -type d -name "*Chrome Canary.app"

你可能会得到一些权限错误,但是你还是会得到一个路径长得像这样:

1
/Applications/Google Chrome Canary.app

我们找到了 Chrome Canary 的路径,我们可以使用它启动 Chrome 并使它运行在 Headless 模式。

我怎么启动 Headless Chrome?

我们得到 Chrome Canary 的路径以后,我们需要用一个命令启动 Chrome 作为一个 Headless 服务。

1
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --headless --remote-debugging-port=9222 --disable-gpu https://chromium.org

特别需要注意的是我们转义了文件名中空格,深入到 Mac .app 文件中去找到实际的 Chrome 二进制文件。然后我们通过启动参数去启动了一个 Headless 浏览器,给它一个初始化的链接 https://chromium.org。这个浏览器会监听 9222 端口等待接下来的指令。保持这个标签和服务运行。 打开另一个标签,我们将会连接到这个浏览器的这个标签,然后给一些指令。

我怎么使用 Headless Chrome 抓取数据?

我将要使用 Node.js 去连接我们运行中的 Chrome Canary 实例。你需要确保你已经安装了 Node,才可以继续这个步骤。
让我们生成一个普通的 Node 项目,只有一个依赖那就是 Chrome Remote Interface 包,它可以帮助我们与 Chrome 沟通。然后我们创建一个空白的 index.js 文件。

1
2
3
4
mkdir my-headless-chrome && cd my-headless-chrome
npm init --yes
npm install --save chrome-remote-interface
touch index.js

现在我们将要放一些代码到index.js。这个模板例子是由 Chrome 团队提供的。它指示这个浏览器导航到github.com,然后通过 client 里的 Network 属性捕获这个页面上所有的网络请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
});

最后启动我们的 Node 应用。

1
node index.js

我们可以看到 Chrome 发出的所有的网络请求,然而并没有一个实际的浏览器窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
https://github.com/
https://assets-cdn.github.com/assets/frameworks-12d63ce1986bd7fdb5a3f4d944c920cfb75982c70bc7f75672f75dc7b0a5d7c3.css
https://assets-cdn.github.com/assets/github-2826bd4c6eb7572d3a3e9774d7efe010d8de09ea7e2a559fa4019baeacf43f83.css
https://assets-cdn.github.com/assets/site-f4fa6ace91e5f0fabb47e8405e5ecf6a9815949cd3958338f6578e626cd443d7.css
https://assets-cdn.github.com/images/modules/site/home-illo-conversation.svg
https://assets-cdn.github.com/images/modules/site/home-illo-chaos.svg
https://assets-cdn.github.com/images/modules/site/home-illo-business.svg
https://assets-cdn.github.com/images/modules/site/integrators/slackhq.png
https://assets-cdn.github.com/images/modules/site/integrators/zenhubio.png
https://assets-cdn.github.com/images/modules/site/integrators/travis-ci.png
https://assets-cdn.github.com/images/modules/site/integrators/atom.png
https://assets-cdn.github.com/images/modules/site/integrators/circleci.png
https://assets-cdn.github.com/images/modules/site/integrators/codeship.png
https://assets-cdn.github.com/images/modules/site/integrators/codeclimate.png
https://assets-cdn.github.com/images/modules/site/integrators/gitterhq.png
https://assets-cdn.github.com/images/modules/site/integrators/waffleio.png
https://assets-cdn.github.com/images/modules/site/integrators/heroku.png
https://assets-cdn.github.com/images/modules/site/logos/airbnb-logo.png
https://assets-cdn.github.com/images/modules/site/logos/sap-logo.png
https://assets-cdn.github.com/images/modules/site/logos/ibm-logo.png
https://assets-cdn.github.com/images/modules/site/logos/google-logo.png
https://assets-cdn.github.com/images/modules/site/logos/paypal-logo.png
https://assets-cdn.github.com/images/modules/site/logos/bloomberg-logo.png
https://assets-cdn.github.com/images/modules/site/logos/spotify-logo.png
https://assets-cdn.github.com/images/modules/site/logos/swift-logo.png
https://assets-cdn.github.com/images/modules/site/logos/facebook-logo.png
https://assets-cdn.github.com/images/modules/site/logos/node-logo.png
https://assets-cdn.github.com/images/modules/site/logos/nasa-logo.png
https://assets-cdn.github.com/images/modules/site/logos/walmart-logo.png
https://assets-cdn.github.com/assets/compat-8a4318ffea09a0cdb8214b76cf2926b9f6a0ced318a317bed419db19214c690d.js
https://assets-cdn.github.com/assets/frameworks-6d109e75ad8471ba415082726c00c35fb929ceab975082492835f11eca8c07d9.js
https://assets-cdn.github.com/assets/github-5d29649478f4a2b05588bbd0d25cd56ff5445b21df31b4cccca942ad8687e1e8.js
https://assets-cdn.github.com/images/modules/site/heroes/home-code-bg-alt-01.svg
https://assets-cdn.github.com/static/fonts/roboto/roboto-light.woff
https://assets-cdn.github.com/static/fonts/roboto/roboto-regular.woff
https://assets-cdn.github.com/static/fonts/roboto/roboto-medium.woff

这是一个非常棒的方式去查看加载了那些资源,但是如果我想要操作页面上的 DOM 元素呢?我们用可以像这样的一个脚本拉取github.com上所有的img标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const CDP = require("chrome-remote-interface");
CDP(chrome => {
chrome.Page
.enable()
.then(() => {
return chrome.Page.navigate({ url: "https://github.com" });
})
.then(() => {
chrome.DOM.getDocument((error, params) => {
if (error) {
console.error(params);
return;
}
const options = {
nodeId: params.root.nodeId,
selector: "img"
};
chrome.DOM.querySelectorAll(options, (error, params) => {
if (error) {
console.error(params);
return;
}
params.nodeIds.forEach(nodeId => {
const options = {
nodeId: nodeId
};
chrome.DOM.getAttributes(options, (error, params) => {
if (error) {
console.error(params);
return;
}
console.log(params.attributes);
});
});
});
});
});
}).on("error", err => {
console.error(err);
});

我们可以得到以下的数据结构展现了页面上标签包含所有图片的链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
[ 'src',
'https://assets-cdn.github.com/images/modules/site/home-illo-conversation.svg',
'alt',
'',
'width',
'360',
'class',
'd-block width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/home-illo-chaos.svg',
'alt',
'',
'class',
'd-block width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/home-illo-business.svg',
'alt',
'',
'class',
'd-block width-fit mx-auto mb-4' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/slackhq.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/zenhubio.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/travis-ci.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/atom.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/circleci.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/codeship.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/codeclimate.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/gitterhq.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/waffleio.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/integrators/heroku.png',
'alt',
'',
'class',
'd-block integrations-collage-img width-fit mx-auto' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/airbnb-logo.png',
'alt',
'Airbnb',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/sap-logo.png',
'alt',
'SAP',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/ibm-logo.png',
'alt',
'IBM',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/google-logo.png',
'alt',
'Google',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/paypal-logo.png',
'alt',
'PayPal',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/bloomberg-logo.png',
'alt',
'Bloomberg',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/spotify-logo.png',
'alt',
'Spotify',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/swift-logo.png',
'alt',
'Swift',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/facebook-logo.png',
'alt',
'Rails',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/node-logo.png',
'alt',
'Node',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/nasa-logo.png',
'alt',
'Nasa',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]
[ 'src',
'https://assets-cdn.github.com/images/modules/site/logos/walmart-logo.png',
'alt',
'Walmart',
'class',
'logo-img px-2 px-sm-4 px-md-5 px-lg-0' ]

让我们愉快的抓取吧!