
在使用puppeteer进行自动化测试时,处理动态密码键盘这类非标准输入组件常遇到点击失效问题,表现为`node is either not clickable or not an htmlelement`错误。本教程将详细介绍如何通过将密码拆分为字符、利用xpath精确匹配键盘按键,并结合shift键处理大小写,从而有效解决此类复杂ui元素的交互挑战,确保自动化流程的稳定执行。
自动化动态密码键盘点击的挑战
在许多银行或安全敏感的网站中,为了提高安全性,会采用动态密码键盘(虚拟键盘)来代替传统的文本输入框。这种键盘的按键通常是动态渲染的html元素,它们的ID可能不固定,或者在页面加载后才出现。当尝试使用Puppeteer直接选择并点击这些元素时,可能会遇到以下问题:
- 元素未就绪: 在尝试点击时,元素可能尚未完全渲染或处于可交互状态。
- 元素选择不准确: 仅通过类名或部分ID可能无法唯一且稳定地选中特定按键。
- node is either not clickable or not an HTMLElement错误: 这通常发生在Puppeteer试图点击一个实际上不可点击的元素(例如,它被其他元素遮挡,或者它不是一个可交互的html元素,如<a>、<button>、<input>等)。
为了克服这些挑战,我们需要一种更精确、更健壮的元素选择和交互策略。
解决方案:XPath与字符级交互
核心解决方案在于结合以下策略:
- 字符级密码输入: 将密码分解为单个字符,逐个模拟点击键盘上的对应按键。
- XPath精确选择: 利用XPath的强大功能,根据元素的文本内容和类名来精确匹配动态键盘上的每个按键。
- 处理特殊按键: 对于大小写字母,需要模拟“Shift”键的按下和释放操作。
- 健壮的等待机制: 在每次交互前,确保目标元素已经加载并可点击。
1. 准备工作与基本设置
首先,确保您的项目中已安装Puppeteer。
const puppeteer = require('puppeteer');
为了提高代码的可读性和复用性,我们可以创建一个辅助函数来封装“等待元素并点击”的常见操作。
// 辅助函数:等待元素出现并点击 async function waitClick(page, selector, isXPath = false) { let btn; if (isXPath) { // 使用page.waitForXPath等待XPath元素 btn = await page.waitForXPath(selector, { visible: true, timeout: 5000 }); // Puppeteer的XPath结果是ElementHandle数组,需要取第一个 await btn.click(); } else { // 使用page.waitForSelector等待css选择器元素 btn = await page.waitForSelector(selector, { visible: true, timeout: 5000 }); await btn.click(); } }
注意: 当使用XPath时,page.waitForXPath返回的是ElementHandle的数组,但click()方法通常直接作用于单个ElementHandle。如果XPath预期只匹配一个元素,可以直接在返回的ElementHandle上调用click()。如果waitClick函数需要通用,可以考虑page.waitForXPath返回的ElementHandle数组的第一个元素,或者直接使用page.click(selector)如果selector是XPath字符串且page.click支持。在上述修改后的waitClick中,为了简化,假设page.waitForXPath返回的btn是可直接点击的ElementHandle(这通常是正确的行为)。
2. 自动化登录流程
以下是完整的自动化登录函数,它将处理用户名输入、密码键盘交互和最终的登录提交。
const puppeteer = require('puppeteer'); // 辅助函数:等待元素出现并点击 async function waitClick(page, selector, isXPath = false) { let elementHandle; try { if (isXPath) { // 使用page.waitForXPath等待XPath元素,并确保可见 const elements = await page.waitForXPath(selector, { visible: true, timeout: 10000 }); // 如果XPath返回多个元素,通常我们希望点击第一个匹配项 if (Array.isArray(elements) && elements.length > 0) { elementHandle = elements[0]; } else { throw new Error(`XPath selector "${selector}" did not find any visible elements.`); } } else { // 使用page.waitForSelector等待css选择器元素,并确保可见 elementHandle = await page.waitForSelector(selector, { visible: true, timeout: 10000 }); } await elementHandle.click(); } catch (error) { console.error(`Error clicking element with selector "${selector}" (isXPath: ${isXPath}):`, error); throw error; // 重新抛出错误以便上层捕获 } } async function login(user, password) { let browser; try { browser = await puppeteer.launch({ headless: false, defaultViewport: NULL }); // 设置headless: false便于观察,defaultViewport: null防止默认视口裁剪 const page = await browser.newPage(); const url = 'https://ebanking.cpa-bank.dz/customer/'; await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); // 等待网络空闲,确保页面完全加载 // 1. 输入用户名 await page.waitForSelector('#form:username', { visible: true }); // 等待用户名输入框出现 await page.type('#form:username', user, { delay: 50 }); // 模拟人类输入,增加延迟 // 2. 点击“下一步”或提交按钮 await waitClick(page, '#form:submit'); // 3. 等待密码键盘区域加载 await page.waitForSelector('body', { visible: true }); // 等待页面体加载完毕 await waitClick(page, '#inputPassId'); // 点击密码输入区域,激活键盘 // 4. 处理密码输入 const passArr = [...password]; // 将密码字符串拆分为字符数组 for (const char of passArr) { if (/[A-Z]/.test(char)) { // 如果字符是大写字母 // 模拟按下Shift键 await waitClick(page, "xpath/" + `//button[contains(@class,"keypad-key") and text()="Shift"]`, true); // 点击大写字母 await waitClick(page, "xpath/" + `//button[contains(@class,"keypad-key") and text()="${char}"]`, true); // 模拟释放Shift键 await waitClick(page, "xpath/" + `//button[contains(@class,"keypad-key") and text()="Shift"]`, true); } else { // 点击普通字符 await waitClick(page, "xpath/" + `//button[contains(@class,"keypad-key") and text()="${char}"]`, true); } } // 5. 点击登录按钮 (根据实际页面调整选择器) // 示例中有一个显示密码的按钮,可能不需要点击,直接点击登录 // await waitClick(page, '#form:showPasswordId a'); // 如果有显示密码按钮,可能需要点击或跳过 await waitClick(page, '#form:loginButton'); // 点击最终的登录按钮 console.log('登录流程执行完毕。'); // 可以在这里添加一些断言来验证登录是否成功 // 例如:await page.waitForSelector('.dashboard-element'); } catch (error) { console.error('登录过程中发生错误:', error); throw error; // 重新抛出错误,方便外部调用者处理 } finally { if (browser) { // await browser.close(); // 登录成功或失败后关闭浏览器 } } } // 执行登录函数 (async () => { try { await login("96391281", "AadBaiudhw"); } catch (error) { console.error('主程序捕获到错误:', error); } })();
3. 代码解析与注意事项
- puppeteer.launch({ headless: false, defaultViewport: null }): headless: false 允许您在浏览器中直观地看到自动化过程,这对于调试非常有用。defaultViewport: null 可以防止Puppeteer设置默认的视口大小,而是使用浏览器本身的默认大小,这在某些响应式网站上可能更自然。
- page.goto(url, { waitUntil: ‘networkidle2’, timeout: 30000 }): waitUntil: ‘networkidle2’ 会等待页面加载完成且网络连接在500ms内没有超过2个请求,这通常比domcontentloaded或load更可靠,能确保所有资源(包括js动态加载的内容)都已加载。timeout设置了页面导航的最大等待时间。
- page.type(selector, text, { delay: 50 }): delay 参数模拟了人类打字的速度,这有助于规避一些反爬机制,并使自动化行为更自然。
- waitClick(page, selector, isXPath) 辅助函数: 这个函数封装了等待元素可见并点击的逻辑,提高了代码的健壮性。
- page.waitForSelector(selector, { visible: true, timeout: 10000 }):等待CSS选择器匹配的元素出现在DOM中并变得可见。
- page.waitForXPath(selector, { visible: true, timeout: 10000 }):等待XPath选择器匹配的元素出现在DOM中并变得可见。
- visible: true 是关键,它确保元素不仅存在于DOM中,而且用户可以看到并与之交互。
- XPath 表达式 //button[contains(@class,”keypad-key”) and text()=”a”]:
- //button:选择页面上所有的<button>元素。
- contains(@class,”keypad-key”):筛选出class属性包含keypad-key的按钮。
- text()=”a”:进一步筛选出其文本内容为”a”的按钮。
- 这种组合方式可以非常精确地定位到动态键盘上具有特定文本内容的按键,即使它们的其他属性(如ID)是动态变化的。
- […password]: 这是一个es6语法,用于将字符串转换为字符数组,方便遍历。
- 处理大小写 (/[A-Z]/.test(char)): 通过正则表达式判断字符是否为大写字母。如果是,则在点击该字母前和后分别点击“Shift”键,模拟Shift键的按下和释放,以确保输入的是大写字符。
- 错误处理与资源清理: 使用try…catch…finally结构来捕获可能发生的错误,并在finally块中确保浏览器实例被关闭,即使在发生错误的情况下也能释放资源。
总结
通过本教程,我们学习了如何使用Puppeteer有效处理动态密码键盘的自动化点击问题。关键在于:将复杂的输入分解为字符级操作,利用XPath进行精确且稳定的元素定位,并妥善处理特殊按键(如Shift)。这种方法不仅解决了Node is either not clickable or not an HTMLElement错误,也为自动化其他复杂的、非标准的用户界面元素提供了通用的策略。在实际应用中,务必根据目标网站的具体DOM结构调整CSS选择器和XPath表达式,并利用headless: false进行充分的调试。