刚学完Python全栈自动化的培训课程,我迫不及待地想运用到公司的项目上,于是自己写代码,想实现当有新的软件发布就可以自动下载软件到手机,之后启动自动化测试的功能,从而实现无人值守,第一时间就可以自动点检测试新软件的功能。要实现这样的功能,关键的一步是实现自动下载指定软件到手机,通过Python+Pywin32+Winspy即可解决:Pywin32提供了Python操作windows程序的接口,而Winspy(或Spy++)则是用于查看windows窗口控件相关属性的辅助工具。
分析:
- 下载指定的软件版本,这里包含了项目名称、主软件和Perso文件,既然是无人值守,当然需要先把对应项目的手机先连接到电脑上,保证电脑可以识别到手机(由于下载工具的限制,只能同时下载一台手机,因而一台电脑只能连接一台手机);
- 下载工具需要选择软件所在的服务器,这个下载工具也需要用户名和密码来登陆,登陆后,要选择项目名称,再加载主软件和perso文件,然后才能下载到手机;
-
加载软件时,默认会打开该项目所在服务器的项目根目录,需要逐步打开主软件所在的目录,之后打开对应的软件版本目录,再打开originfiles目录,然后打开userdebug目录,最后才是选择partition文件进行加载(这样下载的软件就是userdebug版本的,手机下载完软件后无需人工干预,电脑就能识别到手机的ADB口);主软件加载后才能加载Perso文件,加载Perso文件时,也需要从根目录开始,先打开perso所在的目录,再打开对应的软件版本目录,然后打开对应Perso名称的目录,最后是加载对应的Perso文件。例如下载3D48+UE80这样的软件到手机,主软件为3D48,Perso为UE80,对应的主软件目录为appli/v3D48/originfiles/userdebug,Perso文件目录为perso/3D48/UE。
-
可循环监测服务器上是否有新的主软件和Perso(也可通过接收固定格式内容的邮件来作为触发条件),只有新的主软件和Perso都同步到服务器后,才会触发自动化下载软件的程序来启动下载的命令,这一步可通过Jenkins来实现(通过定时查看服务器上的文件来判断,或者判断收件箱中收到新的固定格式内容的邮件,本次代码未包含这部分)。
遇到的问题:
-
下载工具加载软件时,对应的文件或目录是在SysListView32控件中展示的,由于没有在win32api中找到对应的接口,我是通过访问项目软件所在的网站,通过Selenium遍历网站目录找到对应的主软件和Perso,从而得到主软件和perso文件在SysListView32控件中的相对位置,记录各个步骤对应的顺序,再通过相对定位,控制鼠标点击操作下载工具来加载软件的对应的软件版本和Perso的。
-
项目选择的问题,在win32api中未找到读取项目下拉列表组合框当前选择的项目名称的接口,于是翻阅win32api,找到了另外一个解决办法:先遍历所有的下拉列表项,比较下拉列表项是否完全匹配对应的项目名称,取得匹配项目名称的索引,再更新下拉列表的索引为找到索引,这样下拉列表选择的就是我们需要的项目名称了。之前考虑过把下拉列表中所有的项目作为一个列表,然后操作鼠标点击下拉列表,通过模拟键盘的上下方向键来选择第几个项目来实现项目的选择,但由于下载工具会更新,更新后往往有新增的项目,项目的顺序就会有细微的差别,因而没有采取这个解决方案。
Python编写的控制下载工具自动登陆、选择项目、加载主软件和Perso、执行下载操作的所有代码如下:
from selenium import webdriver
#=========== 通用模块,windows系统操作==========>>
import win32gui, win32con, os, win32api
import time, win32ui
def move_to(position):
'''移动鼠标到某个位置'''
win32api.SetCursorPos(position)
print("Move to:", position)
def one_click(position):
'''鼠标单击某个位置'''
move_to(position)
print('Click position:', position)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.05)
time.sleep(1)
def double_click(position):
'''鼠标双击某个位置'''
move_to(position)
print('Double click position:', position)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.01)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.05)
time.sleep(1)
def multi_click(times, position):
'''点击某位置多次(例如滚动条向下的位置,可以实现listview控件向下滚动times行)'''
print('Multi click position ',times, ' times: ', position)
move_to(position)
for i in range(times):
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.05)
time.sleep(2)
def findWindow(*args):
'''args为窗口标题包含的内容,可以是多个字符串'''
findHandle = None
findTitle = None
hWndList = []
win32gui.EnumWindows(lambda hWnd, param: param.append(hWnd), hWndList)
for hWnd in hWndList:
if hWnd:
title = win32gui.GetWindowText(hWnd)
flag = []
i = 0
for a in args:
i = i + 1
if(a in title):
flag.append(True)
else:
flag.append(False)
if False not in flag:
findHandle = hWnd
findTitle = title
if findTitle is not None:
print('Find Window:', findTitle)
return findHandle
def findWindow2(className, title_text):
'''根据class和title查找窗口'''
findHandle = []
findTitle = None
hWndList = []
win32gui.EnumWindows(lambda hWnd, param: param.append(hWnd), hWndList)
for hWnd in hWndList:
if hWnd:
title = win32gui.GetWindowText(hWnd)
class_name = win32gui.GetClassName(hWnd)
if title_text in title and class_name == className:
findHandle.append(hWnd)
if len(findHandle) >= 1:
return findHandle[-1]
else:
return None
def waitHandle(title,seconds=30):
'''根据title检索handle,默认循环检测30秒,每秒检测一次,当打开新窗口句柄时,可以使用此函数'''
handle = None
i = 0
while handle is None:
i = i + 1
handle = findWindow(title)
if handle is None:
time.sleep(1) # 找到窗口句柄
else:
break
if i >= seconds:
print('找不到对应的窗口句柄')
break
return handle
def key_press(text):
'''根据传入的文本模拟相应的按键'''
key_map = {'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, 'j': 74, 'k': 75,
'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82, 's': 83, 't': 84, 'u': 85, 'v': 86,
'w': 87, 'x': 88, 'y': 89, 'z': 90, '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54,
'7': 55, '8': 56, '9': 57, 'Star': 106, '!': 33, '@': 64, '#': 35, '$': 36, 'Enter': 13,
'Backspace': 8,
'Tab': 9, 'Clear': 12, 'Shift': 16, 'Control': 17, 'Alt': 18, 'Cap Lock': 20, 'Esc': 27,
'Spacebar': 32,
'Page Up': 33, 'Page Down': 34, 'End': 35, 'Home': 36, 'Left Arrow': 37, 'Up Arrow': 38,
'Right Arrow': 39, 'Down Arrow': 40, 'Insert': 45, 'Delete': 46, 'Num Lock': 144}
if type(text) == list:
for item in text:
if item in key_map.keys():
code = key_map[item]
win32api.keybd_event(code, 0, 0, 0) # key down
time.sleep(0.01)
win32api.keybd_event(code, 0, win32con.KEYEVENTF_KEYUP, 0) # key up
time.sleep(0.01)
elif item in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": # upper
shift = key_map['Shift']
code = key_map[item.lower()]
win32api.keybd_event(shift, 0, 0, 0) # key down
time.sleep(0.01)
win32api.keybd_event(code, 0, 0, 0) # key down
time.sleep(0.01)
win32api.keybd_event(code, 0, win32con.KEYEVENTF_KEYUP, 0) # key up
time.sleep(0.01)
win32api.keybd_event(shift, 0, win32con.KEYEVENTF_KEYUP, 0) # key up
time.sleep(0.01)
elif text in key_map.keys():
code = key_map[text]
win32api.keybd_event(code, 0, 0, 0) # key down
time.sleep(0.01)
win32api.keybd_event(code, 0, win32con.KEYEVENTF_KEYUP, 0) # key up
time.sleep(0.01)
else:
print('无法识别出入的参数:', text)
raise Exception #抛出异常
def set_index(cbHandle, full_text):
'''根据文本内容设置下拉列表框选中该文本内容项'''
#获取下拉列表索引数量
total = win32gui.SendMessage(cbHandle, win32con.CB_GETCOUNT, 0, 0)
index = -1
for i in range(total):
#遍历索引,按文本强匹配查找对应的索引
index = win32gui.SendMessage(cbHandle,win32con.CB_FINDSTRINGEXACT,0,full_text)
if index < total:
#找到索引,更新下拉列表的索因为找到的索因
win32gui.SendMessage(cbHandle, win32con.CB_SETCURSEL, index, 0)
print('已选择:',full_text)
return True
if index not in range(total):
#没有找到匹配的索因
print('找不到匹配内容的索引')
return False
#<<=========== 通用模块,windows系统操作
class AutoDownloadSW:
"""
自动通过下载工具下载指定项目的主软件和perso到手机
"""
def __init__(self,maincode, perso, project, download_tool_path=None):
"""
下载操作初始化
:param maincode: 主软件名称,默认为4到5个字符
:param perso: Perso名称和编号,默认为4个字符
:param project: 项目名称,可能包含有空格
:param download_tool_path: 下载工具路径,有默认版本的路径,不指定时,用默认版本,新增的项目只有新版本工具才可以下载
"""
self.maincode = maincode
self.perso = perso
self.project = project
self.download_tool_path = download_tool_path
if download_tool_path is None:
self.download_tool_path = r"C:\Program Files (x86)\TeleWeb QCT_SP 3.8.2\TelewebS.exe"
def start_download(self):
'''下载操作'''
# 开始,提示
print('程序开始自动执行,请不要动鼠标或执行其它操作,以免影响自动执行!')
time.sleep(5)
# 打开下载工具
os.popen(self.download_tool_path)
# 获取Teleweb工具的句柄
handle0 = waitHandle('Login TeleWeb')
# 判断是否弹出了更新版本的提示,如果有,点击取消
#此处代码省略
# 找到OK按钮,点击OK按钮(默认已输入用户名和密码,如果没有输入过,需要输入一次,此处省略了用户名和密码的处理)
ok_btn = win32gui.FindWindowEx(handle0, 0, 'Button', 'OK')
win32gui.SendMessage(handle0, win32con.WM_COMMAND, 1, ok_btn)
print("点击OK")
time.sleep(2)
errorHandle = findWindow2("#32770", 'TeleWeb')
if errorHandle is not None:
msg = 'Wrong user mode, user name or password, please try again! (Error code = 401)'
w = win32ui.FindWindow('#32770', 'TeleWeb')
if w.GetDlgItemText(0xFFFF) == msg:
print('发现异常,请手动输入用户名和密码,选择好site,下次会自动记录')
raise Exception
# 查找Teleweb窗口句柄
handle1 = waitHandle('TeleWeb QCT_SP 3.8.2 - Shenzhen') # Teleweb主窗口
handle2 = win32gui.FindWindowEx(handle1, 0, "#32770", None)
handle3 = win32gui.FindWindowEx(handle2, 0, "SysTabControl32", None) # Download Tab窗口
handle4 = win32gui.FindWindowEx(handle3, 0, "#32770", None)
# 遍历窗口子控件
hwndChildList = []
win32gui.EnumChildWindows(handle4, lambda hwnd, param: param.append(hwnd), hwndChildList)
cb = []
for h in hwndChildList:
if win32gui.GetClassName(h) == 'ComboBox':
# print(win32gui.GetWindowRect(h))
cb.append(h)
prjCombobox = cb[-1] #项目下拉列表,此处发现另有一个隐藏的下拉列表,所以只能通过这个方法得到项目下拉列表控件
if not set_index(prjCombobox, self.project.replace(' ','')): #设置下拉列表索引为该项目,项目需要去掉空格
raise Exception #设置失败抛出异常
#获取项目软件加载操作步骤顺序
steps = self.get_download_steps()
# 点击partition按钮,打开软件加载对话框
p_btn = win32gui.FindWindowEx(handle4, 0, "Button", "...")
left, top, right, bottom = win32gui.GetWindowRect(p_btn) #获得按钮的位置信息
p=(int((left+right)/2), int((top+bottom)/2)) #按钮左上的位置稍偏右下
time.sleep(2)
one_click(p)
print("点击Partition按钮")
time.sleep(2)
#选择软件版本
self.select_file(steps[0])
time.sleep(5)
#获取文件加载列表控件
list2 = win32gui.FindWindowEx(handle4,0,'SysListView32','List2') #List2控件
left, top, right, bottom = win32gui.GetWindowRect(list2)
#文件2的位置
p = (left+20, top + 50)
#点击2开头的文件
print('click 2 file')
one_click(p)
time.sleep(2)
#选择perso
self.select_file(steps[1])
time.sleep(5)
#点击download开始下载
dl_btn = win32gui.FindWindowEx(handle4,0,'Button','Download')
left, top, right, bottom = win32gui.GetWindowRect(dl_btn)
# 文件2的位置
p = (left + 10, top + 10)
one_click(p)
time.sleep(5)
#判断是否出现异常,例如手机未连接
errorHandle = findWindow2("#32770", 'TeleWeb')
if errorHandle is not None:
print('发现异常,请确认手机是否已连接')
raise Exception
finish = findWindow2("#32770", 'TeleWeb')
i = 0
while finish is None:
time.sleep(5)
i = i + 5
finish = findWindow2("#32770", 'TeleWeb')
if finish is not None:
w = win32ui.FindWindow('#32770', 'TeleWeb')
if w.GetDlgItemText(0xFFFF) == 'Operate Finished':
print('操作已完成')
w.SendMessage(win32con.WM_CLOSE)
else:
print('未弹出完成窗口,请确认操作是否正确')
raise Exception
if i > 1200:
print('已等待20分钟,下载仍未完成,请确认是否下载正常')
raise Exception
def get_download_steps(self):
'''根据maincode和perso,获取相应的步骤(顺序编号)'''
maincode = self.maincode
perso = self.perso
project = self.project
#软件目录
url = 'https://10.128.161.96/0_Shenzhen/'
#打开浏览器,默认用IE打开,需要提前把对应的IE驱动放入python的根目录中
self.driver = webdriver.Ie()
self.driver.get(url)
time.sleep(5)
#点击继续浏览此网站
self.driver.find_element_by_partial_link_text("继续浏览此网站").click()
time.sleep(2)
for i in range(3): # 按3次向上箭头,定位到用户名输入框
key_press('Up Arrow')
# 输入用户名
user = 'xxxxxxxx' #此处为软件服务器登录的用户名
key_press(list(user))
time.sleep(1)
# Tab键
key_press('Tab')
# 密码
pwd = "xxxxxxxx" #此处为软件服务器登录的密码
key_press(list(pwd))
time.sleep(1)
# 输入回车,登录
key_press('Enter')
time.sleep(2)
# 打开项目目录
project = project.upper() #转为全部大写
project = project.replace(' ', '') # 去掉空格
#打开项目对应的链接
self.driver.find_element_by_partial_link_text(project).click()
time.sleep(1)
list0 = []
#list0存放主软件相对位置顺序
list0.append(self.get_step('appli/'))
list0.append(self.get_step(self.maincode[-4:] + '/')) # 软件版本末4位加上/与目录的末5位字符匹配
# 判断是否找到了主软件
if list0[-1] == -1: # 没有找到主软件,需要从tmp目录下找
list0 = []
# 返回上一级目录
self.driver.find_element_by_partial_link_text('Parent Directory').click()
time.sleep(2)
# 找到tmp目录
list0.append(self.get_step('appli/'))
list0.append(self.get_step(self.maincode[-4:] + '/')) # 软件版本末4位加上/与目录的末5位字符匹配
#判断是否找到
if list0[-1] == -1:
# 仍然找不到,直接返回None
print("找不到指定的软件版本,请确认版本名称是否正确,注意一定要匹配,区分大小写:", maincode)
self.driver.close()
raise Exception #抛出异常
# 找到originfiles的顺序
list0.append(self.get_step('originfiles/'))
# 找到userdebug的顺序
list0.append(self.get_step('userdebug/'))
# 找到P开头的partition文件的顺序
list0.append(self.get_step('P' + maincode[-4:]))
# 获得软件目录路径
swpath = self.driver.current_url
# 返回主目录
for i in range(1, len(list0)):
self.driver.find_element_by_partial_link_text('Parent Directory').click()
time.sleep(2)
list1=[]
# 找到perso文件夹的顺序
list1.append(self.get_step('perso/'))
# 找到主软件文件夹的顺序
list1.append(self.get_step(maincode[-4:] + '/'))
# 找到Perso名文件夹的顺序
list1.append(self.get_step(perso[:2] + '/'))
# 找到Perso 2开头的文件的顺序
list1.append(self.get_step('2' + maincode[-4:-1].lower() + perso.lower()))
time.sleep(2)
#记录perso文件路径
persopath = self.driver.current_url
#关闭浏览器
self.driver.close()
rmpath = url + project
swpath = swpath.replace(rmpath, '') #移除路径前缀
persopath = persopath.replace(rmpath, '') #移除路径前缀
return [list0, list1, swpath, persopath] #返回主软件顺序位置、perso文件顺序位置、主软件目录、perso文件目录,返回的列表是为自动操作下载工具加载软件准备的
def get_step(self, text):
'''根据文本查询对应链接的位置,注意前4个链接是标题,第5个链接是返回上级目录,而teleweb所以返回的位置需要减去5'''
sw = self.driver.find_elements_by_tag_name('a')
# 找到Perso 2开头的文件的顺序
n = 0
for i in sw:
n = n + 1
if text[-1] == "/": #对应为目录,需要右侧匹配
if text in i.text[-len(text):]:
i.click() #打开目录
time.sleep(2)
return n - 5
else: #对应为文件,只需要包含即可
if text in i.text:
return n-5
#没有找到
return -1
def select_file(self, steps):
'''根据传入的各个步骤的文件顺序操作鼠标自动选择软件版本(Userdebug)'''
"""
steps: 一个数值列表,代表每一步的编号
startPosition: List1控件第一行中间的位置(x, y)
step: 每一行的高度(像素)
"""
time.sleep(5)
file_handle = findWindow('Remote File Open Dialog')
back = win32gui.FindWindowEx(file_handle, 0, 'Button', '/')
left, top, right, bottom = win32gui.GetWindowRect(back) # 获得按钮的位置信息
bp = (int((left + right) / 2), int((top + bottom) / 2)) # 按钮中心
print('Click /')
one_click(bp)
time.sleep(2)
list1 = win32gui.FindWindowEx(file_handle, 0, 'SysListView32', 'List1') # List1控件
left, top, right, bottom = win32gui.GetWindowRect(list1)
startPosition = (left + 30, top + 32)
lastPosition = (startPosition[0], bottom - 10)
downPosition = (right - 10, bottom - 10)
for i in steps:
print('steps:', i)
if i <= 17:
# 直接移动鼠标到目标位置
position = (left + 30, startPosition[1] + i * 17)
# 双击打开
double_click(position)
else:
j = i - 17
# 滚动到条目可见
multi_click(j, downPosition)
# 双击打开
double_click(lastPosition)
AutoDownloadSW('3D48','UE80','U50A PLUS VZW').start_download()
欢迎来到testingpai.com!
注册 关于