Python自動化工具:pywinauto
一、pywinauto的安裝
(1)安裝命令
pip install -U pywinauto/pip3 install -U pywinauto
(2)驗證是否安裝成功
from pywinauto.application import Application
二、pywinauto的使用
1.1介紹程序的backend
首先要判斷程序是用什么語言寫的?在實例化會有區別,主要是判斷程序的backend?
程序的backend大致有兩種:
(1)Win32 API(backend=“win32”)
(2)MS UI Automation(backend=“uia”)
1.2如何判斷程序的backend?
推薦使用spy++和inspect來檢查(spy++和inspect工具的下載地址:https://github.com/blackrosezy/gui-inspect-tool);
1.3如何使用inspect來判斷backend的類別
將inspect左上角的下拉列表中切換到“UI Automation”,然后鼠標點一下你需要測試的程序窗體,inspect就會顯示相關信息。
inspect中顯示了相關的信息,如下圖所示。說明backend為uia。
如果inspect中顯示拒絕訪問,說明該程序的backend應該是win32;
這里主要是限制自動化控制進程的范圍。如一個程序有多個實例,自動化控制一個實例,而保證其他實例(進程)不受影響。
主要有兩種對象可以建立這種入口點——
-->Application()
-->Desktop()
Application的作用范圍是一個進程,如一般的桌面應用程序都為此類。
Desktop的作用范圍可以跨進程。主要用於像win10的計算器這樣包含多個進程的程序。這種目前比較少見。使用方法見:https://pywinauto.readthedocs.io/en/latest/getting_started.html#entry-points-for-automation
建立好入口后,需要連接進程;
3.1連接進程的方法
(1)使用Application對象的start()方法
start(self, cmd_line, timeout=app_start_timeout) # instance method:
cmd_line參數就是你使用命令行啟動程序的命令語句(程序路徑)
例如:
app = Application().start(r"F:\python\auto_tools\Terminal_部標_2014-01-12-11.exe")
(2)使用Application對象的connect()方法
-->使用進程ID (PID)進行綁定;
app = Application().connect(process=15860)
進程的PID可以在任務管理器中查看;
-->使用窗口句柄綁定
app = Application().connect(handle=0x00030F9A)
窗口句柄可以在Spy++中查看 :
-->使用程序路徑綁定
app=Application().connect(path=r"F:\python\auto_tools\Terminal_部標_2014-01-12-11.exe")
-->使用標題、類型等匹配
app = Application().connect(title_re="Terminal_部標_2014-01-12.*", class_name="#32770 (對話框)")
三、使用pywinauto操作窗口
1.啟動程序
(1)application.Application().start('路徑+程序名/程序名')
app = application.Application().start('notepad.exe')
例子:
myapp = Application().start("notepad") myapp.__setattr__("name","notepad")
錯誤用法:
from pywinauto import application app = application.Application.start('notepad.exe')
原因:因為start方法必須是針對應用實例的方法, 忘記了實例化操作, ()符號;
(2)MenuSelect方法自動檢索Notepad上的菜單選項
(3)decode(‘gb2312’)方法,python3使用encode('gb2312')方法是把中文強制轉換為unicode編碼,對於非英文的操作系統都是要轉換的;
例如:點擊“幫助->關於記事本”操作;
app.Notepad.MenuSelect('幫助->關於記事本'.decode('gb2312'))
附注:拋出異常
① AttributeError: 'str' object has no attribute 'decode'
主要原因:Python2和Python3在字符串編碼上的區別(必須將字節字符串解碼后才能),python在bytes和str兩種類型轉換,所需要的函數依次是encode(),decode()
解決方案:str通過encode()方法可以編碼為指定的bytes
反過來,當從網絡或磁盤上讀取了字節流,那么讀到的數據就是bytes。要把bytes變為str,就需要用decode()方法。反之,則使用encode()方法即可!正確用法如下:
app.Notepad.MenuSelect('幫助->關於記事本'.encode('gb2312').decode('gb2312'))
例如,對一個寫字板app應用中的窗口,在英文操作系統中,其標題是“untitled - Notepad”
可以使用以下兩種方式調用該窗體
app.Untitled
app.Notepad
對於關於窗口,其標題是“About Notepad”
可以使用以下名稱調用該窗體
app.AboutNotepad
這里的app是你剛才實例的對象,Notepad是類名;
- 查找/調用窗口
通過工具spy++lite查看窗口的類名和標題文字...
這里先介紹官方文檔的兩種方法:
(1)通過top_dlg = app.top_window_() 來獲取最上面的window(不推薦,如果有新進程,就會得到錯誤對象)
(2)通過find_dlg = app.window_(title_re = ‘ ’, class_name = ‘ ’) 方法獲得,title_re和 class_name這兩個可以單獨使用也可以一塊使用,因為有時沒有標題文本,也有時一個窗口類名有多個對象;
例如:
“Edit”有時當一個對話框中有多個輸入框時會有多個Edit類名,對於“關於記事本”我們可以通過以下代碼獲得-->
about_dlg = app.window_(title_re = u"關於", class_name = "#32770")
中文要進行unicode編碼,這里也可以通過decode(‘gb2312’)方法實現,也可以這樣(u”關於”)
這里通過print一下about_dlg可以確定我們得到的是一個什么對象?
例如:
<pywinauto.application.WindowSpecification object at 0x01F0A530>
說明我們得到的是一個application.WindowSpecification對象
(3)通過dlg_spec = app.window(title='')
或者app.window(title_re=’’)
- 定位對話框
例如:我要定位“關於記事本”的對話框
① 第一種方法
about_dlg= app.window_(title_re="關於",class_name="#32770") # 這里可以進行正則匹配title app.window_(title_re='關於“記事本”').window_(title_re='確定').Click()
② 第二種方法
ABOUT= '關於“記事本”' OK= '確定' # about_dlg[OK].Click() # app[ABOUT][OK].Click() app['關於“記事本”']['確定'].Click()
- 在窗口上找到某個按鈕
4.1 例如:在”關於記事本”窗口上找到“確定”按鈕(button)
① 第一種方法
(1)打印當前窗口的所有controller(控件和屬性)
about_dlg.print_control_identifiers()
注意事項:
窗口的控件和屬性打印失敗的原因,有可能是因為程序啟動的時候,但是窗口並沒有馬上出現,可以給一個合適的休眠時間即可。
例如:
在pywinauto中,對話框下面的是controller,button,checkbox,textbox等都是controller。
static,SysLink,button等是它類型,后面接的是title,都是unicode的,這里面就有沒有title的controller,再后面的(L,T,R,B)是這個控件的位置,分別對應着左上右下;
例如:在”關於記事本”窗口上找到“確定”按鈕;
可以通過app.window_()方法,傳入的參數可以是title,也可以是class_name,所以我說這兩個值相當重要,一直在用,這里的title支持正則表達式,非常方便,在app上先找到about_dlg,然后再about_dlg上找確定button,
app.window_(title_re = u'關於“記事本”').window_(title_re = u'確定'),然后通過Click()方法來單擊這個button;
② 第二種方法(非英文系統下)
OK = u'確定' about_dlg[OK].Click()
這種方法需要在about_dlg下找到u’確定’
③ 第三種方法(不需要找about_dlg)
app[u'關於“記事本”'][u'確定'].Click()
- 特殊符號快捷鍵,對應碼表
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} |
|
|
(1)使用type_keys()函數,這里需要的快捷鍵是Alt+T+P:
例如:
dlg_spec = app.window(title='屏幕錄像專家 V2017') dlg_spec.type_keys('%TP')
(2)使用type_keys()函數,光標跳到最后
app[u"Terminal_部標_2014-01-12"]['Edit'].type_keys('{END}')
(3)使用type_keys()函數,刪除輸入框的內容
app[u"Terminal_部標_2014-01-12"]['Edit'].type_keys('{BACKSPACE}' * 20)
這里輸入20個回退鍵
(4)使用TypeKeys()函數,輸入文本,不會清空原來的數據
例如:app["標題"]['類名'].TypeKeys(u"119.23.142.92")
app[u"Terminal_部標_2014-01-12"]['Edit'].TypeKeys(u"119.23.142.92")
注意:如果輸入框內初始有文本,需要將光標移到最后,然后進行刪除操作,最后才能輸入文本;
(5)app['Cy']['Edit3'].SetEditText('bbbb')
(6)#app['Cy']['Edit3'].SetText('bbbb')
(7)app['Cy']['Edit4'].set_edit_text('你好')
#(4)(5)(7)3種輸入值,與.TypeKeys區別在於,這個如果文本框禁止輸入也可強制輸入
a=app['Cy']['Edit1'].WindowText()#獲取值 b=app['Cy']['Edit3'].texts()#獲取值,返回一個數組 c=app['Cy']['Edit4'].text_block()#獲取值
6.操作窗口控件
常見的窗口程序的控件:輸入框(Edit)、按鈕(Button)、復選框(CheckBox)、單選框(RadioButton)、下拉列表(ComboBox).
關於各個控件的函數方法官網:https://pywinauto.readthedocs.io/en/latest/controls_overview.html
6.1 結合程序,控件的用法
大致用法分兩步:
● 找到控件
● 操作控件
接下來,如何讓程序找到控件?
(1)如何匹配控件
① 第一種方法
最簡單的通過空間特征進行匹配(窗體也可以看成大控件),匹配窗口的方法除了前面提到的window()方法,還可以通過中括號加窗口名,例如:
dlg_spec = app.window(title=r'EXE/EXE 轉 MP4')
dlg_spec = app[r'EXE/EXE 轉 MP4']
除了title,還可以使用class或者title+class或者相近的text和類來匹配控件,
② 第二種方法
如果我們知道了程序的層次結構,然后類似尋到DOM元素一樣一層一層的匹配;
那么我們要如何找到層次結構呢?
pywinauto提供了print_control_identifiers()函數來顯示該窗體下所有控件的結構。
得到文件名后面的編輯框的屬性為:
Edit - '' (L259, T88, R429, B107)
['EXE/EXE 轉 MP4Edit', 'Edit3']
所以我們可以通過控件的text或者title來查找控件。如:
edit = dlg_spec[''] # 1 edit = dlg_spec['Edit2'] # 2 edit = dlg_spec.Edit2 # 3
注意,對於輸入控件Edit,一般不建議使用text內容綁定,因為Edit的text內容會發生變化。另外,綁定的控件也可能不唯一。對於title,我這里可能理解不夠,屬性顯示的是Edit3,但實際上綁定的時候用的卻是Edit2,也就是數字要減一。
(2)如何操作控件
對於Edit控件,要么就是向里面寫內容,要么就是讀里面的內容。
參考地址:
https://blog.csdn.net/shawpan/article/details/78170200
① 轉換文件
例如:我們要向Edit3寫入要轉換文件的路徑(r’E:\test test .exe’),這里的文件名在中間又加了空格;
edit.set_text(r'E:\test test .exe') # 1
edit.type_keys(r'E:\test test .exe',with_spaces = True) # 2
上述代碼第一種方法是直接設置edit的text,而第二種是在里面模擬鍵盤輸入(如果字符串中沒有空格,可以省略后面的參數)
注意:使用第二種方法輸入並沒有什么效果,因為該編輯框設置了禁止輸入(自己手動敲鍵盤,發現編輯框沒有反應)
② 點擊對話框的按鈕
--> 第一種方法
about_dlg= app.window_(title_re="關於",class_name="#32770") # 這里可以進行正則匹配title
app.window_(title_re='關於“記事本”').window_(title_re='確定').Click()
使用格式:
app.window_(title_re='窗體標題').window_(title_re='Button名').Click()
--> 第二種方法
ABOUT= '關於“記事本”' OK= '確定' # about_dlg[OK].Click() # app[ABOUT][OK].Click() app['關於“記事本”']['確定'].Click()
使用格式:app['控件標題']['Button名'].Click()
--> 第三種方法
dlg_spec = app[r'EXE/EXE 轉 MP4'] dlg_spec.Button0.click()
使用格式:匹配控件方法.類名.click()
--> 第四種方法
dlg_spec = app[r'EXE/EXE 轉 MP4'] dlg_spec['瀏覽'].click()
使用格式:匹配控件方法 + ['Button名'] + . + click()
舉個例子:
手動操作的步驟: 先在查找范圍后的ComboBox中找到要轉換的源程序所在的文件夾,中間的list中就會出現該文件中的所有文件和文件夾,選中源文件,文件名后面的Edit里面就會顯示文件名,最后點擊打開按鈕完成選擇。
如果按照手動操作的步驟進行模擬,腳本編寫起來會比較復雜,這里我們使用了一個trick。直接在文件名的編輯框中輸入源文件的絕對路徑,然后點擊打開按鈕完成選擇。
dlg_open = app['打開'] dlg_open.Edit.set_text(r'E:\test test .exe') dlg_open['打開'].click()
有三點需要說明的地方:
1、 “打開”對話框中只有一個輸入框,所以,使用Edit就可以綁定文件名的編輯框。
2、 編輯框是可編輯的,也就是說我們可以使用type_keys()函數模擬鍵盤輸入文件路徑,但是需要顯式指明字符串中含有空格,不然空格會被忽略掉。而且需要先清空編輯框的內容。
dlg_open.Edit.type_keys(r'E:\test test .exe')
圖中為沒有事先清理編輯框和沒有顯式指明含有空格的結果。
3、 若文件絕對路徑輸入錯誤,點擊“打開”按鈕會先跳到路徑中最后一個文件夾,然后再點擊“打開”,提示找不到文件的錯誤。
③ 勾選選項操作
現在變成了5幀/秒,建議擴頻后不超過15幀/秒,也就是擴頻3倍即可,默認參數也是3,所以只需要勾選上“自動擴頻”即可。
dlg_spec.CheckBox0.check()
④ 處理對話框的確認按鈕
我們需要做的就是找到這個對話框,點擊里面的OK按鈕;
app['屏幕錄像專家'].Ok.click()
⑤ listview如何進行雙擊
使用DoubleClick()方法
⑥ 鼠標右擊
使用RightClick()方法
⑦ 處理下拉列表
使用select()方法
終端類型選擇“A08”,代碼如下:
⑧ 按、移動、釋放鼠標
Control.PressMouse/MoveMouse/ReleaseMouse()
- 等待窗口出現
實際操作中,我們不知道它什么時候會處理完?如上圖的OK按鈕只有處理完后彈窗后才能點擊;
(1)等待法。首先預估一個所需的最長時間,保證此時已經處理完並彈窗,然后讓程序等待這么長時間后在點擊OK按鈕;
import time ... time.sleep(100) app['屏幕錄像專家'].Ok.click()
(2)查詢法。
寫個循環,一直查詢是否存在“屏幕錄像專家”彈窗,若存在,則退出循環。
...
注意,在查詢的時候,最好不要用app[‘屏幕錄像專家’].exists()。這個匹配不精准,如下圖中的最后一個句柄。這個句柄在開啟程序后就一直存在,且於我們要找的對話框title一樣,所以我們在查找的時候需要加上class_name。
(3)查詢等待法。
查詢有個缺點就是如果一直沒出現,就會一直等待。所以我們最好設置一個等待時間限。
使用模塊中自帶的wait函數就可以實現該功能了;
官網地址:
https://pywinauto.readthedocs.io/en/latest/wait_long_operations.html
- 舉個例子code
(1)批量轉換exe視頻;
# -*- coding: utf-8 -*- """ Created on Wed Oct 4 16:52:13 2017 @author: x """ from pywinauto.application import Application program_path = r"D:\Program Files (x86)\tlxsoft\屏幕錄像專家 共享版 V2017\屏錄專家.exe" app = Application().start(program_path) dlg_spec = app.window(title_re='屏幕錄像專家.*',class_name='TMainForm') #dlg_spec.type_keys('%TP') dlg_spec.menu_select(r"轉換工具->EXE/LXE轉成MP4") dlg_spec = app[r'EXE/EXE 轉 MP4'] dlg_spec.print_control_identifiers() # 打印該窗體下的所有控件結構 for line in open('ToBeConvert.txt'): filename = line.strip() # 去掉讀取每一行時最后帶着的空格和回車符 dlg_spec.Button3.click() # 點擊“瀏覽”按鈕 dlg_open = app.window(title=r'打開') # 獲取“打開”對話框句柄 dlg_open.Edit.type_keys(filename,with_spaces = True) # dlg_open.Edit.set_text(filename) # 將文件絕對路徑寫入編輯框中 dlg_open.Button0.click() # 點擊“打開”按鈕 dlg_open.wait_not('visible') dlg_spec.CheckBox0.check() # 勾選自動擴幀 dlg_spec.Button0.click() # 點擊“轉換” app['另存為'].Button0.click() # 點擊“另存為”對話框的“保存”按鈕 app.window(title=r'屏幕錄像專家',class_name='TMessageForm').Wait('enabled',timeout=300) # 等待轉換結束 app.window(title=r'屏幕錄像專家',class_name='TMessageForm').Ok.click() # 關閉轉換完成后彈出的對話框
附注:
-1- 根據屏幕錄像專家程序的安裝位置修改變量‘program_path’的值。
-2- 在當前目錄新建一個 ToBeConvert.txt 文檔,每行寫上一個帶轉換的源文件,目標文件目錄默認與源文件相同。
E:\test test .exe
E:\test test1.exe
- 注意事項
在MenuSelect函數中不支持正則,完全是全文匹配,如我輸入:
dig = app.Notepad.MenuSelect("編輯->替換".decode('gb2312')) 是找不到對象的
必須要:
dig = app.Notepad.MenuSelect("編輯(E)->替換(R)".decode('gb2312'))
得把(R) (E)寫上才行,但是奇怪的是上面的“幫助->關於記事本”就不用輸入;
(1)例1:
#! /usr/bin/env python #coding=gbk import time from pywinauto import application app = application.Application().start('notepad.exe') app.Notepad.MenuSelect('幫助->關於記事本'.encode('gb2312').decode('gb2312')) time.sleep(0.5) #這里有兩種方法可以進行定位“關於記事本”的對話框 #top_dlg = app.top_window_() 不推薦這種方式,因為可能得到的並不是你想要的 about_dlg = app.window_(title_re = u"關於", class_name = "#32770")#這里可以進行正則匹配title #about_dlg.print_control_identifiers() app.window_(title_re = u'關於“記事本”').window_(title_re = u'確定').Click() app.Notepad.MenuSelect('幫助->關於記事本'.encode('gb2312').decode('gb2312')) time.sleep(0.5) #停0.5s 否則你都看不出來它是否彈出來了! ABOUT = u'關於“記事本”' OK = u'確定' #about_dlg[OK].Click() #app[ABOUT][OK].Click() app[u'關於“記事本”'][u'確定'].Click() app.Notepad.TypeKeys(u"楊彥星") dig = app.Notepad.MenuSelect("編輯(E)->替換(R)".encode('gb2312').decode('gb2312')) Replace = u'替換' Cancle = u'取消' time.sleep(0.5) app[Replace][Cancle].Click() dialogs = app.windows_()
(2)例2:
import time from pywinauto import application app = application.Application().start('notepad.exe') app.Notepad.MenuSelect('幫助->關於記事本') time.sleep(1) OK='確定' app['關於“記事本”']['確定'].Click() #或者app['關於“記事本”'][OK].Click() time.sleep(1) app.notepad.TypeKeys("輸入測試文本") time.sleep(2) dig = app.Notepad.MenuSelect("編輯(E)->替換(R)") time.sleep(1) Replace='替換' Cancel='取消' app[Replace][Cancel].Click() #要不然就寫成app['替換']['取消'].Click() time.sleep(1) dialogs = app.windows_()