python基于pywinauto實(shí)現(xiàn)PC端自動(dòng)化 python操作微信自動(dòng)化
一、 pywinauto安裝和啟動(dòng)
1.安裝:
pip install pywinauto
2.backend選擇 和 控件查看工具inspect介紹
我們安裝好Pywinauto之后,首先要確定哪種可訪問(wèn)性技術(shù)(backend)可以用于我們的應(yīng)用程序,在windows上受支持的有兩種:
-
Win32 API (
backend= "win32") 默認(rèn)的backend -
MS UI Automation (
backend="uia")
如果不能確定程序到底適用于那種backend,可以借助于GUI對(duì)象檢查工具來(lái)做,常用的檢查工具有Inspect.ex,Spy++ ,下載地址:https://github.com/blackrosezy/gui-inspect-tool
giithub的項(xiàng)目中的inspect好像不行了,可以用下面這個(gè):
鏈接:https://pan.baidu.com/s/1LHvbcP5NKqSHC7FLSpiTFQ
提取碼:p4hm
將inspect左上角的下拉列表中切換到“UI Automation”,然后鼠標(biāo)點(diǎn)一下你需要測(cè)試的程序窗體,inspect就會(huì)顯示相關(guān)信息,如下圖所示。說(shuō)明backend為uia
程序里面的任意一個(gè)部位其實(shí)都是控件,在inspect的控件樹中都可以找到,是一層一層分級(jí)別的,可以一個(gè)個(gè)點(diǎn)開所有控件

2.啟動(dòng)(實(shí)例化程序):以微信示例
from pywinauto.application import Application # 常用方式一:連接已有微信進(jìn)程(進(jìn)程號(hào)在 任務(wù)管理器-詳細(xì)信息 可以查看,項(xiàng)目中一般根據(jù)進(jìn)程名稱自動(dòng)獲?。?/span> app = Application(backend='uia').connect(process=8948) # 常用方式二:?jiǎn)?dòng)微信進(jìn)程 (注意路徑中特殊字符的轉(zhuǎn)義,/和\,不注意有時(shí)會(huì)出錯(cuò)) app = Application(backend="uia").start(r'C:\Program Files (x86)\Tencent\WeChat\WeChat.exe')
3.Application對(duì)象app的常用方法
通過(guò)查看pywinauto的源碼中application.py文件,可以看到app的所有屬性方法,下面列舉常用方法:
app.top_window() # 返回應(yīng)用程序當(dāng)前頂部窗口,是WindowSpecification對(duì)象,可以繼續(xù)使用對(duì)象的方法往下繼續(xù)查找控件 # eg:如:app.top_window().child_window(title='地址和搜索欄', control_type='Edit') app.window(**kwargs) # 根據(jù)篩選條件,返回一個(gè)窗口, 是WindowSpecification對(duì)象,可以繼續(xù)適用對(duì)象的方法往下繼續(xù)查找控件 # eg: 微信主界面 app.window(class_name='WeChatMainWndForPC') app.windows(**kwargs) # 根據(jù)篩選條件返回一個(gè)窗口列表,無(wú)條件默認(rèn)全部,列表項(xiàng)為wrapped對(duì)象,可以使用wrapped對(duì)象的方法,注意不是WindowSpecification對(duì)象 # eg:[<uiawrapper.UIAWrapper - '李渝的早報(bào) - Google Chrome', Pane, -2064264099699444098>] app.kill(soft=False) # 強(qiáng)制關(guān)閉 app.cpu_usage() # 返回指定秒數(shù)期間的CPU使用率百分比 app.wait_cpu_usage_lower(threshold=2.5, timeout=None, usage_interval=None) # 等待進(jìn)程CPU使用率百分比小于指定的閾值threshold app.is64bit() # 如果操作的進(jìn)程是64-bit,返回True
二、控件定位方法和控件可用方法
操作控件需要以下幾個(gè)步驟:
第一步 實(shí)例化要操作的進(jìn)程:得到的app是Application對(duì)象
第二步 選擇窗口 :app.window('一個(gè)或多個(gè)篩選條件') 得到的窗口是WindowSpecification對(duì)象
第三步:基于WindowSpecification對(duì)象使用其方法再往下查找,定位到具體的控件
第四步:使用控件的方法屬性執(zhí)行我們需要的操作
WindowSpecification源碼中有一些自帶的方法可以直接使用,也有注釋說(shuō)到:
""" A specification for finding a window or control Windows are resolved when used. You can also wait for existance or non existance of a window .. implicitly document some private functions .. automethod:: __getattribute__ .. automethod:: __getitem__ """
就是說(shuō)這是一個(gè)查找空間或者窗口的規(guī)范,可以使用等待機(jī)制。
并且該對(duì)象中__getattribute__和__getitem__兩個(gè)魔術(shù)方法,隱式地記錄一些私有方法
我的理解是我們可以繼續(xù)往下一層一層的查找,下面一層一層的控件其實(shí)是各種各樣的wrapper對(duì)象,wrapper有很多種是一系列對(duì)象,對(duì)象源碼都在pywinauto源碼的controls目錄中
以下總結(jié)了常用方法,基本可以滿足所有場(chǎng)景的操作,如下:
2.1 層級(jí)查找控件的方法
# 通過(guò)層級(jí)查找控件相關(guān)方法+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ window(**kwargs) # 用于窗口的查找 child_window(**kwargs) # 可以不管層級(jí)的找后代中某個(gè)符合條件的元素,最常用 parent() # 返回此元素的父元素,沒有參數(shù) children(**kwargs) # 返回符合條件的子元素列表,支持索引,是BaseWrapper對(duì)象(或子類) iter_children(**kwargs) # 返回子元素的迭代器,是BaseWrapper對(duì)象(或子類) descendants(**kwargs) # 返回符合條件的所有后代元素列表,是BaseWrapper對(duì)象(或子類) iter_children(**kwargs) # 符合條件后代元素迭代器,是BaseWrapper對(duì)象(或子類)
2.2 kwargs篩選條件
常用的一些篩選條件:
# 這些是常用的 class_name=None, # 類名 class_name_re=None, # 正則匹配類名 title=None, # 控件的標(biāo)題文字,對(duì)應(yīng)inspect中Name字段 title_re=None, # 正則匹配文字 control_type=None, # 控件類型,inspect界面LocalizedControlType字段的英文名 best_match=None, # 這個(gè)有坑,我不喜歡用,下文有講解 auto_id=None, # 這個(gè)也是固定的可以用,inspect界面AutomationId字段,但是很多控件沒有這個(gè)屬性
# 下面這些不常用,基本用不到 parent=None, process=None,# 這個(gè)基本不用,每次啟動(dòng)進(jìn)程都會(huì)變化 top_level_only=True, visible_only=True, enabled_only=False, handle=None, ctrl_index=None, found_index=None, predicate_func=None, active_only=False, control_id=None, framework_id=None, backend=None,
2.3 控件可用的方法屬性
# 以下幾個(gè)只支持窗口模式的控件======================================================================= dlg.close() # 關(guān)閉界面 dlg.minimize() # 最小化界面 dlg.maximize() # 最大化界面 dlg.restore() # 將窗口恢復(fù)為正常大小,比如最小化的讓他正常顯示在桌面 dlg.get_show_state() # 正常0,最大化1,最小化2 dlg.exists(timeout=None, retry_interval=None) # 判斷是否存在 #timeout:等待時(shí)間,一般默認(rèn)5s #retry_interval:timeout內(nèi)重試時(shí)間 dlg.wait(wait_for, timeout=None, retry_interval=None) # 等待窗口處于特定狀態(tài) dlg.wait_not(wait_for_not, timeout=None, retry_interval=None) # 等待窗口不處于特定狀態(tài),即等待消失 # wait_for/wait_for_not: # * 'exists' means that the window is a valid handle # * 'visible' means that the window is not hidden # * 'enabled' means that the window is not disabled # * 'ready' means that the window is visible and enabled # * 'active' means that the window is active # timeout:等待多久 # retry_interval:timeout內(nèi)重試時(shí)間 # eg: dlg.wait('ready') # 鼠標(biāo)鍵盤操作 ===================================================================================== # 我只列舉常用形式,他們有很多默認(rèn)參數(shù)但不常用,可以在源碼中查看 ctrl.click_input() # 最常用的點(diǎn)擊方法,一切點(diǎn)擊操作的基本方法(底層調(diào)用只是參數(shù)不同),左鍵單擊,使用時(shí)一般都使用默認(rèn)不需要帶參數(shù) ctrl.right_click_input() # 鼠標(biāo)右鍵單擊 # 鍵盤輸入,底層還是調(diào)用keyboard.send_keys ctrl.type_keys(keys, pause = None, with_spaces = False,) # keys:要輸入的文字內(nèi)容 # pause:每輸入一個(gè)字符后等待時(shí)間,默認(rèn)0.01就行 # with_spaces:是否保留keys中的所有空格,默認(rèn)去除0 ctrl.double_click_input(button ="left", coords = (None, None)) # 左鍵雙擊 ctrl.press_mouse_input(coords = (None, None)) # 指定坐標(biāo)按下左鍵,不傳坐標(biāo)默認(rèn)左上角 ctrl.release_mouse_input(coords = (None, None)) # 指定坐標(biāo)釋放左鍵,不傳坐標(biāo)默認(rèn)左上角 ctrl.move_mouse_input(coords=(0, 0)) # 將鼠標(biāo)移動(dòng)到指定坐標(biāo),不傳坐標(biāo)默認(rèn)左上角 ctrl.drag_mouse_input(dst=(0, 0)) # 將ctrl拖動(dòng)到dst,是press-move-release操作集合 # 控件的常用屬性=================================================================================== ctrl.children_texts() # 所有子控件的文字列表,對(duì)應(yīng)inspect中Name字段 ctrl.window_text() # 控件的標(biāo)題文字,對(duì)應(yīng)inspect中Name字段 # ctrl.element_info.name ctrl.class_name() # 控件的類名,對(duì)應(yīng)inspect中ClassName字段,有些控件沒有類名 # ctrl.element_info.class_name ctrl.element_info.control_type # 控件類型,inspect界面LocalizedControlType字段的英文名 ctrl.is_child(parent) # ctrl是否是parent的子控件
ctrl.legacy_properties().get('Value') # 可以獲取inspect界面LegacyIAccessible開頭的一系列字段,在源碼uiawraper.py中找到了這個(gè)方法,非常有用
#如某些按鈕顯示值是我們想要的,但是window_text獲取到的是固定文字‘修改群昵稱’,這個(gè)值才是我們修改后的新名字
# 控件常用操作======================================================================================== ctrl.draw_outline(colour='green') # 空間外圍畫框,便于查看,支持'red', 'green', 'blue' ctrl.print_control_identifiers(depth=None, filename=None) # 打印其包含的元素,詳見打印元素 ctrl.scroll(direction, amount, count=1,) # 滾動(dòng) # direction :"up", "down", "left", "right" # amount:"line" or "page" # count:int 滾動(dòng)次數(shù) ctrl.capture_as_image() # 返回控件的 PIL image對(duì)象,可繼續(xù)使用其方法如下: eg: ctrl.capture_as_image().save(img_path) ret = ctrl.rectangle() # 控件上下左右坐標(biāo),(L430, T177, R1490, B941),可.輸出上下左右 eg: ret.top=177 ret.bottom=941 ret.left=430 ret.right=1490
三、具體使用舉例
第二節(jié)中列舉了能用到的方法屬性,本節(jié)列舉實(shí)際操作中的具體用法
1.對(duì)話框dialog選擇
根據(jù)pywinauto的源碼中application.py文件介紹,窗口選擇有三種方式:
Once you have an Application instance you can access dialogs in that application either by using one of the methods below. :: dlg = app.YourDialogTitle dlg = app.child_window(title="your title", classname="your class", ...) dlg = app['Your Dialog Title']
以微信主界面窗口為例:

# 微信主界面幾種方式: # 這個(gè)最好用,下面幾種不指名道姓容易出錯(cuò)且速度很慢 dlg1 = app.window(class_name='WeChatMainWndForPC') # 是WindowSpecification對(duì)象 # 下面幾種方法速度慢,我是不喜歡用 # dlg2_1 = app.Dialog # dlg2_2 = app.微信 # dlg3_1 = app['Dialog'] # dlg3_2 = app['微信']
2.打印元素
我們拿到控件后,是可以將該控件下的所有子控件及其屬性以樹形結(jié)構(gòu)打印出來(lái)的:
# 拿到微信主窗口 win_main_Dialog = app.window(class_name='WeChatMainWndForPC') # 判斷是否為dialog,一個(gè)微信是一個(gè)dialog,就是窗口 print(win_main_Dialog.is_dialog) # 給控件畫個(gè)紅色框便于看出是哪個(gè) win_main_Dialog.draw_outline(colour = 'red') # 打印當(dāng)前窗口的所有controller(控件和屬性) win_main_Dialog. print_control_identifiers(depth=None, filename=None) # 源碼內(nèi)部函數(shù)名鏈?zhǔn)劫x值了,都能用,一樣的 # print_ctrl_ids = dump_tree = print_control_identifiers
depth:打印的深度,缺省時(shí)打印最大深度。
filename:將返回的標(biāo)識(shí)存成文件(生成的文件與當(dāng)前運(yùn)行的腳本在同一個(gè)路徑下)
eg:dlg. print_control_identifiers(filename =’a.txt’)
打印出來(lái)的文檔樹就是inspect中的控件樹完全展開的樣子,都是有層級(jí)的,和微信程序中的各個(gè)元素是一一對(duì)應(yīng)的:

3 常用查找方法
# 拿到微信主窗口 win_main_Dialog = app.window(class_name='WeChatMainWndForPC') # 主窗口下的某個(gè)窗口,不管層級(jí)的找 chat_list = win_main_Dialog.child_window(control_type='List', title='會(huì)話') first = chat_list.items()[0] # 第一個(gè)聊天項(xiàng) 列表支持items(),支持循環(huán),支持索引 # 詳情頁(yè)修改備注操作 parent()和children()都是只往上或往下查找一個(gè)層級(jí),所有滿足的放進(jìn)列表 details_page = win_main_Dialog.child_window(class_name='ContactProfileWnd') # 窗口下的某個(gè)窗口 we_id = details_page.child_window(title="微信號(hào):", control_type="Text").parent().children()[1].window_text() # 窗口的爸爸的第二個(gè)兒子的文字 alia = details_page.child_window(title="微信號(hào):", control_type="Text").parent().parent().children()[0].children()[0].window_text() edit_btn = details_page.child_window(title="備 注", control_type="Text").parent().children()[1] edit_btn.click_input() btn_modify_name_edit = edit_btn # 先ctrl+a選中所有然后再type_keys替換 btn_modify_name_edit.type_keys('^a').type_keys('備注名字', with_spaces=True) # descendants查找所有后代中滿足的,不管層級(jí),所有滿足的放進(jìn)列表 btns_list = win_main_Dialog.child_window(control_type='ToolBar').parent().descendants(control_type='Button') btns_list[0].click_input() dialog.child_window(title="文件名(N):", auto_id="1148", control_type="Edit")
4 快速定位
定位一個(gè)元素我們可以一層一層定位,但是這樣真就有點(diǎn)笨蛋了,不僅效率低下還不容易適應(yīng)結(jié)構(gòu)變化,可以先定位某個(gè)頁(yè)面,打印出頁(yè)面結(jié)構(gòu),然后基于頁(yè)面快速定位

def we_name(self): # todo+++++++++++++++++++++++++++++++++++++ try: self._popup = wechat.win_main.child_window(class_name='ContactProfileWnd') self._popup.wait('visible') self._popup.print_control_identifiers(depth=None, filename=None) print(self._popup.Edit.window_text()) # www.pu?? print(self._popup.Edit0.window_text()) # www.pu?? print(self._popup.Edit1.window_text()) # www.pu?? print(self._popup.Edit2.window_text()) # qwer1315458571 print(self._popup.child_window(best_match='微信號(hào):Edit').window_text()) # qwer1315458571 print(self._popup.child_window(best_match='Edit2').window_text()) # qwer1315458571 return self._popup.Edit.window_text() # return self._popup.child_window(title="微信號(hào):", control_type="Text").parent().parent().children()[0].children()[0].window_text() except: return None
四、控件自帶的的方法
1. 點(diǎn)擊和輸入
# 左點(diǎn)擊,可以點(diǎn)進(jìn)源碼,還有double_click_input,right_click_input等 edit_btn.click_input() # 先ctrl+a選中所有然后再type_keys替換,和我們選中然后修改一樣的 edit_btn.type_keys('^a').type_keys('備注名字', with_spaces=True)
SHIFT + CTRL ^ ALT % 空格鍵 {SPACE} BACKSPACE {BACKSPACE}、{BS} or {BKSP} BREAK {BREAK} CAPS LOCK {CAPSLOCK} DEL or DELETE {DELETE} or {DEL} DOWN ARROW {DOWN} END {END} ENTER {ENTER} or ~ ESC {ESC} HELP {HELP} HOME {HOME} INS or INSERT {INSERT} or {INS} LEFT ARROW {LEFT} NUM LOCK {NUMLOCK} PAGE DOWN {PGDN} PAGE UP {PGUP} PRINT SCREEN {PRTSC} RIGHT ARROW {RIGHT} SCROLL LOCK {SCROLLLOCK} TAB {TAB} UP ARROW {UP} + {ADD} - {SUBTRACT} * {MULTIPLY} / {DIVIDE}
常規(guī)使用很方便,但是有些字符,比如微信中的用戶昵稱什么的帶有表情等特殊符號(hào),用自帶的輸入方法就會(huì)不適用,可以使用keyboard模塊(見下)
2.對(duì)控件截圖并保存
ctrl_qrcode = self.win_login.child_window(title='二維碼', control_type='Image') if ctrl_qrcode.exists(): ctrl_qrcode.capture_as_image().save(img_path)
capture_as_image() 方法 返回控件的其實(shí)是 PIL image對(duì)象,所以可用該方法的屬性方法,比如save
3.窗口的等待
窗口加載需要時(shí)間,我們又不能一直sleep就需要等待,等待窗口出現(xiàn)或者等待窗口關(guān)閉:
save_dialog.wait('ready',timeout=2) save_dialog.close() save_dialog.wait_not('visible') # 'exists':窗口是有效的句柄 # 'visible':窗口未隱藏,常用 # 'enabled':未禁用窗口 # 'ready':窗口可見并啟用,常用 # 'active':窗口處于活動(dòng)狀態(tài)
4.窗口存在和關(guān)閉
self.chatwnd = wechat.app.window(class_name='ChatWnd') if self.chatwnd.exists(): self.chatwnd.close()
5.其他
# 頂層窗口 dlg = app.top_window() # 點(diǎn)方法取值 print(dlg.class_name()) #'WeChatMainWndForPC' # 滾動(dòng) 常用于頁(yè)面的滾動(dòng),比如好友列表、聊天列表、消息界面 chat_list.scroll(direction='up', amount='page')
五、鼠標(biāo)操作
pywinauto自帶的鼠標(biāo)操作有些時(shí)候并不能完全滿足要求,可以調(diào)用mouse的方法
導(dǎo)入:
from pywinauto import mouse
常見操作:
# 移動(dòng)鼠標(biāo) mouse.move(coords=(x, y)) # 指定位置,鼠標(biāo)左擊 mouse.click(button='left', coords=(40, 40)) # 鼠標(biāo)雙擊 mouse.double_click(button='left', coords=(140, 40)) # 將屬性移動(dòng)到(140,40)坐標(biāo)處按下 mouse.press(button='left', coords=(140, 40)) # 將鼠標(biāo)移動(dòng)到(300,40)坐標(biāo)處釋放, mouse.release(button='left', coords=(300, 40)) # 右鍵單擊指定坐標(biāo) mouse.right_click(coords=(400, 400)) # 鼠標(biāo)中鍵單擊指定坐標(biāo)(很少用的到) mouse.wheel_click(coords=(400, 400)) # 滾動(dòng)鼠標(biāo) wheel_dist指定鼠標(biāo)滾輪滑動(dòng),正數(shù)往上,負(fù)數(shù)往下。 mouse.scroll(coords=(1200,300),wheel_dist=-3)
示例:
# 以控件中心為起點(diǎn),滾動(dòng) def mouse_scroll(control, distance): rect = control.rectangle() cx = int((rect.left+rect.right)/2) cy = int((rect.top + rect.bottom)/2) mouse.scroll(coords=(cx, cy), wheel_dist=distance) mouse_scroll(control=win_main_Dialog.child_window(control_type='List', title='聯(lián)系人'), distance=-5)
六、鍵盤操作
和控件自己的type_keys方法效果一樣,但是更快,那個(gè)是從前到后啪啪啪的輸入,這個(gè)是一下就出來(lái)了那種
在發(fā)送文件和圖片的時(shí)候可以使用鍵盤模塊,復(fù)制粘貼,比啪啪啪輸入路徑再發(fā)送速度快多了
并且該模塊可以適配很多表情等特殊符號(hào)
import keyboard import io for line in io.StringIO(msg): keyboard.write(line.strip()) # keyboard.send('ctrl+enter') keyboard.write(chat_name) keyboard.send('enter') keyboard.send('ctrl+v')
純干貨,可以說(shuō)是全網(wǎng)最詳細(xì)最全面講解,如果確實(shí)幫助到了你,右側(cè)打賞一分或者點(diǎn)個(gè)推薦吧~

浙公網(wǎng)安備 33010602011771號(hào)