Python 自动控制下载工具下载指定软件到手机

本贴最后更新于 1694 天前,其中的信息可能已经渤澥桑田

刚学完Python全栈自动化的培训课程,我迫不及待地想运用到公司的项目上,于是自己写代码,想实现当有新的软件发布就可以自动下载软件到手机,之后启动自动化测试的功能,从而实现无人值守,第一时间就可以自动点检测试新软件的功能。要实现这样的功能,关键的一步是实现自动下载指定软件到手机,通过Python+Pywin32+Winspy即可解决:Pywin32提供了Python操作windows程序的接口,而Winspy(或Spy++)则是用于查看windows窗口控件相关属性的辅助工具。

分析:

  1. 下载指定的软件版本,这里包含了项目名称、主软件和Perso文件,既然是无人值守,当然需要先把对应项目的手机先连接到电脑上,保证电脑可以识别到手机(由于下载工具的限制,只能同时下载一台手机,因而一台电脑只能连接一台手机);
  2. 下载工具需要选择软件所在的服务器,这个下载工具也需要用户名和密码来登陆,登陆后,要选择项目名称,再加载主软件和perso文件,然后才能下载到手机;

image.png

  1. 加载软件时,默认会打开该项目所在服务器的项目根目录,需要逐步打开主软件所在的目录,之后打开对应的软件版本目录,再打开originfiles目录,然后打开userdebug目录,最后才是选择partition文件进行加载(这样下载的软件就是userdebug版本的,手机下载完软件后无需人工干预,电脑就能识别到手机的ADB口);主软件加载后才能加载Perso文件,加载Perso文件时,也需要从根目录开始,先打开perso所在的目录,再打开对应的软件版本目录,然后打开对应Perso名称的目录,最后是加载对应的Perso文件。例如下载3D48+UE80这样的软件到手机,主软件为3D48,Perso为UE80,对应的主软件目录为appli/v3D48/originfiles/userdebug,Perso文件目录为perso/3D48/UE。
    image.png

  2. 可循环监测服务器上是否有新的主软件和Perso(也可通过接收固定格式内容的邮件来作为触发条件),只有新的主软件和Perso都同步到服务器后,才会触发自动化下载软件的程序来启动下载的命令,这一步可通过Jenkins来实现(通过定时查看服务器上的文件来判断,或者判断收件箱中收到新的固定格式内容的邮件,本次代码未包含这部分)。

遇到的问题:

  1. 下载工具加载软件时,对应的文件或目录是在SysListView32控件中展示的,由于没有在win32api中找到对应的接口,我是通过访问项目软件所在的网站,通过Selenium遍历网站目录找到对应的主软件和Perso,从而得到主软件和perso文件在SysListView32控件中的相对位置,记录各个步骤对应的顺序,再通过相对定位,控制鼠标点击操作下载工具来加载软件的对应的软件版本和Perso的。
    image.png

  2. 项目选择的问题,在win32api中未找到读取项目下拉列表组合框当前选择的项目名称的接口,于是翻阅win32api,找到了另外一个解决办法:先遍历所有的下拉列表项,比较下拉列表项是否完全匹配对应的项目名称,取得匹配项目名称的索引,再更新下拉列表的索引为找到索引,这样下拉列表选择的就是我们需要的项目名称了。之前考虑过把下拉列表中所有的项目作为一个列表,然后操作鼠标点击下拉列表,通过模拟键盘的上下方向键来选择第几个项目来实现项目的选择,但由于下载工具会更新,更新后往往有新增的项目,项目的顺序就会有细微的差别,因而没有采取这个解决方案。
    image.png

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()
4 回帖
请输入回帖内容 ...
  • HappyEarth

    大佬666,请问一下这个pywin32的库适用于64位的应用程序吗?

  • 其他回帖
  • ta_qian

    有联系方式没 加一下

  • steve

    没试过,可以试一试,我电脑是64位系统的

  • steve

    本来录屏了自动操作过程演示为视频的,因涉及公司机密,现去掉视频链接。