
前提准备什么是游戏自动化游戏自动化是指通过对游戏的界面结构的解析或界面图像的处理与识别再模拟人工对软件进行的各种操作从而实现自动化达到解放双手节约时间提高效率的目标。在本教程中我们要先实现自动刷快手这个功能再实现一个叫《豆腐女孩》的自动游戏脚本。通过图像的处理通过轮廓识别出图像中的目标计算出要操作的位置或时间再通过模拟点击或划动的方式实现对手机的自动化操作。为什么使用模拟器因为通过图像的处理和识别无论真机还是模拟器解决方案和实现思路都是一致的为了方便学员构造环境本教程使用运行在 Windows 下的模拟器进行演示。模拟器怎么下载安装进入雷电模拟器官方网站雷电模拟器官网_安卓模拟器_手游模拟器下载 点击模拟器下载按钮下载完成安装后并打开待用。快手极速版怎么下载安装https://z4gvregzdz.feishu.cn/file/boxcnm4ZYHsEDRO5eWIKUfoEXxc 下载后把 apk 文件拖入到模拟器窗口中就会自动安装。游戏怎么下载安装https://z4gvregzdz.feishu.cn/file/boxcnBPgwu80WEvWHMeMWsMr0Jd 下载后把 apk 文件拖入到模拟器窗口中就会自动安装。窗口与截图什么是pywin32库pywin32是一个第三方模块库主要的作用是方便python开发者快速调用windows API的一个模块库。同时pywin32也是绝大部分windows上第三方python模块库的前提比如wmi如果没有安装pywin32是无法正常使用wmi这个三方模块库的。安装pywin32由于pywin32是第三方模块库Python并没有自带所以我们使用之前必须先按照安装也很简单只需要再pycharm下方终端 Terminal窗口中输入pip install pywin32 回车就可以自动下载安装了。看到success这样的字样则是说明安装成功了。如果你不放心还可以通过 pip list 可以看到pywin32是否已经安装成功了pywin32提供的模块Pywin32提供的模块有很多常用的几个如下win32api 提供了常用的用户API函数win32gui 提供了有关windows用户界面图形操作的API函数win32con 提供了定义windows Api内的宏win32clipboard 提供了有关粘贴板的API函数win32console 提供了有关控制台的API函数win32service 提供了有关服务操作的API函数win32file 提供了有关文件操作的API函数我们平时最常用的的模块有3个 win32api win32guiwin32con下面我们就导入 win32apiwin32con这两个模块写一个简单的弹出框顺便验证下我们pywin32是否安装成功了。 上代码import win32api import win32con win32api.MessageBox(0, 我是渣男教父, 我是标题, win32con.MB_OK | win32con.MB_ICONWARNING)运行下看看什么是窗口句柄这里我们要先说下句柄的概念通俗点说句柄就是窗口的身份证号是一串整数。举个栗子你有你自己的身份证号,一报身份证号你应该知道是你了。你也有名字但是大家都知道名字并不是唯一的你可以叫张三别人也可以叫张三而且没有数字来得方便。所以窗口句柄就相当于身份证号每个窗口都有一个唯一编号, 操作系统用这个编号来发送消息。这就是操作系统的消息机制。获取到窗口句柄我们就可以通过这个句柄对窗口进行操作了。查找窗口句柄pywin32是如何查找窗口句柄的这里我们用到了win32gui模块以及模块的FindWindow方法。直接看代码看看import win32gui hwnd win32gui.FindWindow(None, 雷电模拟器) print(hwnd) hwnd2 win32gui.FindWindow(None, 英雄联盟) print(hwnd2)运行下看看5506644 0我们现在详细解析下win32gui.FindWindow(0, wdname)这个函数这个函数其实就是根据窗口标题获取窗口句柄# 函数功能该函数获得一个顶层窗口的句柄该窗口的类名和窗口名与给定的字符串相匹配。这个函数不查找子窗口。在查找时不区分大小写 # 参数1 窗口类名 # 参数2 窗口标题--必须完整如果该参数为None则为所有窗口全匹配 # 返回值如果函数成功返回值为窗口句柄如果函数失败返回值为0因为我电脑上现在打开了雷电模拟器有这个窗口所以结果打印了雷电模拟器窗口对应的句柄数字我没有打开英雄联盟的窗口所以没有得到相应的句柄返回值为0。激活窗口我们大多数情况下操作窗口都是需要窗口在最前方这样对窗口的操作才会行之有效。所以接下来我们来写一个通用的方法来查找并激活窗口如果查找到就使之前台显示并返回句柄。我们将文件名保存为获取句柄激活.py 直接上代码import win32con import win32gui import time def activate_window(title): hwnd win32gui.FindWindow(None, title) if hwnd 0: print(未找到游戏窗口) return 0 if win32gui.GetForegroundWindow() ! hwnd: win32gui.SetForegroundWindow(hwnd) time.sleep(1) return hwnd def activate_emulator(): return activate_window(雷电模拟器) if __name__ __main__: print(activate_emulator())运行下先手动将窗口放置到后台不要最小化最小化会有问题。这样我们就实现了将后台窗口重新激活放置到前台了。这里又用到了两个win32gui的新方法。win32gui.GetForegroundWindow()这个方法是获取当前前台窗口的句柄win32gui.SetForegroundWindow(hwnd)这个方法则是将hwnd对应的窗口前台显示。程序的逻辑也很简单首先判断雷电模拟器的窗口是否存在如果存在则检查当前前台窗口hwnd与雷电模拟器的窗口hwnd是否一致如果一致则不需要处理否则就将雷电模拟器前台显示最后再返回hwnd信息。截图平时大家聊qq时经常会用到截图功能这是一个相当实用的功能。那我们今天也用Python来写一个截图功能。我们先看下桌面截图的代码这个代码是只截取了部分代码并不完整只是用于展示。后面会有完整代码。 上代码import win32gui import win32ui import win32con import numpy as np def capture_screen(hwnd): # 根据窗口句柄获取窗口的设备上下文DCDivice Context desktop win32gui.GetDesktopWindow() dc win32gui.GetWindowDC(desktop) # 根据窗口的DC获取mfcDC mfc_dc win32ui.CreateDCFromHandle(dc) # mfcDC创建可兼容的DC save_dc mfc_dc.CreateCompatibleDC() # 创建bitmap准备保存图片 save_bit_map win32ui.CreateBitmap() left, top, right, bottom win32gui.GetWindowRect(hwnd) w, h right - left, bottom - top # 为bitmap开辟空间 save_bit_map.CreateCompatibleBitmap(mfc_dc, w, h) # 高度saveDC将截图保存到saveBitmap中 save_dc.SelectObject(save_bit_map) # 截取从左上角00长宽为wh的图片 save_dc.BitBlt((0, 0), (w, h), mfc_dc, (left, top), win32con.SRCCOPY) signed_ints_array save_bit_map.GetBitmapBits(True) im_opencv np.frombuffer(signed_ints_array, dtypeuint8) im_opencv.shape (h, w, 4) save_dc.DeleteDC() win32gui.DeleteObject(save_bit_map.GetHandle()) win32gui.ReleaseDC(hwnd, dc) return im_opencvfrombuffer(buffer, dtypefloat, count-1, offset0) 作用将缓冲区data以流的形式读入转化成ndarray对象Parameters:buffer:目标缓冲区对象dtype:默认浮点型count -1表示缓冲区中所有数据offset 0,从0开始读取截图方法最终返回的是一个ndarray对象也就是一个数字组成的矩阵。截图方法写好了这个方法用到的知识点比较繁琐大家暂时知道怎么用就好了只需要传入窗口句柄就可以得到相应的图像了。结合前面说到的获取句柄模块 【 获取句柄激活.py 】 我们来截图看看最终结果的ndarray是怎样的上代码import win32gui import win32ui import win32con import numpy as np import get_handle_activate as window def capture_screen(hwnd): # 根据窗口句柄获取窗口的设备上下文DCDivice Context desktop win32gui.GetDesktopWindow() dc win32gui.GetWindowDC(desktop) # 根据窗口的DC获取mfcDC mfc_dc win32ui.CreateDCFromHandle(dc) # mfcDC创建可兼容的DC save_dc mfc_dc.CreateCompatibleDC() # 创建bitmap准备保存图片 save_bit_map win32ui.CreateBitmap() left, top, right, bottom win32gui.GetWindowRect(hwnd) w, h right - left, bottom - top # 为bitmap开辟空间 save_bit_map.CreateCompatibleBitmap(mfc_dc, w, h) # 高度saveDC将截图保存到saveBitmap中 save_dc.SelectObject(save_bit_map) # 截取从左上角00长宽为wh的图片 save_dc.BitBlt((0, 0), (w, h), mfc_dc, (left, top), win32con.SRCCOPY) signed_ints_array save_bit_map.GetBitmapBits(True) im_opencv np.frombuffer(signed_ints_array, dtypeuint8) im_opencv.shape (h, w, 4) save_dc.DeleteDC() win32gui.DeleteObject(save_bit_map.GetHandle()) win32gui.ReleaseDC(hwnd, dc) return im_opencv def get_emulator_image(): hwnd window.activate_emulator() return capture_screen(hwnd) if __name__ __main__: image get_emulator_image() print(image)main函数作为代码入口我们看看截图的流程其实就是两步第一步找到并激活窗口获得它的hwnd第二步根据它的hwnd截图是不是很简单。我们运行下看看效果[[[ 65 63 60 255] [ 65 63 60 255] [ 65 63 60 255] ... [153 136 123 255] [195 170 144 255] [195 182 168 255]] [[ 65 63 60 255] [ 65 63 60 255] [ 65 63 60 255] ...以下省略这个数据看起来比较复杂这里我们先暂时不用管它接下来我们还会讲到opencv的模块到时候我们再来处理这个数据。这里需要注意一点电脑显示设置中的缩放与布局必须是100%否则截图会不对。ps上面面代码中的capture_screen()方法以及activate_window()方法是可以复用的因为我们后面会大量用到激活雷电模拟器和截取模拟器的图像所以我们又封装了activate_emulator()跟get_emulator_image()两个函数。这个文件可保存为window_capture.py。鼠标点击我们玩消消乐就需要点击两个相邻的小方块进行交换只需要用鼠标分别点击一次就可实现交互了所以这一节课我们来学习下如何如何利用python在Windows系统下调用win32 api来实现一些系统级的功能比如控制鼠标点击来实现游戏辅助。回顾在窗口模块的时候有说过由于python并没有自带pywin32库文件需要自己动手安装pywin32这个库安装方法是在pycharm底下的终端Terminal窗口中输入pip install pywin32 然后回车就可自动安装了。模拟点击命令实现鼠标模拟点击我们需要用到win32api中的两个函数一个是win32api.SetCursorPos用于设置鼠标当前位置 另一个就是win32api.mouse_event 用于发送鼠标指令。举个最简单的例子我们要点击某个图标我们是需要把鼠标移动到那个图标上方然后进行左键点击。点击操作其实也是有两个步骤的鼠标点下去与鼠标弹起。我直接代码实操一下import win32api import win32con desktop_coord (0, 0) # 括号中分别是x, y坐标单位是像素 win32api.SetCursorPos(desktop_coord) # 设置鼠标位置 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) # 鼠标按下 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) # 鼠标弹起运行下看看可以看到鼠标跑到屏幕最左上角的位置去了。因为左上角没什么东西所以点击效果好像看不出来没关系后面我们还会继续举更多例子到时候就能看到更直观的效果了。接下来我们来看下代码:前面两行是导入win32api跟win32con模块;第四行是赋值给desktop_coord这个变量为 0,0, 这个括号表示一个坐标前一个0是横向x坐标后一个0是纵向坐标y 坐标的单位是像素而在windows中桌面坐标的起始点是从桌面左上角开始计算的。所以刚才我们运行的时候鼠标跑到了最左上角去点了一下;第五行代码则是设置鼠标位置为桌面坐标;第六行第七行是一个连贯的操作分别发送鼠标左键按下与鼠标左键弹起的指令。短短几行代码一套鼠标点击某个位置的流程就实现了是不是很简单。屏幕坐标和窗口坐标上面我们点击坐标点0,0鼠标是跑到了桌面最左上角去了。这里我们就要普及下坐标系的概念了。Windows 窗体的坐标系基于设备坐标设备坐标包括三种客户区坐标窗口坐标屏幕坐标它的特点是以左上角为原点x轴向右递增y轴向下递增单位是像素。如下图所示屏幕坐标包含整个屏幕屏幕的左上角为0,0); 窗口坐标包含一个程序的整个窗口比如雷电模拟器整个窗口包含标题栏菜单滚动条和窗口框窗口的左上角为0,0。眼尖的同学应该已经率先发现点击窗口坐标中的某个坐标其实也约等于点击桌面中的某个坐标。所以当我们想点击模拟器窗口中的某个坐标时可以通过坐标转化为点击桌面中的某个坐标。win32gui也确实提供了这个相应的方法方法名为 ClientToScreen(窗口句柄窗口坐标)这个函数会返回对应的桌面坐标。眼见为实我们通过代码来验证一下。首先我们来封装一下点击函数def 点击(桌面坐标): win32api.SetCursorPos(桌面坐标) # 设置鼠标位置 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) # 鼠标按下 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) # 鼠标弹起 def 点击窗口坐标(窗口句柄, 窗口坐标): 桌面坐标 win32gui.ClientToScreen(窗口句柄, 窗口坐标) # 窗口坐标换算成桌面坐标 点击(桌面坐标)我们点击下模拟器右边菜单图标326, 15位置看看再两个坐标系的坐标是多少我们改下代码加点打印坐标的语句。 上代码import win32api import win32gui import win32con def 点击(桌面坐标): win32api.SetCursorPos(桌面坐标) # 设置鼠标位置 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) # 鼠标按下 win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) # 鼠标弹起 def 点击窗口坐标(窗口句柄, 窗口坐标): 桌面坐标 win32gui.ClientToScreen(窗口句柄, 窗口坐标) # 窗口坐标换算成桌面坐标 print(窗口坐标 str(窗口坐标)) print(桌面坐标 str(桌面坐标)) 点击(桌面坐标) if __name__ __main__: 句柄 win32gui.FindWindow(None, 雷电模拟器) 窗口坐标 (326, 15) 点击窗口坐标(句柄, 窗口坐标)我们把模拟器置顶运行下看看。窗口打印出窗口坐标(326, 15) 桌面坐标(372, 44)鼠标也成功点击到模拟器顶上的菜单图标弹出了菜单。实战-自动刷快手刷快手跟直接点击就不一样了就需要有个滑动操作了。模拟器的分辨率设置是720*1280滑动操作所谓滑动就是先按下鼠标左键然后移动到目标位置再松开鼠标左键。 上代码import time import win32api import win32gui import win32con def 向上划动(窗口句柄, 窗口起点坐标, 划动距离): 桌面坐标 win32gui.ClientToScreen(窗口句柄, 窗口起点坐标) # 窗口坐标换算成桌面坐标 win32api.SetCursorPos(桌面坐标) # 设置鼠标位置 time.sleep(0.1) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) # 鼠标按下 time.sleep(0.1) for i in range(1, 划动距离 1, 10): 轨迹坐标 (窗口起点坐标[0], 窗口起点坐标[1] - i) 桌面坐标 win32gui.ClientToScreen(窗口句柄, 轨迹坐标) # 窗口坐标换算成桌面坐标 win32api.SetCursorPos(桌面坐标) # 设置鼠标位置 time.sleep(0.01) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) # 鼠标弹起 if __name__ __main__: 句柄 win32gui.FindWindow(None, 雷电模拟器) win32gui.SetForegroundWindow(句柄) # 窗口前置 time.sleep(0.5) 起点坐标 (300, 750) 向上划动(句柄, 起点坐标, 500)本章小结这一章咱们从零开始把游戏自动化的地基给打牢了。来回顾一下都学了啥窗口句柄是啥玩意儿简单说就是窗口的身份证号一串唯一的整数。有了它我们就能精准找到要操作的窗口不会张冠李戴。用win32gui.FindWindow()就能根据窗口标题拿到句柄找不到就返回0。坐标系怎么理解的Windows里有屏幕坐标和窗口坐标两种。屏幕坐标以整个屏幕左上角为(0,0)窗口坐标以窗口左上角为(0,0)。想点击窗口里的某个位置得用win32gui.ClientToScreen()把窗口坐标转成屏幕坐标不然鼠标会点错地方。鼠标点击怎么模拟的一套完整的点击动作其实分三步走先用SetCursorPos()把鼠标移到目标位置再用mouse_event()发送左键按下的指令最后再发送左键弹起的指令。这三步缺一不可光按下不弹起或者光弹起不按下都不行。滑动操作咋实现的滑动就是在按下的基础上中间加了一段移动轨迹。我们在代码里用了一个循环每次移动一小段距离中间加点延时这样滑起来才自然不会太生硬。截图是怎么回事截图的核心流程是拿到窗口句柄 → 获取设备上下文 → 创建兼容DC和位图 → 把窗口内容拷贝到位图里 → 转成numpy数组。返回的是一个四维矩阵每个像素点包含RGBA四个通道的值。后面学了OpenCV就能处理这些数据了。激活窗口为啥重要很多操作都得窗口在前台才有效所以得先用SetForegroundWindow()把窗口提到最前面。但要注意窗口不能是最小化状态最小化了会有问题。这些知识点看似零散但其实都是一条线上的找到窗口 → 激活窗口 → 截图看画面 → 算出坐标 → 模拟点击或滑动。把这条线串起来游戏自动化的基本套路就掌握了。动手试试选择题1、窗口坐标系的起始点0,0是从窗口的哪个位置开始计算的A、左上角B、右上角C、左下角D、右下角答案A解析Windows的坐标系都是以左上角为原点的x轴向右递增y轴向下递增。不管是屏幕坐标还是窗口坐标(0,0)都在左上角。这个跟咱们平时数学课学的笛卡尔坐标系不一样数学里原点在左下角y轴向上递增。但在计算机图形学里几乎都是从左上角开始的记住这个就行。2、在Python中利用pywin32获取到的窗口句柄都是整数这句话正确吗A、正确B、不正确答案A解析窗口句柄本质上就是一个整数你可以理解成操作系统给每个窗口分配的唯一编号。代码里打印出来的5506644这种就是句柄值。如果窗口不存在FindWindow()会返回00也是个整数。所以句柄确实是整数类型的。编程挑战3、完善自动刷快手程序让它能自动刷10个视频并且每隔一个视频点一次赞双击屏幕可点赞。参考代码import time import win32gui import win32api import win32con def 点击(桌面坐标): win32api.SetCursorPos(桌面坐标) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) def 双击(窗口句柄, 窗口坐标): 桌面坐标 win32gui.ClientToScreen(窗口句柄, 窗口坐标) 点击(桌面坐标) time.sleep(0.1) # 两次点击之间要有点间隔 点击(桌面坐标) def 向上划动(窗口句柄, 窗口起点坐标, 划动距离): 桌面坐标 win32gui.ClientToScreen(窗口句柄, 窗口起点坐标) win32api.SetCursorPos(桌面坐标) time.sleep(0.1) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) time.sleep(0.1) for i in range(1, 划动距离 1, 10): 轨迹坐标 (窗口起点坐标[0], 窗口起点坐标[1] - i) 桌面坐标 win32gui.ClientToScreen(窗口句柄, 轨迹坐标) win32api.SetCursorPos(桌面坐标) time.sleep(0.01) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) if __name__ __main__: 句柄 win32gui.FindWindow(None, 雷电模拟器) win32gui.SetForegroundWindow(句柄) time.sleep(1) 起点坐标 (360, 800) # 屏幕中间偏下的位置适合滑动 for i in range(10): print(f正在刷第 {i1} 个视频) time.sleep(5) # 看5秒视频 # 每隔一个视频点一次赞第2、4、6、8、10个视频 if (i 1) % 2 0: print(点赞) 双击(句柄, (360, 640)) # 双击屏幕中间位置点赞 time.sleep(1) 向上划动(句柄, 起点坐标, 500) # 向上滑动到下一个视频 time.sleep(2) # 等视频加载 print(刷完10个视频啦)思路解析这道题考察的是前面学的点击和滑动操作的组合运用。首先双击点赞其实就是快速点击两次同一个位置中间要加个短暂的延时0.1秒左右不然系统可能识别不出来是双击。其次每隔一个视频意味着第2、4、6、8、10个视频要点赞用(i 1) % 2 0就能判断当前是不是偶数个视频。然后滑动操作直接复用前面写好的向上划动()函数就行起点坐标选在屏幕中间偏下的位置比较合适这样滑起来跟真实用户操作差不多。最后每个视频之间要加点等待时间一个是让视频有加载的时间另一个是模拟真人看视频的节奏别滑得太快不然容易被系统判定为异常操作。