引言

遇到问题,关键是解决问题的思路,而不是解决问题本身

在一个平常的工作日,接到了一项任务,需要在某平台上批量添加并处理多份任务材料。每份任务完成后都需要下载报告,过程异常繁琐 —— 由于采用动态加载,每份报告的下载都需要两次点击操作。简单计算后,我意识到这将需要整整2000次重复性操作。

起初,我抱着"既来之则安之"的心态,想着按部就班地完成这项工作。毕竟,工作量在那里。

这让我想起几年前的一段经历:当时我在微博上关注了大量博主,后来想要清理关注列表时,发现平台并没有提供批量操作的功能。面对类似的困境,想到了用浏览器控制台来解决问题。更何况GPT业已成熟,不在话下。

问题描述

背景

  • 平台上有1000份需要下载的报告
  • 每份报告需要两步操作:
    1. 点击"下载"按钮
    2. 在弹出的选项中点击"格式化Excel"按钮
  • 平台没有提供批量下载或操作的功能

目标

开发一个自动化脚本,能够:

  1. 自动识别并点击所有的"下载"按钮
  2. 对每个下载操作,自动点击对应的"格式化Excel"按钮
  3. 处理大量报告而不出错

环境准备

  • 浏览器:Google Chrome(版本 96.0.4664.110)
  • 编程语言:JavaScript
  • 执行环境:浏览器控制台(DevTools)

解决过程

步骤1:初步分析页面结构

首先,我打开了浏览器的开发者工具,查看了页面的HTML结构。我发现:

  • "下载"按钮是普通的<button>元素
  • "格式化Excel"按钮在点击"下载"后才会出现
  • 按钮没有特定的id,但有特定的类名

步骤2:初次尝试

我首先尝试了一个简单的函数来计数"格式化Excel"按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
function countExcelButtons() {
const buttons = document.querySelectorAll('button');
let count = 0;
buttons.forEach(button => {
if (button.textContent.trim() === '格式化Excel') {
count++;
}
});
console.log(`总共找到 ${count} 个"格式化Excel"按钮`);
return count;
}

countExcelButtons();

结果:这个函数返回0,没有找到任何"格式化Excel"按钮。

问题分析:我意识到"格式化Excel"按钮是动态加载的,只有在点击"下载"按钮后才会出现。

步骤3:改进脚本

基于上述发现,我编写了一个新的脚本,先点击"下载"按钮,然后查找并点击"格式化Excel"按钮:

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
async function clickDownloadAndExcel() {
const downloadButtons = Array.from(document.querySelectorAll('button')).filter(button =>
button.textContent.trim() === '下载'
);

console.log(`找到 ${downloadButtons.length} 个下载按钮`);

for (const button of downloadButtons) {
button.click();
console.log('点击了下载按钮');
await new Promise(resolve => setTimeout(resolve, 500));

const excelButtons = Array.from(document.querySelectorAll('button')).filter(button =>
button.textContent.trim() === '格式化Excel'
);

if (excelButtons.length > 0) {
console.log(`找到 ${excelButtons.length} 个"格式化Excel"按钮`);
excelButtons.forEach(excelButton => {
console.log('点击格式化Excel按钮');
excelButton.click();
});
} else {
console.log('没有找到对应的"格式化Excel"按钮');
}
}

console.log('所有操作完成');
}

clickDownloadAndExcel().then(() => {
console.log('脚本执行完毕');
});

执行结果

1
2
3
4
5
6
7
8
9
10
11
找到 20 个下载按钮
点击了下载按钮
找到 1 个"格式化Excel"按钮
点击格式化Excel按钮
点击了下载按钮
找到 2 个"格式化Excel"按钮
点击格式化Excel按钮
点击格式化Excel按钮
...
所有操作完成
脚本执行完毕

问题:虽然脚本成功执行,但我发现它下载了超过100份报告,远超预期的20份。

步骤4:问题分析和优化

分析日志后,我发现每次点击"下载"按钮后,页面上的"格式化Excel"按钮数量都在增加,脚本每次都点击了所有可见的"格式化Excel"按钮,导致重复下载。

步骤5:最终优化版本

为了解决重复下载的问题,我对脚本进行了以下优化:

  1. 使用Set来跟踪已处理过的"格式化Excel"按钮
  2. 每次操作只处理一个新出现的"格式化Excel"按钮

以下是最终的脚本:

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
async function clickDownloadAndExcel() {
const downloadButtons = Array.from(document.querySelectorAll('button')).filter(button =>
button.textContent.trim() === '下载'
);

console.log(`找到 ${downloadButtons.length} 个下载按钮`);

let processedExcelButtons = new Set();

for (let i = 0; i < downloadButtons.length; i++) {
const button = downloadButtons[i];
button.click();
console.log(`点击了第 ${i + 1} 个下载按钮`);
await new Promise(resolve => setTimeout(resolve, 500));

const excelButtons = Array.from(document.querySelectorAll('button')).filter(button =>
button.textContent.trim() === '格式化Excel' && !processedExcelButtons.has(button)
);

if (excelButtons.length > 0) {
const newExcelButton = excelButtons[0];
console.log(`点击第 ${i + 1} 个格式化Excel按钮`);
newExcelButton.click();
processedExcelButtons.add(newExcelButton);
await new Promise(resolve => setTimeout(resolve, 500));
} else {
console.log(`没有找到新的"格式化Excel"按钮`);
}
}

console.log('所有操作完成');
}

clickDownloadAndExcel().then(() => {
console.log('脚本执行完毕');
});

执行结果

1
2
3
4
5
6
7
8
9
10
找到 20 个下载按钮
点击了第 1 个下载按钮
点击第 1 个格式化Excel按钮
点击了第 2 个下载按钮
点击第 2 个格式化Excel按钮
...
点击了第 20 个下载按钮
点击第 20 个格式化Excel按钮
所有操作完成
脚本执行完毕

这次脚本成功地只下载了20份报告,每个报告只被处理一次。

image-20241108102415092

突破点:

  1. 异步编程范式
    • 利用async/await精准控制异步流程
    • 通过Promise链确保操作的顺序性和可靠性
    • 解决传统同步编程在网页自动化中的阻塞问题
  2. 动态DOM交互策略
    • 针对动态加载元素,采用动态选择器
    • 实现元素可见性和可点击性的实时检测
    • 使用MutationObserver或显式等待机制,应对页面动态变化
  3. 状态管理与去重
    • 引入Set数据结构实现操作去重
    • 维护操作状态,防止重复执行
    • 建立闭环的状态追踪机制

PS:其他自动化记录

查询网页所有开始按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function findStartButtons() {
// 查找所有按钮
const buttons = Array.from(document.querySelectorAll('.light-btn-link')).filter(btn =>
btn.textContent.includes('开始')
);

// 打印找到的按钮
if (buttons.length > 0) {
console.log(`找到 ${buttons.length} 个开始按钮:`);
buttons.forEach((button, index) => {
console.log(`按钮 ${index + 1}:`, button);
});
} else {
console.log('未找到任何开始按钮');
}
}

// 执行查找
findStartButtons();

点击网页开始按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function clickAllStartButtons() {
// 查找所有按钮
const buttons = Array.from(document.querySelectorAll('.light-btn-link')).filter(btn =>
btn.textContent.includes('开始')
);

// 打印找到的按钮数量
console.log(`找到 ${buttons.length} 个开始按钮,开始点击...`);

// 直接依次点击所有按钮
buttons.forEach((button, index) => {
button.click();
console.log(`点击第 ${index + 1} 个开始按钮`);
});

// 所有按钮点击完成的提示
console.log('所有开始按钮点击完成');
}

// 执行点击
clickAllStartButtons();

任务状态统计

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
function countTaskStatuses() {
// 查找所有任务状态的 <span> 元素
const statuses = Array.from(document.querySelectorAll('.light-table-cell .light-tag'));

// 初始化计数器
const statusCounts = {
'任务等待中': 0,
'任务已结束': 0,
'任务执行中': 0
};

// 统计各状态数量
statuses.forEach(status => {
const text = status.textContent;
if (statusCounts.hasOwnProperty(text)) {
statusCounts[text]++;
}
});

// 打印统计结果
console.log('任务状态统计:');
console.log(`任务等待中:${statusCounts['任务等待中']} 个`);
console.log(`任务已结束:${statusCounts['任务已结束']} 个`);
console.log(`任务执行中:${statusCounts['任务执行中']} 个`);

// 总任务数
const totalTasks = statuses.length;
console.log(`总任务数:${totalTasks} 个`);
}

// 执行统计
countTaskStatuses();

删除任务

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
function clickAllDeleteButtons() {
// 查找所有删除按钮
const deleteButtons = Array.from(document.querySelectorAll('.anticon-delete'));

// 打印找到的删除按钮数量
console.log(`找到 ${deleteButtons.length} 个删除按钮,开始点击...`);

// 依次点击所有删除按钮
deleteButtons.forEach((button, index) => {
// 找到最近的可点击的父元素
const clickableParent = button.closest('button') || button.closest('a') || button;

if (clickableParent) {
clickableParent.click();
console.log(`点击第 ${index + 1} 个删除按钮`);
} else {
console.log(`第 ${index + 1} 个删除按钮无法点击`);
}
});

// 所有删除按钮点击完成的提示
console.log('所有删除按钮点击完成');
}

// 执行点击
clickAllDeleteButtons();

© Rabbit 使用 Stellar 创建

✨ 营业:

共发表 56 篇Blog 🔸 总计 123.6k