Python開發遊戲自動化後臺腳本的實現
前言
前段時間沉迷豬場一夢江湖,由於實在太肝便萌生出用腳本做日常的想法,寫瞭第一個test.py,隨著後來各種功能的逐步添加,腳本也從前臺變成瞭支持後臺靜默運行,功能漸漸完善,包括瞭常用的
1.鼠標左鍵單擊指定坐標
2.識別並單擊指定圖像
3.識別圖像中文字
4.後臺截取程序畫面以供識別
5.鼠標滾輪上下滾動
6.鼠標左鍵范圍點擊以防檢測
7.程序中的鍵盤控制
8.程序中字符的輸入
說明
獲取窗口句柄
尋找標題為title的窗口,激活該窗口並置於x_coor, y_coor處,title可利用visual studio的spy++.exe查看;SWP_NOSIZE指定瞭窗口大小不變
def get_winds(self, title: str): """ @description : 獲取遊戲句柄 ,並把遊戲窗口置頂並激活窗口 --------- @param : 窗口名 ------- @Returns : 窗口句柄 ------- """ # self.__handle = win32gui.FindWindowEx(0, 0, "Qt5QWindowIcon", "MuMu模擬器") self.__handle = windll.user32.FindWindowW(None, title) self.__classname = win32gui.GetClassName(self.__handle) # print(self.__classname) if self.__classname == 'Qt5QWindowIcon': self.__mainhandle = win32gui.FindWindowEx(self.__handle, 0, "Qt5QWindowIcon", "MainWindowWindow") # print(self.__mainhandle) self.__centerhandle = win32gui.FindWindowEx(self.__mainhandle, 0, "Qt5QWindowIcon", "CenterWidgetWindow") # print(self.__centerhandle) self.__renderhandle = win32gui.FindWindowEx(self.__centerhandle, 0, "Qt5QWindowIcon", "RenderWindowWindow") # print(self.__renderhandle) self.__clickhandle = self.__renderhandle else: self.__clickhandle = self.__handle # self.__subhandle = win32gui.FindWindowEx(self.__renderhandle, 0, "subWin", "sub") # print(self.__subhandle) # self.__subsubhandle = win32gui.FindWindowEx(self.__subhandle, 0, "subWin", "sub") # print(self.__subsubhandle) # win32gui.ShowWindow(hwnd1, win32con.SW_RESTORE) # print(win32gui.GetWindowRect(hwnd1)) win32gui.SetWindowPos(self.__handle, win32con.HWND_TOP, x_coor, y_coor, 0, 0, win32con.SWP_SHOWWINDOW | win32con.SWP_NOSIZE) print(self.__clickhandle) return self.__handle
獲得後臺窗口截圖
窗口上方有39個像素的邊框,左、右、下則有8個像素的邊框
def get_src(self): """ @description : 獲得後臺窗口截圖 --------- @param : None ------- @Returns : None ------- """ left, top, right, bot = win32gui.GetWindowRect(self.__handle) #Remove border around window (8 pixels on each side) bl = 8 #Remove border on top bt = 39 width = int((right - left + 1) * scale) - 2 * bl height = int((bot - top + 1) * scale) - bt - bl # 返回句柄窗口的設備環境,覆蓋整個窗口,包括非客戶區,標題欄,菜單,邊框 hWndDC = win32gui.GetWindowDC(self.__handle) # 創建設備描述表 mfcDC = win32ui.CreateDCFromHandle(hWndDC) # 創建內存設備描述表 saveDC = mfcDC.CreateCompatibleDC() # 創建位圖對象準備保存圖片 saveBitMap = win32ui.CreateBitmap() # 為bitmap開辟存儲空間 saveBitMap.CreateCompatibleBitmap(mfcDC, width, height) # 將截圖保存到saveBitMap中 saveDC.SelectObject(saveBitMap) # 保存bitmap到內存設備描述表 saveDC.BitBlt((0, 0), (width, height), mfcDC, (bl, bt), win32con.SRCCOPY) ###獲取位圖信息 bmpinfo = saveBitMap.GetInfo() bmpstr = saveBitMap.GetBitmapBits(True) ###生成圖像 im_PIL = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1) # 內存釋放 win32gui.DeleteObject(saveBitMap.GetHandle()) saveDC.DeleteDC() mfcDC.DeleteDC() win32gui.ReleaseDC(self.__handle, hWndDC) ###PrintWindow成功,保存到文件,顯示到屏幕 im_PIL.save("src.jpg") # 保存 # im_PIL.show() # 顯示
數字識別
依賴項——Tesseract OCR
下載並添加至系統環境變量
註意:這裡將ocr識別范圍限定為0-9的數字以提高準確率
截取范圍為src.jpg中左上(x1,y1)到右下(x2,y2)的矩形區域
def get_num(self, x1, y1, x2, y2): """ @description : 獲取屏幕截圖中的數字 --------- @param : 截圖中需要截取的含數字部分邊界 ------- @Returns : num:int ------- """ img = Image.open("src.jpg") num_img = img.crop((x1, y1, x2, y2)) num_img = ImageOps.invert(num_img) num = pytesseract.image_to_string(num_img, lang="eng", config='--psm 6 --oem 3 -c tessedit_char_whitelist=0123456789').strip() # num = pytesseract.image_to_string(num_img, lang="eng") try: print("數量為", int(num)) return int(num) except: print("未檢測到數字") return 0
識別並點擊圖片位置
所需識別的圖片模板事先準備好並放在同一目錄下,輸入圖片文件路徑
這裡confidence設置為0.9
def mouse_click_image(self, name : str, times = 0.5): """ @Description : 鼠標左鍵點擊識別到的圖片位置 --------- @Args : name:輸入圖片名; times:單擊後延時 ------- @Returns : None ------- """ try: result = self.recognize(name) if result is None or result['confidence'] < 0.9: print("No results!") else: print(result['result'][0] + x_coor * scale + 8, " ",result['result'][1] + y_coor * scale + 39) self.mouse_click(result['result'][0] + x_coor * scale + 8, result['result'][1] + y_coor * scale + 39) except: raise Exception("error")
後臺文字輸入
def type_str(self, msg: str): """ @Description : 打字 --------- @Args : msg:目標字符 ------- @Returns : None ------- """ for i in msg: self.__PostMessageW(self.__handle, win32con.WM_CHAR, ord(i), 0)
完整代碼
GITEE網址: 項目-AutoClick.
#!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @File : AutoClick.py @Time : 2021/10/09 15:10:01 @Author : Yaadon ''' # here put the import lib import win32con import win32gui import win32ui import time # import threading import numpy as np import os from PIL import Image from PIL import ImageOps import aircv as ac import pytesseract from ctypes import windll, byref from ctypes.wintypes import HWND, POINT import string # import sys # import cv2 # from memory_pic import * # import win32api # import autopy # from PIL import ImageGrab scale = 1.25 # 電腦的縮放比例 radius = 5 # 隨機半徑 x_coor = 10 # 窗口位置 y_coor = 10 # 窗口位置 class AutoClick(): """ @description :自動點擊類,包含後臺截圖、圖像匹配 --------- @param : ------- @Returns : ------- """ __PostMessageW = windll.user32.PostMessageW __SendMessageW = windll.user32.SendMessageW __MapVirtualKeyW = windll.user32.MapVirtualKeyW __VkKeyScanA = windll.user32.VkKeyScanA __ClientToScreen = windll.user32.ClientToScreen __WM_KEYDOWN = 0x100 __WM_KEYUP = 0x101 __WM_MOUSEMOVE = 0x0200 __WM_LBUTTONDOWN = 0x0201 __WM_LBUTTONUP = 0x202 __WM_MOUSEWHEEL = 0x020A __WHEEL_DELTA = 120 __WM_SETCURSOR = 0x20 __WM_MOUSEACTIVATE = 0x21 __HTCLIENT = 1 __MA_ACTIVATE = 1 __VkCode = { "back": 0x08, "tab": 0x09, "return": 0x0D, "shift": 0x10, "control": 0x11, "menu": 0x12, "pause": 0x13, "capital": 0x14, "escape": 0x1B, "space": 0x20, "end": 0x23, "home": 0x24, "left": 0x25, "up": 0x26, "right": 0x27, "down": 0x28, "print": 0x2A, "snapshot": 0x2C, "insert": 0x2D, "delete": 0x2E, "lwin": 0x5B, "rwin": 0x5C, "numpad0": 0x60, "numpad1": 0x61, "numpad2": 0x62, "numpad3": 0x63, "numpad4": 0x64, "numpad5": 0x65, "numpad6": 0x66, "numpad7": 0x67, "numpad8": 0x68, "numpad9": 0x69, "multiply": 0x6A, "add": 0x6B, "separator": 0x6C, "subtract": 0x6D, "decimal": 0x6E, "divide": 0x6F, "f1": 0x70, "f2": 0x71, "f3": 0x72, "f4": 0x73, "f5": 0x74, "f6": 0x75, "f7": 0x76, "f8": 0x77, "f9": 0x78, "f10": 0x79, "f11": 0x7A, "f12": 0x7B, "numlock": 0x90, "scroll": 0x91, "lshift": 0xA0, "rshift": 0xA1, "lcontrol": 0xA2, "rcontrol": 0xA3, "lmenu": 0xA4, "rmenu": 0XA5 } def __get_virtual_keycode(self, key: str): """根據按鍵名獲取虛擬按鍵碼 Args: key (str): 按鍵名 Returns: int: 虛擬按鍵碼 """ if len(key) == 1 and key in string.printable: # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscana return self.__VkKeyScanA(ord(key)) & 0xff else: return self.__VkCode[key] def __key_down(self, handle: HWND, key: str): """按下指定按鍵 Args: handle (HWND): 窗口句柄 key (str): 按鍵名 """ vk_code = self.__get_virtual_keycode(key) scan_code = self.__MapVirtualKeyW(vk_code, 0) # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown wparam = vk_code lparam = (scan_code << 16) | 1 self.__PostMessageW(handle, self.__WM_KEYDOWN, wparam, lparam) def __key_up(self, handle: HWND, key: str): """放開指定按鍵 Args: handle (HWND): 窗口句柄 key (str): 按鍵名 """ vk_code = self.__get_virtual_keycode(key) scan_code = self.__MapVirtualKeyW(vk_code, 0) # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup wparam = vk_code lparam = (scan_code << 16) | 0XC0000001 self.__PostMessageW(handle, self.__WM_KEYUP, wparam, lparam) def __activate_mouse(self, handle: HWND): """ @Description : 激活窗口接受鼠標消息 --------- @Args : handle (HWND): 窗口句柄 ------- @Returns : ------- """ # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate lparam = (self.__WM_LBUTTONDOWN << 16) | self.__HTCLIENT self.__SendMessageW(handle, self.__WM_MOUSEACTIVATE, self.__handle, lparam) def __set_cursor(self, handle: HWND, msg): """ @Description : Sent to a window if the mouse causes the cursor to move within a window and mouse input is not captured --------- @Args : handle (HWND): 窗口句柄, msg : setcursor消息 ------- @Returns : ------- """ # https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor lparam = (msg << 16) | self.__HTCLIENT self.__SendMessageW(handle, self.__WM_SETCURSOR, handle, lparam) def __move_to(self, handle: HWND, x: int, y: int): """移動鼠標到坐標(x, y) Args: handle (HWND): 窗口句柄 x (int): 橫坐標 y (int): 縱坐標 """ # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove wparam = 0 lparam = y << 16 | x self.__PostMessageW(handle, self.__WM_MOUSEMOVE, wparam, lparam) def __left_down(self, handle: HWND, x: int, y: int): """在坐標(x, y)按下鼠標左鍵 Args: handle (HWND): 窗口句柄 x (int): 橫坐標 y (int): 縱坐標 """ # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown wparam = 0x001 # MK_LBUTTON lparam = y << 16 | x self.__PostMessageW(handle, self.__WM_LBUTTONDOWN, wparam, lparam) def __left_up(self, handle: HWND, x: int, y: int): """在坐標(x, y)放開鼠標左鍵 Args: handle (HWND): 窗口句柄 x (int): 橫坐標 y (int): 縱坐標 """ # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup wparam = 0 lparam = y << 16 | x self.__PostMessageW(handle, self.__WM_LBUTTONUP, wparam, lparam) def __scroll(self, handle: HWND, delta: int, x: int, y: int): """在坐標(x, y)滾動鼠標滾輪 Args: handle (HWND): 窗口句柄 delta (int): 為正向上滾動,為負向下滾動 x (int): 橫坐標 y (int): 縱坐標 """ self.__move_to(handle, x, y) # https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel wparam = delta << 16 p = POINT(x, y) self.__ClientToScreen(handle, byref(p)) lparam = p.y << 16 | p.x self.__PostMessageW(handle, self.__WM_MOUSEWHEEL, wparam, lparam) def __scroll_up(self, handle: HWND, x: int, y: int): """在坐標(x, y)向上滾動鼠標滾輪 Args: handle (HWND): 窗口句柄 x (int): 橫坐標 y (int): 縱坐標 """ self.__scroll(handle, self.__WHEEL_DELTA, x, y) def __scroll_down(self, handle: HWND, x: int, y: int): """在坐標(x, y)向下滾動鼠標滾輪 Args: handle (HWND): 窗口句柄 x (int): 橫坐標 y (int): 縱坐標 """ self.__scroll(handle, -self.__WHEEL_DELTA, x, y) def get_winds(self, title: str): """ @description : 獲取遊戲句柄 ,並把遊戲窗口置頂並激活窗口 --------- @param : 窗口名 ------- @Returns : 窗口句柄 ------- """ # self.__handle = win32gui.FindWindowEx(0, 0, "Qt5QWindowIcon", "MuMu模擬器") self.__handle = windll.user32.FindWindowW(None, title) self.__classname = win32gui.GetClassName(self.__handle) # print(self.__classname) if self.__classname == 'Qt5QWindowIcon': self.__mainhandle = win32gui.FindWindowEx(self.__handle, 0, "Qt5QWindowIcon", "MainWindowWindow") # print(self.__mainhandle) self.__centerhandle = win32gui.FindWindowEx(self.__mainhandle, 0, "Qt5QWindowIcon", "CenterWidgetWindow") # print(self.__centerhandle) self.__renderhandle = win32gui.FindWindowEx(self.__centerhandle, 0, "Qt5QWindowIcon", "RenderWindowWindow") # print(self.__renderhandle) self.__clickhandle = self.__renderhandle else: self.__clickhandle = self.__handle # self.__subhandle = win32gui.FindWindowEx(self.__renderhandle, 0, "subWin", "sub") # print(self.__subhandle) # self.__subsubhandle = win32gui.FindWindowEx(self.__subhandle, 0, "subWin", "sub") # print(self.__subsubhandle) # win32gui.ShowWindow(hwnd1, win32con.SW_RESTORE) # print(win32gui.GetWindowRect(hwnd1)) win32gui.SetWindowPos(self.__handle, win32con.HWND_TOP, x_coor, y_coor, 0, 0, win32con.SWP_SHOWWINDOW | win32con.SWP_NOSIZE) print(self.__clickhandle) return self.__handle def get_src(self): """ @description : 獲得後臺窗口截圖 --------- @param : None ------- @Returns : None ------- """ left, top, right, bot = win32gui.GetWindowRect(self.__handle) #Remove border around window (8 pixels on each side) bl = 8 #Remove border on top bt = 39 width = int((right - left + 1) * scale) - 2 * bl height = int((bot - top + 1) * scale) - bt - bl # 返回句柄窗口的設備環境,覆蓋整個窗口,包括非客戶區,標題欄,菜單,邊框 hWndDC = win32gui.GetWindowDC(self.__handle) # 創建設備描述表 mfcDC = win32ui.CreateDCFromHandle(hWndDC) # 創建內存設備描述表 saveDC = mfcDC.CreateCompatibleDC() # 創建位圖對象準備保存圖片 saveBitMap = win32ui.CreateBitmap() # 為bitmap開辟存儲空間 saveBitMap.CreateCompatibleBitmap(mfcDC, width, height) # 將截圖保存到saveBitMap中 saveDC.SelectObject(saveBitMap) # 保存bitmap到內存設備描述表 saveDC.BitBlt((0, 0), (width, height), mfcDC, (bl, bt), win32con.SRCCOPY) ###獲取位圖信息 bmpinfo = saveBitMap.GetInfo() bmpstr = saveBitMap.GetBitmapBits(True) ###生成圖像 im_PIL = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1) # 內存釋放 win32gui.DeleteObject(saveBitMap.GetHandle()) saveDC.DeleteDC() mfcDC.DeleteDC() win32gui.ReleaseDC(self.__handle, hWndDC) ###PrintWindow成功,保存到文件,顯示到屏幕 im_PIL.save("src.jpg") # 保存 # im_PIL.show() # 顯示 def recognize(self, objs): """ @description : 圖像識別之模板匹配 --------- @param : 需要匹配的模板名 ------- @Returns : 將傳進來的圖片和全屏截圖匹配如果找到就返回圖像在屏幕的坐標 否則返回None ------- """ imobj = ac.imread(objs) imsrc = ac.imread('%s\\src.jpg' % os.getcwd()) pos = ac.find_template(imsrc, imobj, 0.5) return pos def mouse_click(self, x, y, times=0.5): """ @description : 單擊左鍵 --------- @param : 位置坐標x,y 單擊後延時times(s) ------- @Returns : ------- """ # self.__set_cursor(self.__clickhandle, self.__WM_MOUSEACTIVATE) # self.__move_to(self.__clickhandle, int(x / scale), int(y / scale)) # self.__activate_mouse(self.__clickhandle) # self.__set_cursor(self.__clickhandle, self.__WM_LBUTTONDOWN) self.__left_down(self.__clickhandle, int(x / scale), int(y / scale)) self.__move_to(self.__clickhandle, int(x / scale), int(y / scale)) self.__left_up(self.__clickhandle, int(x / scale), int(y / scale)) time.sleep(times) def mouse_click_image(self, name : str, times = 0.5): """ @Description : 鼠標左鍵點擊識別到的圖片位置 --------- @Args : name:輸入圖片名; times:單擊後延時 ------- @Returns : None ------- """ try: result = self.recognize(name) if result is None or result['confidence'] < 0.9: print("No results!") else: print(result['result'][0] + x_coor * scale + 8, " ",result['result'][1] + y_coor * scale + 39) self.mouse_click(result['result'][0] + x_coor * scale + 8, result['result'][1] + y_coor * scale + 39) except: raise Exception("error") def mouse_click_radius(self, x, y, times=0.5): """ @description : 在范圍內隨機位置單擊(防檢測) --------- @param : 位置坐標x,y 單擊後延時times(s) ------- @Returns : ------- """ random_x = np.random.randint(-radius, radius) random_y = np.random.randint(-radius, radius) self.mouse_click(x + random_x, y + random_y) # self.__left_down(self.__clickhandle, int((x + random_x) / scale), int((y + random_y) / scale)) # time.sleep(0.1) # self.__left_up(self.__clickhandle, int((x + random_x) / scale), int((y + random_y) / scale)) time.sleep(times) def push_key(self, key: str, times = 1): """ @Description : 按鍵 --------- @Args : key:按鍵 times:按下改鍵後距松開的延時 ------- @Returns : None ------- """ self.__key_down(self.__clickhandle, key) time.sleep(times) self.__key_up(self.__clickhandle, key) time.sleep(0.5) def type_str(self, msg: str): """ @Description : 打字 --------- @Args : msg:目標字符 ------- @Returns : None ------- """ for i in msg: self.__PostMessageW(self.__clickhandle, win32con.WM_CHAR, ord(i), 0) if __name__ == '__main__': click = AutoClick() click.get_winds("微信") click.get_src() # click.mouse_click(254, 536) click.mouse_click_image('test.png') # click.mouse_click(1086, 269) # 輸入框 # click.mouse_click(237, 211) # 輸入框 # click.mouse_click(1228, 201) # 輸入框 # click.type_str("123木頭人abc")
參考
部分參考自: Python開發遊戲自動化腳本(四)後臺鍵鼠操作.
到此這篇關於Python開發遊戲自動化後臺腳本的實現的文章就介紹到這瞭,更多相關Python 遊戲自動化後臺腳本內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- VC中控制臺程序創建窗口的實例方法
- 使用 Python 實現微信消息的一鍵已讀的思路代碼
- Python截圖並保存的具體實例
- python基於win32實現窗口截圖
- VC通過托盤圖標得到該所屬進程的實現代碼