Selenium 滑动验证码处理

简述

在使用 Selenium 抓取数据的时候,遇到了滑动验证码,找了下相关资料,发现一篇博客正是想要的,非常感谢原作者。不过经试验,原文的成功率达不到需求,主要原因是模拟鼠标移动的轨迹不够理想。于是针对鼠标轨迹模拟部分进行改进,使成功率达到了 80% 左右,满足了实际使用的需求。

模拟验证过程

  1. 加载验证码页面
  2. 获取背景图片和缺口图片
  3. 根据两张图片的差异计算出平移距离
  4. 模拟鼠标移动,移动滑块到目标位置
  5. 完成验证

其中背景图片和缺口图片需要拼接还原才能得到原图。

鼠标移动轨迹优化

滑动验证码主要是通过滑动轨迹来区分人和机器,所以模拟移动的轨迹就是关键部分。越接近人类滑动的轨迹,成功率就越高。那么最真实的轨迹就是采集真实用户的鼠标轨迹,然后还原出来。

轨迹采集

轨迹采集使用 GhostMouse 这款软件,可以将采集的轨迹保存成脚本文件,文件的格式大概是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
{Delay 0.45}
{LMouse down (858,563)}
{Delay 0.25}
{Move (858,563)}
{Delay 0.04}
{Move (858,563)}

...

{Move (1001,559)}
{Delay 0.66}
{LMouse up (1002,559)}

轨迹转化

有了真实的轨迹之后,需要抽象转化为 Java 对象,方便还原。轨迹主要的动作有四个:按下、释放、移动、延时,移动动作的绝对坐标转换为相对上个位置的偏移坐标,使用 MouseTrack 表示一次动作。

MouseTrack.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 鼠标轨迹
* @author cengt
*/
public class MouseTrack {
public enum OP {
DELAY, MOVE, MOUSE_UP, MOUSE_DOWN;
}

/** 动作 */
private OP mOp;

/** 延时时间(ms) */
private int mDelay;

/** 移动偏移量 */
private Point mPoint;

...

}

读取保存的轨迹文件:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class MouseTrackManager {

...

/** 读取 GhostMouse 记录的鼠标轨迹 */
public void readTracks(String trackPath) {
File dir = new File(trackPath);
File[] files = dir.listFiles();
if (files == null || files.length == 0) {
logger.error("no mouse track files: {}", trackPath);
return;
}

for (File trackFile : dir.listFiles()) {
try {
List<String> lines = FileUtils.readLines(trackFile, "UTF-8");
int startX = 0, endX = 0;
int startY = 0;
int lastX = 0, lastY = 0;
List<MouseTrack> tracks = new ArrayList<>();
for (String line : lines) {
int delay = 0:
Point point = null;
OP op = null;
if (line.contains("Delay")) {
String delayString = line.replaceAll("[^0-9.]*", "");
delay = (int) (Float.parseFloat(delayString) * 1000);
op = OP.DELAY;
} else if (line.contains("LMouse down")) {
point = getPoint(line);
startX = point.x;
startY = point.y;
lastX = startX;
lastY = startY;
op = OP.MOUSE_DOWN;
} else if (line.contains("LMouse up")) {
point = getPoint(line);
endX = point.x;
op = OP.MOUSE_UP;
} else if (line.contains("Move")) {
Point p = getPoint(line);
point = new Point(p.x - lastX, p.y - lastY);
lastX = p.x;
lastY = p.y;
op = OP.MOVE;
}

if (op == null) {
logger.error("unsupported op: {}", line);
} else {
tracks.add(new MouseTrack(op, delay, point));
}
}

// 以移动的偏移量作为 Key 保存
int offset = endX - startX;
mMouseTracks.put(offset, tracks);
} catch (IOException e) {
e.printStackTrace();
}
}

logger.info("read tracks: {}", mMouseTracks.size());
}

private Point getPoint(String line) {
String[] axis = line.replaceAll("[^0-9,]*", "").split(",");
int x = Integer.parseInt(axis[0]);
int y = Integer.parseInt(axis[1]);
return new Point(x, y);
}

...

}

轨迹还原

有了轨迹的相关数据之后,就可以对轨迹进行还原了。

获取轨迹

根据目标位移距离,选择一个最接近的轨迹来还原。

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
/**
* 选取轨迹
* @param xDis 目标位移距离
* @return 最接近目标距离的轨迹
*/
private List<MouseTrack> getMouseTrack(int xDis) {
Map<Integer, List<MouseTrack>> tracks = mTrackManager.getMouseTracks();
List<MouseTrack> result = tracks.get(xDis);
if (result != null) {
return result;
}

int offset = 0;
int min = Integer.MAX_VALUE;
for (int k : tracks.keySet()) {
int t = Math.abs(xDis - k);
if (t < min) {
offset = k;
min = t;
}
}

result = mTrackManager.getMouseTracks().get(offset);
return result;
}

执行轨迹动作

一个轨迹是一系列动作的列表,依次执行每个动作就可以还原出来了。

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
37
38
private void moveWithMouseTrack(WebDriver driver, WebElement element, int distance) 
throws InterruptedException {
List<MouseTrack> tracks = getMouseTrack(distance);
if (tracks == null) {
logger.error("no match track: {}", distance);
return;
}

Actions actions = new Actions(driver);
actions.moveToElement(element);
for (MouseTrack track : tracks) {
switch (track.getOp()) {
case DELAY:
int delay = track.getDelay();
// 忽略时间太短的延时动作
if (delay >= 60) {
delay -= 160;
}
if (delay > 60) {
Thread.sleep(delay);
}
break;

case MOUSE_DOWN:
actions.clickAndHold(element).perform();
break;

case MOUSE_UP:
actions.release(element).perform();
break;

case MOVE:
Point point = track.getPoint();
actions.moveByOffset(point.x, point.y).perform();
break;
}
}
}

有个地方需要注意一下,由于延时这个动作使用 Thread.sleep() 实现,但是这个方法是有误差的,而且执行次数较多,累计起来的误差就会较大,导致移动速度比原来的慢。所以对于延时这个动作,忽略掉延时时间太短,以加快整个动作的流畅性。

最终效果图

无图无真相,上图!

源码

源码,是没有的,自己写吧。不要像我这样那么懒。