Puppeteer-浏览器自动化操作实践
# 前言
Puppeteer
是一款浏览器自动化产品,他可以自动化的帮你完成一些对浏览器的重复操作,还可以作为一个爬虫抓取浏览器的各种数据。总是是一个非常强大的工具,下面通过一个简单的实践来了解一下他的基本使用
# 什么是Puppeteer
官网对于他的介绍是这样的
提示
Puppeteer
是一个 Node.js
库,它提供了一个高级 API
来通过 开发工具协议 控制 Chrome/Chromium
。 Puppeteer
默认以 无头 模式运行,但可以配置为在完整 ("有头")Chrome/Chromium
中运行。
我们在浏览器中的大部分操作都可以通过Puppeteer
来完成!
官网还提供了一个简单的例子可供入门可以自行观看官网 (opens new window) ,下面我们来实现一个自动登录Tik Tok
的脚本。
# 开始
首先我们新建一个工程,并创建lib
文件夹,文件夹下创建一个index.js
文件用来写脚本。
初始化好工程后,安装一下puppeteer
npm i puppeteer
Puppeteer
使用多个默认值,可以通过配置文件进行自定义。
例如,要更改 Puppeteer
用于安装浏览器的默认缓存目录,你可以在应用的根目录中添加 .puppeteerrc.cjs
(或 puppeteer.config.cjs
),其中包含以下内容
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
2
3
4
5
6
7
8
9
然后开始着手写脚本
try{
// 创建一个无头浏览器,可以通过配置取消无头,headless设置为true则会静默执行,这只为false则会打开一个浏览器窗口
const browser = await puppeteer.launch({ headless: false });
// 创建一个新窗口,后续对网页的所有操作都会通过page对象进行完成
const page = await browser.newPage();
// 控制窗口跳转至目标站点,必须带上协议
await page.goto('https://www.tiktok.com');
console.log('---------开始执行任务:' + data.funcName)
// 关闭窗口
await page.close()
}catch (e) {
console.log('---------执行失败:' + e)
}
2
3
4
5
6
7
8
9
10
11
12
13
这样,一个简单的自动打开浏览器进入目标网站的操作就完成了。接下来就是一些对网页元素的基本操作,再开始之前我们先熟悉几个api
# 查找元素
page.$('xpath')
我们可以通过page
的$
方法查找元素,需要传入一个元素的xpath
,xpath
可以打开网页控制台,鼠标右键放到元素上点击copy
会弹出选项,我们选择xpath
就可以复制了。
此方法会返回一个数组,将符合xpath
的所有元素都返回。
# 填充表单
当我们查找到元素之后,可以像正常操作dom
对象一样,调用元素的click
等方法,如果是input
元素,可以通过元素的type
方法,填充内容
const element = await page.$('xpath')[0]
await element.type('填充input');
2
# 等待网页操作
page
的waitForTimeout
方法可以等待一下网页的操作,交互需要时间,如果快速的进行操作,可能会导致意想不到的错误。所以最好在处理完交互动作后等待个一两秒。例如某些操作需要发起请求等待响应,如果我们不等待的话就会导致后续流程无法继续
此次脚本操作里只用到了这些操作,page
对象上还有非常多的方法,可根据需要自行了解,下面开始来写脚本的具体操作内容
# 查找登录按钮并点击登录
const elements = await page.$x('//*[@id="header-login-button"]');
// 找到了匹配XPath表达式的元素
const element = elements[0];
element.click()
page.waitForTimeout(2000)
2
3
4
5
# 选择登录方式
const elements = await page.$x('//!*[@id="loginContainer"]/div/div/a[2]/div/p');
// 找到了匹配XPath表达式的元素
const element = elements[0];
element.click()
page.waitForTimeout(2000)
2
3
4
5
重复之前的操作,找到目标元素,并点击
# 选择账号密码登录
const elements = await page.$x('//!*[@id="loginContainer"]/div[2]/form/div[1]/a');
// 找到了匹配XPath表达式的元素
const element = elements[0];
element.click()
page.waitForTimeout(2000)
2
3
4
5
# 输入用户名和密码
const elements = await page.$x('//!*[@id="loginContainer"]/div[2]/form/div[1]/input');
// 找到了匹配XPath表达式的元素
const element = elements[0];
await element.type('用户名');
page.waitForTimeout(2000)
2
3
4
5
输入好之后再点击登录这个自动登录Tik Tok
的脚本就算完成了,当然其中还有验证码的过程。这里就不赘述了,后面会贴出详细的代码。
# 封装
通过上文的描述,对puppeteer
已经有了一个基本的认识,但是上述步骤的代码是针对了某一个动作去操作。我们是不是可以把这个动作通过一个json
文件来描述,然后封装一个通用脚本来去执行这些json
文件呢?这样一来就不需要写那么多重复的代码了。
关于json
文件,我是这样定义的
{
"funcName": "自动登录",
"targetUrl": "https://www.tiktok.com",
"autoXpath": [
{
"title": "点击登录按钮",
"type": "click",
"xpath": [
"//*[@id=\"header-login-button\"]"
],
"successCriteriaXPaths": [],
"failureCriteriaXPaths": []
},
{
"title": "选择登录类型",
"type": "click",
"xpath": [
"//*[@id=\"loginContainer\"]/div/div/a[2]/div/p",
"//*[@id=\"tux-3-tab-email/username\"]/span"
],
"successCriteriaXPaths": [],
"failureCriteriaXPaths": []
},
{
"title": "选择账号登录",
"type": "click",
"xpath": [
"//*[@id=\"loginContainer\"]/div[2]/form/div[1]/a"
],
"successCriteriaXPaths": [],
"failureCriteriaXPaths": []
},
{
"title": "输入账号",
"xpath": [
"//*[@id=\"loginContainer\"]/div[2]/form/div[1]/input",
"//*[@id=\"tux-3-panel-email/username\"]/div/form/div[1]/input"
],
"type": "input",
"inputValue": "123456",
"successCriteriaXPaths": [],
"failureCriteriaXPaths": []
},
{
"title": "输入密码",
"xpath": [
"//*[@id=\"loginContainer\"]/div[2]/form/div[2]/div/input",
"///*[@id=\"tux-3-panel-email/username\"]/div/form/div[2]/div/input"
],
"type": "input",
"inputValue": "123456",
"successCriteriaXPaths": [],
"failureCriteriaXPaths": []
},
{
"title": "点击登录",
"type": "click",
"xpath": [
"//*[@id=\"loginContainer\"]/div[2]/form/button",
"//*[@id=\"tux-3-panel-email/username\"]/div/form/button"
],
"successCriteriaXPaths": [],
"failureCriteriaXPaths": [
"//*[@id=\"loginContainer\"]/div[2]/form/div[3]/span"
]
}
]
}
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
文件内容大致包含了,本次脚本执行的内容名称,目标网址,以及自动化操作的步骤细分。autoXpath
里包含了每一个自动化操作,目标元素的xpath
,操作类型
,以及标题
等。
下面我们开始改造一下之前的代码。让他去识别json
文件。
# 改造
首先创建一个run
方法,用于启动浏览器和解析脚本
const run = async (data) => {
try{
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
page.on('response', response => responseHandler(response, page));
await page.goto(data.targetUrl);
console.log('---------开始执行任务:' + data.funcName)
if(data.autoXpath.length>0){
for (const job of data.autoXpath) {
console.log('---------开始执行步骤:' + job.title)
}
}
}catch (e) {
console.log('---------执行失败:' + e)
}
// await browser.close();
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
run
方法接受本次执行内容作为参数,然后对autoXpath
进行一个遍历,去执行每一个步骤。注意这里使用的for of
来进行遍历,因为我们需要等待操作。用别的循环的话无法等待上一步完成
接下来我们对每个步骤的操作类型进行一个细化封装,比如上述脚本中出现了click
和input
两种操作类型。一个是点击,一个是输入。我们创建两个处理函数
/**
* 处理元素的点击事件
*/
const handleClick = async (job,page)=>{
try{
if(job.xpath.length>0){
const element = await handleSearchDom(job.xpath,page)
element.click();
await page.waitForTimeout(3000);
}
}catch (e) {
console.log(`---------步骤-${job.title}-执行失败:${e}`)
}
}
/**
* 处理元素的输入事件
*/
const handleInput = async (job,page)=>{
try{
if(job.xpath.length>0){
const element = await handleSearchDom(job.xpath,page)
await element.type(job.inputValue);
await page.waitForTimeout(2000);
}
}catch (e) {
console.log(`---------步骤${job.title}执行失败:${e}`)
}
}
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
再来创建一个操作字典
/**
* 交互字典
* @type {{click}}
*/
const INTERACTION_TYPE = {
'click':handleClick,
'input':handleInput
}
2
3
4
5
6
7
8
这样,我们就可以在run
方法中加入执行步骤的代码了
const run = async (data) => {
...
if(data.autoXpath.length>0){
for (const job of data.autoXpath) {
console.log('---------开始执行步骤:' + job.title)
await INTERACTION_TYPE[job.type](job,page)
}
}
}
2
3
4
5
6
7
8
9
下面我们再封装一下查找元素的函数
/**
* 查找元素
*/
const handleSearchDom = async (xpath,page)=>{
try{
async function search(i=0){
if(!xpath[i]){
console.log('---------未找到匹配XPath表达式的元素');
return null
}
const elements = await page.$x(xpath[i]);
if (elements.length > 0) {
// 找到了匹配XPath表达式的元素
const element = elements[0];
// 在这里可以对找到的元素进行操作
return element
} else {
return search(i++)
}
}
return search()
}catch (e) {
console.log('---------查找元素失败:' + e)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
因为元素可能会有多个xpath
,所以这里通过递归的形式去查找每一条xpath
,只要找到了就立刻返回目标元素。
这样一个能识别json
的通用脚本就差不多了。最后再完善一下验证逻辑。就可以用来跑登录的功能了。下面贴一下完整的代码
const puppeteer = require('puppeteer');
const fs = require('fs');
const axios = require('axios');
const {Random} = require('random-js');
const random = new Random();
let jsonData = null
// 读取 JSON 文件
fs.readFile('src/login.json', 'utf8', (err, data) => {
if (err) {
console.error('读取文件时发生错误:', err);
return;
}
try {
// 解析 JSON 数据
jsonData = JSON.parse(data);
// jsonData 现在包含 JSON 文件的内容
run(jsonData)
} catch (parseError) {
console.error('解析 JSON 时发生错误:', parseError);
}
});
const logger = {
info: (message) => console.log(`[INFO] ${message}`),
error: (message, err) => {
console.error(`[ERROR] ${message}`);
if (err) console.error(err.stack || err);
},
debug: (message) => console.log(`[DEBUG] ${message}`),
warn: (message) => console.warn(`[WARN] ${message}`) // 添加的warn方法
};
const IMG_CACHE = {
url1: null,
url2: null
};
const CAPTCHA = {
TYPE_DEFAULT: 0,
TYPE_ROTATE: 2,
TYPE_CLICK: 1
};
const URLS = {
login: 'https://www.tiktok.com/explore',
captchaGet: 'https://us.tiktok.com/captcha/get',
captchaOversea: 'https://p16-security-va.ibyteimg.com/img/security-captcha-oversea-usa'
};
async function b64_api(img_b64 = null, large_b64 = null, small_b64 = null, username = 'jstsoso', password = 'cc123456', module_id = '07216914') {
let data = {
"username": username,
"password": password,
"ID": module_id,
"b64": img_b64,
"version": "3.1.1"
};
if (large_b64) {
data['b64_large'] = large_b64;
}
if (small_b64) {
data['b64_small'] = small_b64;
}
try {
const response = await axios.post("http://www.fdyscloud.com.cn/tuling/predict", data);
return response.data;
} catch (error) {
logger.error('Error in b64_api:', error.message);
return null;
}
}
async function getImageAsBase64(url) {
try {
const response = await axios.get(url, {responseType: 'arraybuffer'});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
return base64;
} catch (error) {
logger.error('Error fetching the image:', error.message);
return null;
}
}
// 验证码
async function handleRotateCaptcha(page) {
try {
if (!IMG_CACHE.url1 || !IMG_CACHE.url2) {
logger.error('Image URLs are missing in the cache.');
return;
}
const large_b64 = await getImageAsBase64(IMG_CACHE.url1);
const small_b64 = await getImageAsBase64(IMG_CACHE.url2);
const result = await b64_api(null, large_b64, small_b64, 'jstsoso', 'cc123456', '47839879');
console.log(result)
if (!result || !result.data || typeof result.data['小圆顺时针旋转度数'] === 'undefined') {
logger.error('Failed to get the rotation degree from the API.');
return;
}
const degree = result.data['小圆顺时针旋转度数'];
const offset = Math.floor((340 - 64.5) / 180 * degree / 2);
const hk = await page.$x('//div[contains(@class,"captcha-drag-icon")]');
if (hk.length === 0) {
logger.error('Failed to find the captcha drag icon on the page.');
return;
}
const hk_box = await hk[0].boundingBox();
let start_x = hk_box.x;
let start_y = hk_box.y;
await page.mouse.move(start_x, start_y);
await page.mouse.down();
for (let i = 0; i < offset; i++) {
await page.waitForTimeout(20);
start_x += random.integer(2, 6);
start_y += random.integer(-3, 3);
if (start_y < hk_box.y - 3) {
start_y = hk_box.y - 3;
} else if (start_y > hk_box.y + 3) {
start_y = hk_box.y + 3;
}
if (start_x > hk_box.x + offset) {
start_x = hk_box.x + offset;
await page.mouse.move(start_x, start_y);
break;
}
await page.mouse.move(start_x, start_y);
}
await page.mouse.up()
const verify_resp = await page.waitForResponse('https://us.tiktok.com/captcha/verify*');
const verify_json = await verify_resp.json();
if (!verify_json) {
logger.error('Failed to get the verification response.');
return;
}
logger.debug(verify_json);
} catch (error) {
logger.error(`Error handling the rotate captcha: ${error.message}`);
}
}
const handleClickCaptcha = async ()=>{
logger.info('点击验证')
}
/**
* 查找元素
*/
const handleSearchDom = async (xpath,page)=>{
try{
async function search(i=0){
if(!xpath[i]){
console.log('---------未找到匹配XPath表达式的元素');
return null
}
const elements = await page.$x(xpath[i]);
if (elements.length > 0) {
// 找到了匹配XPath表达式的元素
const element = elements[0];
// 在这里可以对找到的元素进行操作
return element
} else {
return search(i++)
}
}
return search()
}catch (e) {
console.log('---------查找元素失败:' + e)
}
}
/**
* 处理元素的点击事件
*/
const handleClick = async (job,page)=>{
try{
if(job.xpath.length>0){
const element = await handleSearchDom(job.xpath,page)
element.click();
console.log(element,123123123)
await page.waitForTimeout(3000);
}
}catch (e) {
console.log(`---------步骤-${job.title}-执行失败:${e}`)
}
}
/**
* 处理元素的输入事件
*/
const handleInput = async (job,page)=>{
try{
if(job.xpath.length>0){
const element = await handleSearchDom(job.xpath,page)
await element.type(job.inputValue);
await page.waitForTimeout(2000);
}
}catch (e) {
console.log(`---------步骤${job.title}执行失败:${e}`)
}
}
/**
* 交互字典
* @type {{click}}
*/
const INTERACTION_TYPE = {
'click':handleClick,
'input':handleInput
}
const determineCaptchaType = (question) => {
if (question.url1 && question.url2) {
logger.info('旋转验证码');
IMG_CACHE.url1 = question.url1;
IMG_CACHE.url2 = question.url2;
return CAPTCHA.TYPE_ROTATE;
} else if (question.url1 && !question.url2) {
logger.info('点选验证码');
return CAPTCHA.TYPE_CLICK;
}
return CAPTCHA.TYPE_NONE;
}
const handleCaptchaByType = async (type, page) => {
switch (type) {
case CAPTCHA.TYPE_ROTATE:
await handleRotateCaptcha(page);
break;
case CAPTCHA.TYPE_CLICK:
await handleClickCaptcha(page);
break;
default:
break;
}
}
const responseHandler = async (response, page) => {
const url = response.url();
try {
if (url.includes(URLS.captchaGet)) {
const jsonData = await response.json();
const question = jsonData.data.question;
if (question) {
CAPTCHA_TYPE = determineCaptchaType(question);
}
}
if (url.includes(URLS.captchaOversea)) {
await handleCaptchaByType(CAPTCHA_TYPE, page);
}
} catch (error) {
logger.error('Error handling response:', error.message);
}
};
const run = async (data) => {
try{
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
page.on('response', response => responseHandler(response, page));
await page.goto(data.targetUrl);
console.log('---------开始执行任务:' + data.funcName)
if(data.autoXpath.length>0){
for (const job of data.autoXpath) {
console.log('---------开始执行步骤:' + job.title)
await INTERACTION_TYPE[job.type](job,page)
}
}
}catch (e) {
console.log('---------执行失败:' + e)
}
// await browser.close();
};
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# 结语
至此,一个简单的自动登录脚本就算完成了。在熟练使用了puppeteer
之后,我们就可以解放双手,自动化的去做很多事情。不只是操作页面,爬取数据也是一样。puppeteer
提供了监听页面接口响应的api
,比如上文的验证码就是通过监听返回的图片来去做验证的动作。