使用Python + win32api实现简单自动鼠标点击
使用tkinter设计GUI界面并用pyinstaller打包
不知不觉肝阴阳师也快一年了,对这游戏真是又爱又恨,最近刚刚发布了PC版,突然很想尝试着写个脚本挂机,话不多说进入正题。
基础模拟点击
简单的鼠标操作
游戏挂机脚本,无非就是自动移动鼠标,自动点击,进行重复操作,所以,第一步就是如何控制鼠标
1
2
3
4
5
6
7
8
9
10
11
12
13
14import win32api
import time
def move_click(x, y, t=0): # 移动鼠标并点击左键
win32api.SetCursorPos((x, y)) # 设置鼠标位置(x, y)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN |
win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) # 点击鼠标左键
if t == 0:
time.sleep(random.random()*2+1) # sleep一下
else:
time.sleep(t)
return 0
# 测试
move_click(30, 30)
当然,再后续操作中你可能需要获取屏幕分辨率,我只打算让脚本能在自己电脑上跑就满足了,所以没有实现适配不同分辨率
1
2def resolution(): # 获取屏幕分辨率
return win32api.GetSystemMetrics(0), win32api.GetSystemMetrics(1)
值得注意的是,一定要在管理员权限下的cmd中运行,否则点击无效
这个时候,你已经可以写个while循环,不停地点击屏幕上不同的几个点了,最基础的挂机脚本就实现了
使用PIL识别图像
我们肯定不满足于机械式的连续点击,万一被封号呢…
所以需要识别图像,再进行点击
首先,就需要定位到阴阳师的窗口
1
2
3
4
5
6
7
8
9
10import win32gui
def get_window_info(): # 获取阴阳师窗口信息
wdname = u'阴阳师-网易游戏'
handle = win32gui.FindWindow(0, wdname) # 获取窗口句柄
if handle == 0:
# text.insert('end', '小轩提示:请打开PC端阴阳师 ')
# text.see('end') # 自动显示底部
return None
else:
return win32gui.GetWindowRect(handle)
get_window_info()函数返回阴阳师窗口信息(x1, y1, x2, y2),(x1, y1)是窗口左上角的坐标,(x2, y2)是窗口右下角的坐标,代码中的text可以暂时忽略,这在后续GUI界面中用于输出提示信息。
下面使用PIL获取游戏截图
1
2
3
4
5
6
7
8
9
10
11
12def get_posx(x, window_size): # 返回x相对坐标
return (window_size[2] - window_size[0]) * x / 870
def get_posy(y, window_size): # 返回y相对坐标
return (window_size[3] - window_size[1]) * y / 520
topx, topy = window_size[0], window_size[1]
img_ready = ImageGrab.grab((topx + get_posx(500, window_size), topy + get_posy(480, window_size),
topx + get_posx(540, window_size), topy + get_posy(500, window_size)))
# 查看图片
im_ready.show()
考虑到窗口大小不同,位置会有所偏移,这里使用屏幕上点的相对位置
获取到关键位置的截图之后,计算图片的hash值
1
2
3
4
5def get_hash(img):
img = img.resize((16, 16), Image.ANTIALIAS).convert('L') # 抗锯齿 灰度
avg = sum(list(img.getdata())) / 256 # 计算像素平均值
s = ''.join(map(lambda i: '0' if i < avg else '1', img.getdata())) # 每个像素进行比对,大于avg为1,反之为0
return ''.join(map(lambda j: '%x' % int(s[j:j+4], 2), range(0, 256, 4)))
将关键位置截图的hash值保存下来,下次脚本运行时,将截图hash值与原始hash值进行比对,判断是否相似。
这里使用汉明距离进行计算,比较hash值中相同位置上不同元素的个数
1
2
3
4
5
6def hamming(hash1, hash2, n=20):
b = False
assert len(hash1) == len(hash2)
if sum(ch1 != ch2 for ch1, ch2 in zip(hash1, hash2)) < n:
b = True
return b
准备工作做完了,下面就可以开心刷御灵了
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
52def yu_ling(window_size):
global is_start
topx, topy = window_size[0], window_size[1]
state = []
while is_start:
# print 'start'
# text.insert('end', 'start')
time.sleep(0.5)
img_ready = ImageGrab.grab((topx + get_posx(750, window_size), topy + get_posy(465, window_size),
topx + get_posx(840, window_size), topy + get_posy(500, window_size)))
if hamming(get_hash(img_ready), ready_hash, 10):
state.append(0)
move_click(topx + get_posx(740, window_size), topy + get_posy(380, window_size))
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 点击准备 ')
text.see('end') # 自动显示底部
time.sleep(15)
continue
img_success = ImageGrab.grab((topx + get_posx(400, window_size), topy + get_posy(320, window_size),
topx + get_posx(470, window_size), topy + get_posy(400, window_size)))
if hamming(get_hash(img_success), success_hash):
time.sleep(2)
state.append(1)
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 成功%d次 ' % state.count(1))
text.see('end') # 自动显示底部
move_click(topx + get_posx(730, window_size), topy + get_posy(380, window_size))
continue
img_fail = ImageGrab.grab((topx + get_posx(560, window_size), topy + get_posy(340, window_size),
topx + get_posx(610, window_size), topy + get_posy(390, window_size)))
if hamming(get_hash(img_fail), fail_hash):
time.sleep(2)
state.append(2)
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 失败%d次 ' % state.count(2))
text.see('end') # 自动显示底部
move_click(topx + get_posx(720, window_size), topy + get_posy(380, window_size))
continue
img_attack = ImageGrab.grab((topx + get_posx(615, window_size), topy + get_posy(350, window_size),
topx + get_posx(675, window_size), topy + get_posy(375, window_size)))
if hamming(get_hash(img_attack), yu_attack_hash):
move_click(topx + get_posx(670, window_size), topy + get_posy(365, window_size))
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 点击进攻 ')
text.see('end') # 自动显示底部
state.append(3)
if state[-6:] == [3]*6:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 痴汉券可能不够了 ')
text.see('end') # 自动显示底部
click()
break
continue
至此,我们已经可以通过管理员cmd运行脚本了。
但这样的脚本运行起来比较麻烦,也没有好看的界面。接下来,我们将使用tkinter设计GUI界面,并用pyinstaller打包成.exe文件
GUI
tkinter
tkinter是Python内置的GUI设计界面,对小白来说容易上手,你也可以尝试用pyqt或者wx
关于tkinter可以看一下莫烦教程
首先创建一个窗口,并设置必要信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import tkinter as tk
from icon import img
window = tk.Tk() # 创建一个窗口
window.title('奴良小轩v0.1')
window.geometry('240x480+120+30') # 窗口的位置以及大小
# 设置图标
with open('tmp.ico', 'wb+') as fp:
fp.write(base64.b64decode(img))
window.iconbitmap('tmp.ico')
os.remove('tmp.ico')
# 设置图标
label = tk.Label(window, font=('微软雅黑', 12),
text='请将PC端阴阳师调节与小宝等高') # 显示一段文本
label.pack()
设置图标
默认情况下,窗口图标是红色的TK,想修改则使用.iconbitmap(path)方法,但是,在实际使用踩坑了。因为后面我会使用pyinstaller打包,因为找不到path路径运行程序会报错,找了好久才找到这个错误。
解决方案是先将图标读取并写入ico.py文件,调用.iconbitmap(path)时读取ico.py,代码如下:
1
2
3
4
5
6
7
8import base64
open_icon = open('yaodao.ico', 'rb')
b64str = base64.b64encode(open_icon.read())
open_icon.close()
write_data = "img = '%s'" % b64str
f = open('icon.py', 'w+')
f.write(write_data)
f.close()
功能选择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# Radiobutton #
fun_var = tk.IntVar()
fun_text = ''
def print_selection():
global fun_text
if fun_var.get() == 1:
fun_text = '寮突破'
elif fun_var.get() == 2:
fun_text = '御灵、业原火'
elif fun_var.get() == 3:
fun_text = '魂十队员(未完成)'
elif fun_var.get() == 4:
fun_text = '魂十队长(未完成)'
elif fun_var.get() == 5:
fun_text = '狗粮队员(未完成)'
label.config(text='功能选择: ' + fun_text)
rb1 = tk.Radiobutton(window, text='寮突破', font=('微软雅黑', 10),
variable=fun_var, value=1, command=print_selection)
rb1.place(x=15, y=30)
rb2 = tk.Radiobutton(window, text='御灵、业原火', font=('微软雅黑', 10),
variable=fun_var, value=2, command=print_selection)
rb2.place(x=15, y=60)
rb3 = tk.Radiobutton(window, text='魂十队员', font=('微软雅黑', 10),
variable=fun_var, value=3, command=print_selection)
rb3.place(x=15, y=90)
rb4 = tk.Radiobutton(window, text='魂十队长', font=('微软雅黑', 10),
variable=fun_var, value=4, command=print_selection)
rb4.place(x=15, y=120)
rb5 = tk.Radiobutton(window, text='狗粮队员', font=('微软雅黑', 10),
variable=fun_var, value=5, command=print_selection)
rb5.place(x=15, y=150)
# Radiobutton #
开始按钮start_mission()中定义了每一个功能所要执行的函数,注意的是,独立功能需要放在一个线程中执行,不然界面会被阻塞卡死
全局变量is_start用来控制功能的执行与停止
click()函数用来改变按钮显示以及锁定功能选择
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# button start#
rb_list = [rb1, rb2, rb3, rb4, rb5]
button_var = tk.StringVar()
button_var.set('开始')
is_click = False
def start_mission():
global is_start
if fun_var.get() == 1:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 开始执行寮突破 ')
text.see('end') # 自动显示底部
window_size = get_window_info()
if window_size: # 打开了阴阳师
window.geometry('240x480+%d+%d' % (window_size[0]-240, window_size[1]))
is_start = True
thread1 = threading.Thread(target=liao_tupo, args=(window_size,))
thread1.start()
elif fun_var.get() == 2:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 开始执行御灵、业原火 ')
text.see('end') # 自动显示底部
window_size = get_window_info()
if window_size: # 打开了阴阳师
window.geometry('240x480+%d+%d' % (window_size[0] - 240, window_size[1]))
is_start = True
thread2 = threading.Thread(target=yu_ling, args=(window_size,))
thread2.start()
elif fun_var.get() == 3:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 魂十队员功能未开发 ')
text.see('end') # 自动显示底部
elif fun_var.get() == 4:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 魂十队长功能未开发 ')
text.see('end') # 自动显示底部
elif fun_var.get() == 5:
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 狗粮队员功能未开发 ')
text.see('end') # 自动显示底部
def stop_mission():
global is_start
is_start = False
text.insert('end', strftime('%H:%M:%S', localtime()) + ' 停止执行 ')
text.see('end') # 自动显示底部
def click():
global is_click
if not is_click:
is_click = True
button_var.set('停止')
label.config(text=fun_text + ' 已经开始')
for rb in rb_list: # 将选项锁定
rb.config(state='disabled')
button_adjust.config(state='disabled')
start_mission()
else:
is_click = False
button_var.set('开始')
label.config(text=fun_text + ' 已经停止')
for rb in rb_list:
rb.config(state='active')
button_adjust.config(state='active')
stop_mission()
button = tk.Button(window, textvariable=button_var, width=10,
height=1, command=click)
button.place(x=140, y=60)
# button start#
文本显示1
2
3
4import ScrolledText
text = ScrolledText.ScrolledText(window, width=29, height=17) # 滚动输出文本框
# text = tk.Text(window, width=29, height=17) # 输出文本框
text.place(x=15, y=180)
注意的一点是,再每次输出文本的时候希望自动显示低端,这时需要在insert之后执行text.see('end')
Pyinstaller打包1pyinstaller -F -w -i ./yaodao.ico ./tk_gui.py
-F表示输出单文件exe
-w表示不显示命令行
-i设置图标
更多参数设置详见这里
至此全部搞定,打开exe时记得右键管理员权限打开
Have Fun!
下一篇,简单碎片登记系统(鸽了)