京东网页端登录有时候需要输入滑动验证码,就像这样:
在做自动签到脚本的时候遇到这个很不舒服,如果不处理的话就只能每次弹出浏览器手动登录,因此稍微研究了下。下面是一个非常简单,但成功率很高(达到80%)的自动识别并输入方案,使用 puppeteer 实现。
总体思路:通过图像特征识别出滑块缺口的位置,然后通过模拟用户点击将滑块拖动到该处。
首先,我们能够看到这个滑块缺口是一个黑色半透明的遮罩区域,通过代码分析并不能得到它的具体色值。因此只能自行比对尝试。通过肉眼测试与对比,可以得到这个遮罩的色值大约为 rgba(0,0,0,0.65)
得到这个色值的目的是:通过判断相邻像素点 a, b 的色值之差,来决定像素点 b 的色值是否是像素点 a 的色值加上遮罩之后的结果,以此来推断遮罩所在的位置。
下面介绍两个函数:
combineRgba
rgba 色值相加,返回相加结果
tolerance
rgba 色值比对,通过传入一个「容忍值」,返回颜色是否相似
export function combineRgba (rgba1: number[], rgba2: number[]): number[] { const [r1, g1, b1, a1] = rgba1 const [r2, g2, b2, a2] = rgba2 const a = a1 + a2 - a1 * a2 const r = (r1 * a1 + r2 * a2 - r1 * a1 * a2) / a const g = (g1 * a1 + g2 * a2 - g1 * a1 * a2) / a const b = (b1 * a1 + b2 * a2 - b1 * a1 * a2) / a return [r, g, b, a] }
export function tolerance (rgba1: number[], rgba2: number[], t: number): boolean { const [r1, g1, b1] = rgba1 const [r2, g2, b2] = rgba2 return ( r1 > r2 - t && r1 < r2 + t && g1 > g2 - t && g1 < g2 + t && b1 > b2 - t && b1 < b2 + t ) }
|
接下来就可以写出距离算法了,通过传入包含缺口的验证码图片的 base64 编码,以及图片的实际宽度,返回缺口位置 x 值。具体思路是:通过自左而右,自上而下的逐列像素分析,找出第一个跟上个像素的色值与遮罩色值相加后的结果相似的像素点,就认为是遮罩的 x 位置。
function getVerifyPosition (base64: string, actualWidth: number): Promise<number> { return new Promise((resolve, reject) => { const canvas = createCanvas(1000, 1000) const ctx = canvas.getContext('2d') const img = new Image() img.onload = () => { const width: number = img.naturalWidth const height: number = img.naturalHeight ctx.drawImage(img, 0, 0) const maskRgba: number[] = [0, 0, 0, 0.65] const t: number = 10 let prevPixelRgba = null for (let x = 0; x < width; x++) { prevPixelRgba = null for (let y = 0; y < height; y++) { const rgba = ctx.getImageData(x, y, 1, 1).data if (prevPixelRgba) { prevPixelRgba[3] = 1 const maskedPrevPixel = combineRgba(prevPixelRgba, maskRgba) if (tolerance(maskedPrevPixel, rgba, t)) { resolve(x * actualWidth / width) return } } else { prevPixelRgba = rgba } } } resolve(0) } img.onerror = reject img.src = base64 }) }
|
得到 x 位置后,就可以使用 puppeteer 操纵滑块来实现验证了:
const img = await page.$('.JDJRV-bigimg > img')
const distance: number = await getVerifyPosition( await page.evaluate(element => element.getAttribute('src'), img), await page.evaluate(element => parseInt(window.getComputedStyle(element).width), img) )
const dragBtn = await page.$('.JDJRV-slide-btn') const dragBtnPosition = await page.evaluate(element => {
const {x, y, width, height} = element.getBoundingClientRect() return {x, y, width, height} }, dragBtn)
const x: number = dragBtnPosition.x + dragBtnPosition.width / 2 const y: number = dragBtnPosition.y + dragBtnPosition.height / 2
if (distance > 10) { const distance1: number = distance - 10 const distance2: number = 10 await page.mouse.move(x, y) await page.mouse.down() await page.mouse.move(x + distance1, y, {steps: 30}) await page.waitFor(500) await page.mouse.move(x + distance1 + distance2, y, {steps: 20}) await page.waitFor(500) await page.mouse.up() } else { await page.mouse.move(x, y) await page.mouse.down() await page.mouse.move(x + distance, y, {steps: 30}) await page.mouse.up() }
await page.waitFor(3000)
|
大概就这样。