2025年3月17日 星期一 甲辰(龙)年 月十六 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Python

Python+uiautomator2+夜神模拟器,实现安卓自动化操作的一些尝试

时间:09-17来源:作者:点击数:54

前言

说一下这个配置的来源,最开始是想抓取某个应用里面的一些文本信息,自己的手机没root不好抓包,所以下载了安卓模拟器,然后安装抓包APP,直接抓,发现内容传输是加密的。那么在不去研究加密方法,最简单的方式,就是直接从屏幕控件中提取文本了,毕竟文本本身是明文显示在屏幕上的控件里的。

先说一下直接用adb操作安卓手机/安卓模拟器

在不知道uiautomator2之前,最初考虑的是直接用adb操作手机

方法如下:

  1. 进入安装位置 C:/Program Files (x86)/Nox/bin/
  2. 输入 nox_adb.exe connect 127.0.0.1:62001 即可以连接到adb
  3. adb devices查看是否连接成功
    这里用到是夜神模拟器,如果是手机,直接在有adb.exe的文件夹执行adb devices连手机就行了

adb指令

功能 代码
模拟输入001 adb shell input text “001”
模拟home按键 adb shell input keyevent 3
模拟点击 adb shell input tap 540 1104 # (540, 1104)坐标
模拟滑动 adb shell input swipe 250 250 300 300 # 从(250,250)滑动到(300,300)

然后就是考虑获取屏幕控件内容了

功能 代码
抓取界面 adb shell uiautomator dump /sdcard/ui.xml
导出到电脑 adb pull /sdcard/ui.xml ui.xml

效率比较低,而且导出的xml解析也需要花时间去弄

然后在查找资料的时候发现了uiautomator2

uiautomator2操作安卓手机/安卓模拟器

安装

  • # 安装 uiautomator2(安装总是超时,所以用了清华的源)
  • pip install -i https://pypi.tuna.tsinghua.edu.cn/simple uiautomator2 -U uiautomator2
  • # 连接ADB调试,安装包含httprpc服务的apk到手机
  • python -m uiautomator2 init

然后开始写代码,这里写个抓取屏幕文字的例子,可以跑跑看看

  • import uiautomator2 as u2
  • # 连接手机
  • d = u2.connect()
  • print(d.info)
  • def printAll():
  • for i,v in enumerate(d.xpath('//*').all()):
  • if v.text!='':
  • print("【{0:0=4}】{1}".format(i,v.text))
  • def printTextviewAll():
  • for i,v in enumerate(d.xpath('//android.widget.TextView').all()):
  • print("【{0:0=4}】{1}".format(i,v.text))
  • printAll()
  • # printTextviewAll()

执行结果如下,会将屏幕上的文字逐一输出

在这里插入图片描述

uiautomator2在github上有快速开始指南推荐搭配web-editor快速抓取控件信息

还是很好懂的,抽出几分钟看一遍基本就可以写东西了,比如简单的

  • d.click(x,y) # 点击坐标
  • d.xpath(xp).click() # 点击控件
  • d.xpath(xp).wait(timeout=3) # 等待控件
  • d.press('back') # 按键/返回

至于xpath的写法,最常用的大概是这句了xp = "//*[re:match(@text, '^正则语句')]",用正则匹配查找对应文本的控件。

除了xpath还支持Uiselector的写法, 二者稍有不同, 以下是我对比测试两种写法的对比作为参考, 实际写的时候我还是用Uiselector比较多一些因为写起来整洁

UiSelector xpath 备注
d(text='立即开户')d(description='立即开户') d.xpath("立即开户") text或description等于立即开户的元素
d(textMatches='正则语句') d.xpath("//*[re:match(@text, '^正则语句')]") 正则
d(text='文本') d.xpath('//*[@text="文本"]') text
d(description='文本') d.xpath('//*[@content-desc="文本"]') description
d(resourceId='文本') d.xpath('//*[@resource-id="文本"]') resourceId
d(text='') d.xpath('//*[@text=""]').all() 如果存在多个, xpath需要通过all()返回列表,列表为空返回[]
d(text='').info d.xpath('//*[@text=""]').info 获取info
d(text='').bounds() d.xpath('//*[@text=""]').bounds 获取bounds

有了通过文本正则查找控件、获取文本、点击、返回等等,剩下的就是将这些动作组合循环,实现规律性抓取了,具体怎么写结合需要自行组合实现。

================

接下来说一下过程中遇到的问题:

在实际执行的时候,每跑几个小时,adb就会崩一次,“adb.exe 已停止工作”:

在这里插入图片描述

试了nox_adb.exe和adb.exe都不行(对比了一下夜神自带的这两个adb的sha1校验码,发现一模一样,说明这两个adb文件只是名字不同而已没什么区别)。

又从网上下载几个adb还会崩,于是开始写崩了自动重连的方法。

Windows弹出的这个崩溃弹窗,会导致程序阻塞,不往下进行,于是将注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting分支下的DontShowUIDisabled选项都改成1,避免崩溃弹窗阻塞程序。

没有弹窗干扰,接下来就是写重连了

  • # 临时写的,凑活用,大概意思就是先把运行的adb都杀掉,然后重连
  • for i in range(3):
  • os.system("taskkill /F /IM adb.exe")
  • time.sleep(2)
  • os.system("taskkill /F /IM nox_adb.exe")
  • time.sleep(2)
  • while True:
  • try:
  • subprocess.call("nox_adb.exe connect 127.0.0.1:62001")
  • d = u2.connect() # connect to device
  • print(d.info)
  • break
  • except:
  • print("[Maybe] Can't find any android device/emulator")
  • time.sleep(10)

到目前为止,程序连续跑了两三天,还算稳定。

觉得这个东西还蛮好用,于是用这个它写了几个APP签到,放到NAS上每天定时执行去了。


回头想想,这两三年间,还用过一些其他的自动化手段。

Chrome浏览器,直接F12,Console输入一些纯js脚本实现的自动化:

点击:document.querySelector('#vreplysubmit').click();

文本:vf_tips = '我来说说爱看的书';$('vmessage').value = vf_tips;

随机数:Math.random()*5000

定时循环:setInterval(function(){//循环代码}, //毫秒)

计次停止:var timesRun = 0;var interval = setInterval(function(){timesRun += 1;if(timesRun === 60){clearInterval(interval);}//循环代码}, //毫秒);

简单组装一下,就能变成自动发贴机;

  • var timesMax = 70;
  • vf_tips = '每日灌水今天也要灌满50贴';
  • var timesRun = 0;
  • var interval = setInterval(function(){timesRun += 1;
  • if(timesRun === timesMax){clearInterval(interval);}
  • $('vmessage').value = vf_tips+' '+Math.floor(Math.random()*100);
  • document.querySelector('#vreplysubmit').click();
  • }, Math.random()*500+5000);

或者像是网页端自动抽奖,连续点击;

  • var timesRun = 0;var interval = setInterval(function(){timesRun += 1;if(timesRun === 120){ clearInterval(interval);}hidepop();lottery();},Math.random()*500+7000);

另外工作需要的时候,也可以用js实现一些,网页端功能测试循环点击之类的。

Selenium+Chromedriver的网页自动化:

其实严格来说Selenium+很多浏览器都可以,个人偏好Chrome,需要下载chromedriver.exe搭配使用。还需要注意文件支持的Chrome的版本,不过这一两年貌似我的Chrome更新过很多次,chromedriver一直都还没换过,大概是不那么强调对应关系了。

(就在写完这个文档草稿之后两天,居然又用到了一次这个方法。

在抓取某个站点的时候发现,该站点网页源代码的里的标签内容居然是加密的,展示的时候通过js方法解密显示成正常文字。

貌似是源自google的方法,居然还有这种网页源代码加密,大概了解到是通过js实现的后,首先想到的方法是Python+PyExecJS模块。

然后就想到用Selenium+Chromedriver试试看,发现也能绕过加密,直接获取网页上实际各框架标签内解密后的实际展示的文本)

Selenium这个东西不知道有多少人用过,想看安装使用的,可以搜一下网上教程如何安装使用。大概两年前我拿这个东西写过一些论坛自动签到的东西,现在挂在NAS上,每天跑的依旧很正常。

这里大致贴几条当初研究selenium时记录,就能看出这个东西能干什么。(从变量命名来看,不像是自己写的,可能是之前网上的代码自己加了注释,这里贴出来做个参考)

  • from selenium import webdriver # pip install selenium
  • from PIL import Image,ImageGrab # pip install pillow
  • from selenium.webdriver.common.keys import Keys
  • from selenium.webdriver.common.action_chains import ActionChains
  • import json,os,time,random
  • def initWork(chromedriverpath = "chromedriver.exe"):
  • # 初始化配置根据自己chromedriver位置做相应的修改
  • os.environ["webdriver.chrome.driver"] = chromedriverpath
  • driver = webdriver.Chrome(chromedriverpath)
  • return driver
  • def closeWork(driver):
  • driver.close()
  • driver.quit()
  • def SimpleLogin(driver,un,pw,url,unxp,pwxp,lgbutnxp):
  • driver.set_window_size(480, 800) # 设置窗口大小
  • driver.get(url) # 打开执行操作的页面地址
  • time.sleep(2) # 休眠两秒钟后执行填写用户名和密码操作
  • elem = driver.find_element_by_xpath(unxp)
  • elem.send_keys(un) # 输入用户名
  • elem = driver.find_element_by_xpath(pwxp)
  • elem.send_keys(pw) # 输入密码
  • elem = driver.find_element_by_xpath(lgbutnxp) # 根据xpath获取登录按钮
  • elem.send_keys(Keys.ENTER) # 发送确认按钮
  • driver_cookie = driver.get_cookies() # 获得cookie信息
  • cookies = {c['name']:c['value'] for c in driver_cookie} # 整理成requests使用的dict形式
  • return cookies
  • def screenshot(driver,path='screenshot.png',position=(0,0,0,0)): # 截图(还可以剪切局部)
  • driver.save_screenshot(path)
  • if position[2]!=0:
  • im = Image.open(path)
  • im = im.crop(position)
  • im.save(path)
  • def elem_get_position(driver,elem): # 返回元素的左上右下四周边界位置
  • left = elem.location['x']
  • top = elem.location['y']
  • right = elem.location['x'] + elem.size['width']
  • bottom = elem.location['y'] + elem.size['height']
  • return (left,top,right,bottom)
  • def elem_drag(driver,elem,x,y,iter,rdm=False): # 拖拽元素,按指定方向,迭代指定次数
  • action = ActionChains(driver)
  • action.click_and_hold(elem).perform() #鼠标左键按下不放
  • for index in range(iter):
  • r = 2*random.random() if rdm else 1
  • try:
  • action.move_by_offset(x*r,y*r).perform() #平行移动鼠标
  • except UnexpectedAlertPresentException:
  • break
  • action.reset_actions()
  • time.sleep(0.1*r) #等待停顿时间
  • action.click_and_hold(elem).release().perform()

对网页元素的控制,点击、填写、拖动、滚动都可以实现。

如果结合pillow和tesseract还可以做一些,过验证码识别滑动等操作。不过现在的各种验证还在不断的推陈出新,定向去破解这些验证除非有必要,不然还是比较花时间和精力,来训练提高精准度的。

至于导入导出Cookies,搭配python的requests库可做的东西就更多了。

安卓手机,使用安卓软件直接实现的自动化:

手机是日常使用最多,所以也会有一些自动化的个人需求。公司的一些项目,也有用到一些安卓端的自动化。

这里说一下我个人用的在安卓端本身实现的自动化,

如果只是简单的频繁点击,那么有一款APP叫做 自动点击器 的,基本就能满足你的大部分需求,使用起来非常简单。只要将你要点击的所有点位置、顺序、间隔时间、循环次数设定好,执行自动连续点击就行了。适合一些单调的循环点击操作。

如果你想实现一些更多复杂的一些方式,而且还是在安卓端本身,那么可以考虑Auto.js。编写js脚本,使用Auto.js安卓APP应用执行。

编写可以在SublimeText3或VisualStudioCode安装一个插件,搭建控制台,连接手机编写和调试代码。

曾经用过这个应用写过很多APP的自动化签到,但是后续发现维护也需要消耗很多时间,就不再弄了。

为了最后能判断问题出在哪一步,可以将常用的操作,包装成附带日志打印的函数,方便出问题的直接看执行到了哪一步,每一步都执行了什么,贴一些之前写过功能模块。

  • function 按住(x, y, seconds) {
  • // 按住(x,y),按住时长默认2秒
  • if (!seconds) { seconds = 2000 }
  • press(x, y, seconds);
  • console.verbose('按住: (' + x.toString() + ',' + y.toString() + ')' + seconds.toString() + '秒');
  • }
  • function 滑动(x1, y1, x2, y2, seconds) {
  • // 滑动(x1,y1)滑动到(x2,y2),滑动完成时长默认0.5秒
  • if (!seconds) { seconds = 500 }
  • gesture(seconds, [x1, y1], [x2, y2]);
  • console.verbose('滑动: ' + seconds.toString() + '秒内由(' + x1.toString() + ',' + y1.toString() + ')滑动到(' + x2.toString() + ',' + y2.toString() + ')');
  • }

更复杂的可以写一些,通过控件的点击操作、找字等待点击、通过大小查找控件、正则匹配查找控件、模拟随机化滑动等等。

  • function 控件点击(obj, moveX, moveY) {
  • // 输入控件,实现点击
  • // 可点控件,直接调用系统方法,实现点击
  • // 不可点控件,如果在屏幕范围内,模拟屏幕点击操作,实现点击
  • // 不可点控件,实现点击时,可以附加偏移量。moveX为负正整数,表示点击控件中心位置偏左右N个像素点,同理moveY表示上下
  • if (!moveX) { moveX = 0; }
  • if (!moveY) { moveY = 0; }
  • if (typeof (obj) != 'object') {
  • console.verbose('控件点击: 输入内容非控件类型:', typeof (obj), '具体内容', obj);
  • exit();
  • }
  • else if (obj.length > 1) {
  • console.verbose('控件点击: 输入控件不唯一:', obj.length, '具体内容', obj);
  • exit();
  • }
  • else {
  • //直接控件点击
  • if (obj.clickable() && moveX == 0 && moveY == 0) {
  • console.verbose('控件点击: 直接控件');
  • obj.click();
  • }
  • //模拟屏幕坐标点击
  • else {
  • //控件在屏幕显示范围内
  • var b = obj.bounds();
  • if (b.left >= 0 && b.top >= 0 && b.right <= device.width && b.bottom <= device.height) {
  • var x = b.centerX() + moveX;
  • var y = b.centerY() + moveY;
  • console.verbose('控件点击: 模拟点屏(' + x.toString() + ',' + y.toString() + ')');
  • click(x, y);
  • }
  • else {
  • console.verbose('控件点击: 控件不在屏幕范围,无法点击:', b);
  • exit();
  • }
  • }
  • }
  • }
  • function 读取文本(obj) {
  • // 输入控件,获取desc或text文本内容
  • // obj:输入控件
  • // if (!para) { para = ''; }
  • var text = obj.text();
  • var desc = obj.desc();
  • var wenben = '';
  • if (desc == null) { desc = ''; }
  • if (text == null) { text = ''; }
  • wenben = desc + text;
  • return wenben;
  • }
  • function 找控件大小D(width, height, w_dif, h_dif, returntype) { //Matches
  • // 找指定大小控件,支持像素误差范围
  • if (!w_dif) { w_dif = 0; }
  • if (!h_dif) { h_dif = 0; }
  • var arr_find = [];
  • var t = '';
  • t = enabled(true).find(); sleep(100);
  • t.forEach(function (e) {
  • var w = e.bounds().right - e.bounds().left;
  • var h = e.bounds().bottom - e.bounds().top;
  • if (Math.abs(w - width) <= w_dif && Math.abs(h - height) <= h_dif) {
  • arr_find.push(e);
  • }
  • });
  • console.verbose('找控件大小D: 找到', arr_find.length, '个');
  • // 打印所有arr_find
  • // arr_find.forEach(function (e) {
  • // console.log('wz:', BoundsToWHM(e.bounds()), 'desc:', e.desc(), 'text:', e.text(), 'id:', e.id());
  • // });
  • // 返回结果
  • if (!returntype) {
  • if (arr_find.length < 1) { return ''; }
  • else if (arr_find.length == 1) { return arr_find[0]; }
  • else { return arr_find; }
  • }
  • else if (returntype == 'list') {
  • if (arr_find.length < 1) { return []; }
  • else { return arr_find; }
  • }
  • else {
  • console.log('找控件大小D:', 'returntype输入值错误');
  • exit();
  • }
  • }
  • function 找字M(str, returntype) { //Matches
  • //以descMatches和textMatches方式找字,可定义返回方式
  • var arr_find = [];
  • var t = '';
  • t = descMatches(str).find(); sleep(100); t.forEach(function (e) { arr_find.push(e); });
  • t = textMatches(str).find(); sleep(100); t.forEach(function (e) { arr_find.push(e); });
  • console.verbose('找字:', str, '找到', arr_find.length, '个(正则)');
  • // 打印所有arr_find
  • // arr_find.forEach(function (e) {
  • // console.log('wz:', BoundsToWHM(e.bounds()), 'desc:', e.desc(), 'text:', e.text(), 'id:', e.id());
  • // });
  • // 返回结果
  • if (!returntype) {
  • if (arr_find.length < 1) { return ''; }
  • else if (arr_find.length == 1) { return arr_find[0]; }
  • else { return arr_find; }
  • }
  • else if (returntype == 'list') {
  • if (arr_find.length < 1) { return []; }
  • else { return arr_find; }
  • }
  • else {
  • console.log('找字M:', 'returntype输入值错误');
  • exit();
  • }
  • }
  • function 滑动R(x1, y1, x2, y2) { //添加正负轻微随机数(所以注意四个点的范围不要距离上下所有边界太近)
  • // 默认构建一个随机滑动,从中下,向右上滑动
  • if (!x1) { x1 = 500; }
  • if (!y1) { y1 = 1350; }
  • if (!x2) { x2 = 700; }
  • if (!y2) { y2 = 450; }
  • // 构建随机位置
  • x1 = x1 + 100 - getRndInteger(0, 280);
  • y1 = y1 + 100 - getRndInteger(0, 280);
  • x2 = x2 + 100 - getRndInteger(0, 280);
  • y2 = y2 + 100 - getRndInteger(0, 280);
  • w = getRndInteger(300, 500); // 随机滑动时间
  • // 滑动
  • 滑动(x1, y1, x2, y2, w);
  • }

关于自动化

其实各种编程语言或多或少都有一些实现日常自动化的方法,比如微信跳一跳比较流行的时候,同事是做安卓开发的,就写了一个跳一跳脚本,当时也忘了问他是怎么实现的,大概是通过ADB。

另外这两天还看了一些Uipath,刚上手这个搭模块的感觉,一下子觉得回到了大学模电的时候的LabVIEW,但是这个东西更多的是搭逻辑,再加上输入输出和变量,封装好了一些模块化的东西给你用。如果你有相对稳定的重复的动作,这个东西可以减轻你的工作量。我一开始以为是给完全不懂编程的人用的,结果发现变量和一些语法的实现,还是要写一些VB代码。

这个东西我测试Chrome网页自动化的时候,发现找字点击会异常报错。网上也有其他人说,同样的代码IE正常,Chrome就报错。官网论坛提问了一下,很快有人答复,但是并没有解决。我自己研究了一下,通过 Click set Selector ""的方式实现了网页找字点击,间接解决了问题,写在了提问底下,相当于自问自答了。现在一周过去了,除了第一个回复我的,也没有收到其他答复。答复我的人头衔为Robot Master,意思是Users who completed the Advanced training in Academy,大概是完成了官方三种教程任一的人,成员组才只有一千多人。官网有免费在线网课,,国内也有论坛提供了一些翻译好的教程。

当我以为这个东西只是个不成熟的产品的时候,发现Uipath这个东西,确实也有一些大公司在用,所以也不太好给这个软件下结论,大概只有真正用这个软件实际开发人,才知道这个软件可以胜任什么工作。和我聊起这个的人是财务人员,整个部门还都在培训期。不过大致可以猜测一下,自动化软件做的,应该也就是跨应用文档,将一些手工的操作,变为自动化实现。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门