引言
遇到问题,关键是解决问题的思路,而不是解决问题本身
在一个平常的工作日,接到了一项任务,需要在某平台上批量添加并处理多份任务材料。每份任务完成后都需要下载报告,过程异常繁琐 —— 由于采用动态加载,每份报告的下载都需要两次点击操作。简单计算后,我意识到这将需要整整2000次重复性操作。
起初,我抱着"既来之则安之"的心态,想着按部就班地完成这项工作。毕竟,工作量在那里。
这让我想起几年前的一段经历:当时我在微博上关注了大量博主,后来想要清理关注列表时,发现平台并没有提供批量操作的功能。面对类似的困境,想到了用浏览器控制台来解决问题。更何况GPT业已成熟,不在话下。
问题描述
背景
- 平台上有1000份需要下载的报告
- 每份报告需要两步操作:
- 点击"下载"按钮
- 在弹出的选项中点击"格式化Excel"按钮
- 平台没有提供批量下载或操作的功能
目标
开发一个自动化脚本,能够:
- 自动识别并点击所有的"下载"按钮
- 对每个下载操作,自动点击对应的"格式化Excel"按钮
- 处理大量报告而不出错
环境准备
- 浏览器: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:最终优化版本
为了解决重复下载的问题,我对脚本进行了以下优化:
- 使用
Set来跟踪已处理过的"格式化Excel"按钮
- 每次操作只处理一个新出现的"格式化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份报告,每个报告只被处理一次。

突破点:
- 异步编程范式
- 利用
async/await精准控制异步流程
- 通过Promise链确保操作的顺序性和可靠性
- 解决传统同步编程在网页自动化中的阻塞问题
- 动态DOM交互策略
- 针对动态加载元素,采用动态选择器
- 实现元素可见性和可点击性的实时检测
- 使用
MutationObserver或显式等待机制,应对页面动态变化
- 状态管理与去重
- 引入
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();
|