前言
前一阵子打球认识了一个小学生,为了骗他钱,吹了好半天,什么盗取心上人的QQ密码,吃鸡外挂,学校饭卡无限充值,最终达成交易10块钱帮他刷QQ赞。
回家赶快百度研究了一下,原来刷赞很简单:很多网站都可以每天免费领取100个赞,心想把这些网站统计一下,写个程序每天自动提交不就可以了。但是我仔细看了一下,几乎所有的网站都需要人工验证才能免费领取,由于暂时我的客户数量较少,先手工领一下吧。
然后又发现一个站可以通过分享推广链接来刷。想起来之前帮别人用PHP模拟IP刷票,同理应该可以用来刷赞。于是简单写了个页面,每天能刷几千个赞。
给小学生刷了几万个赞,取得他的信任后,我说让他做总代理,也不收他费用了,让他发展下线,让他的同学朋友都来刷赞,然后给他15%的抽成,他很高兴。
我开始考虑怎么用程序过人工验证,这样才能实现每天免费领取几百万个赞,建立我的刷赞帝国。
代码正文
本文以geetest官方demo为例,使用nodejs与selenium模拟人工滑动验证码。
先百度一番,发现都是一两年前的文章,没有现成的代码。读来读去其实思路都差不多:
- 模拟点击
- 获取缺口的座标位置
- 模拟鼠标拖动滑块
一、模拟点击
selenium和nodejs的安装使用就不赘述了,官网文档都有。
新建一个firefox实例:
let options = new firefox.Options()
.setProfile('C:/Users/2bt/AppData/Roaming/Mozilla/Firefox/Profiles/m1mwcwm8.default')
let driver = await new webdriver.Builder().forBrowser('firefox').setFirefoxOptions(options).build()
我这里添加的设置项是为了每次调试的时候在firefox中改动的参数保留下来,比如浏览器的位置,大小。不然每次启动firefox都是新的临时窗口。其次是为了加载我专门为了方便测试写的一个模拟鼠标指针的扩展插件(selenium不显示鼠标的移动轨迹),但比较郁闷的是自己写的扩展没法安装。
打开极验首页,并且把验证区域置顶:
await driver.get('http://www.geetest.com/en/')
// 置顶DOM
// await driver.executeScript('window.scrollBy(0, 5142)')
await driver.executeScript('document.querySelector(".gt-en--experience").style.position = "absolute";document.querySelector(".gt-en--experience").style.top = 0')
注释掉的那个代码可能会因为浏览器/分辨率/系统的不同产生误差,所以直接把页面CSS的position设置为absolute放在页面顶部就可以。这样也为了方便后面截取缺口的图片(若使用第一种方案,直接截图得不到想要的区域,需要截取完整的页面图片,然后计算缺口在页面的相对位置,再裁剪图片才能得到,很麻烦)。
选择SLIDE方式验证并且点击验证按钮:
let slide = await driver.wait(webdriver.until.elementLocated(webdriver.By.css('.experience-box .box-left ul li:nth-child(2)')), 10000)
await slide.click();
console.log('选择滑动验证')
let btn = await driver.wait(webdriver.until.elementLocated(webdriver.By.css('.geetest_btn')), 10000);
await btn.click()
console.log('点击验证')
加个监听鼠标移动的特效,模拟鼠标指针:
await driver.executeScript('ox=document.createElement("div");oy=document.createElement("div");tip=document.createElement("div");ox.style.width="100%";ox.style.pointerEvents="none";ox.style.height="1px";ox.style.zIndex=32767;ox.style.backgroundColor="#ddd";ox.style.position="fixed";ox.style.left=0;document.body.appendChild(ox);oy.style.pointerEvents="none";oy.style.height="100%";oy.style.width="1px";oy.style.zIndex=32767;oy.style.backgroundColor="#ddd";oy.style.position="fixed";oy.style.top=0;document.body.appendChild(oy);tip.id="sele-mouse";tip.style.position="fixed";tip.style.top=0;tip.style.left=0;tip.style.zIndex=32767;tip.style.backgroundColor="#fffa";document.body.appendChild(tip);document.οnmοusemοve=function(e){e=e||event;x=e.pageX;y=e.pageY-document.documentElement.scrollTop;ox.style.top=y+"px";oy.style.left=x+"px";document.getElementById("sele-mouse").innerHTML="x:"+x+"<br/>y:"+y;};document.querySelector(".geetest_slider_button").addEventListener("mousemove",function(e){e=e||event;x=e.pageX;y=e.pageY-document.documentElement.scrollTop;ox.style.top=y+"px";oy.style.left=x+"px";document.getElementById("sele-mouse").innerHTML="x:"+x+"<br/>y:"+y},true);document.querySelector(".geetest_slider_button").addEventListener("mousedown",function(e){ox.style.backgroundColor="red";oy.style.backgroundColor="red";},true);document.querySelector(".geetest_slider_button").addEventListener("mouseup",function(e){ox.style.backgroundColor="#ddd";oy.style.backgroundColor="#ddd";},true);')
这样selenium做模拟鼠标移动操作的时候,就能方便观察了。
二、获取缺口的座标位置
先找到验证码背景图的页面DOM, 是一个 canvas,隐藏滑块后截图:
const bgCanvas = await driver.wait(webdriver.until.elementLocated(webdriver.By.css('.geetest_window')), 10000)
//把滑块的透明度改成0
await driver.executeScript('document.querySelector(".geetest_canvas_bg").style.opacity = 1;document.querySelector(".geetest_canvas_slice").style.opacity = 0;')
const bgPng = await bgCanvas.takeScreenshot()
把截取的图片保存后分析出缺口座标/位置:
fs.writeFile("bg.png", bgPng,{encoding: 'base64'}, function(err) {
if(err){
console.log('截取背景图片错误')
}else{
getPixels('bg.png',function(err, pixels) {
if(err) {
console.log("读取背景缺口位置错误")
return
}
bgX = getBoundary(pixels,getGrey(pixels)/10*3)
console.log('读取背景缺口边界X座标: ',bgX);
})
}
});
//获取灰度值
function getGrey(pixels){
var rgb = 0;
for(var i=0; i < pixels.shape[0]; i++) {
for(var j=0; j < pixels.shape[1]; j++) {
rgb += pixels.get(i,j,0) + pixels.get(i,j,1) + pixels.get(i,j,2)
}
}
return Math.floor(rgb/pixels.shape[0]/pixels.shape[1])
}
//getBoundary查找边界函数。
function getBoundary(pixels, level){
var lastY=0, count=0;
for(var i=0; i < pixels.shape[0]; i++) {
for(var j=0; j < pixels.shape[1]; j++) {
var rgb = pixels.get(i,j,0) + pixels.get(i,j,1) + pixels.get(i,j,2)
if(rgb < level && lastY+1 == j) {
count++;
//console.log(rgb, i,j ,count);
}
else count = 0;
lastY = j;
if(count > 6) return i;
}
}
return 0;
}
我把这个截取到的图片称为”缺口背景图片”,下文还会出现“缺口图片”
分析图片像素用的是get-pixels。后面的getBoundary和getGrey这两个函数不太精确,但应付demo效果还差不多。
getBoundary的原理是读取图片所有像素相对黑的点(RGB加起来小于一定的值,纯黑的RGB和是0),如果满足多个点位处于同一条竖线,也就是X座标一致,Y座标相邻,就判定这是缺口的一个边。
getGrey是获取图片的灰度值,原理是把所有的像素点RGB值加起来,然后除以全部点数,得到一个平均值。这个函数是为了辅助getBoundary,因为不同颜色深浅的带有缺口的图片,缺口位置的黑色浓度也不一样,有时候特别黑,有时一般,如果不变通,有时会取不到边界座标。
由于拖动滑块时会产生一个随机的位移,计算需要模拟拖动的距离要把这个位移去掉:
// 获取拼图滑块按钮
const button = await driver.wait(webdriver.until.elementLocated(webdriver.By.css('.geetest_slider_button')), 5000)
// 初始化 action
let actions = driver.actions({async: true})
console.log('开始模拟拖动')
// 把鼠标移动到滑块上, 然后点击
await actions.move({
origin: button,
duration: 1000
}).pause(100).press().move({
origin: button,
x: 1,
duration: 10
}).pause(300).perform()
这里模拟滑动了1px,可以看到缺口图片随机向右移动了一段距离,这时把背景隐藏,再去单独截取“缺口图片”。
缺口图片的边界座标很容易获取,因为背景色是白色的,相对黑的像素很容易找,PS里面取左边阴影的RGB均为118,所以这里用的是固定值360,基本不会出现偏差。
getPixels('slice.png',function(err, pixels) {
if(err) {
console.log("读取初始位置错误")
return
}
dragX = getBoundary(pixels,360)
})
//需要滑动的距离
distance = bgX - dragX;
有了“缺口图片”和“缺口背景图片”的边界座标,我们接下来就可以模拟鼠标拖动滑块了。
三、模拟鼠标拖动滑块
我刚开始采用的是直接匀速拖动:
async function move(distance){
await driver.sleep(1000)
let actions = driver.actions({bridge: true})
await actions.move({
origin: webdriver.Origin.POINTER,
x:distance,
duration:1000
}).release().perform()
console.log("滑动距离: ",distance)
}
这样当然是不行的,太机器化了,验证无法通过。
然后我就依照网上的思路,让鼠标先做匀加速运动,超过缺口一段距离后,然后再拽回缺口:
function getTrack(distance){
var track = [],
current = 0,
mid = distance * 4 / 5,
t = 0.4,
v = 0;
while (current < distance){
if(current < mid) a = 2
else a = -3
v0 = v
v = v0 + a * t
move = v0 * t + 1 / 2 * a * t * t
current += move
track.push(Math.round(move))
}
return track
}
通过这个getTrack函数,返回单位时间运动距离的数组,按照数组去拖拽滑块就莫得问题了。
模拟拖拽有个大坑就是,可能不同环境分辨率有差别,canvas截图取得的图片是325200像素的,网页中canvas的大小是260160,呈5:4的比例,导致我从截取的图片计算出来的距离,模拟拖拽的时候总是会拖拽过头,我一直以为是geetest为了防止机器加入的随机量,费了好大的劲写了很多检测方法才搞明白是比例问题,最终把计算出来的距离按比例缩小就可以得到精确值。
最终效果如下图:
由于图片取边界算法以及计算运动位移函数的粗略,导致模拟拖拽会产生微小的偏差,识别率还没达到50%。还有图片存储过程等方法需要进一步优化,下一篇刷赞程序待续。