您当前的位置:首页 > 计算机 > 编程开发 > Python

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

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

前言

说一下这个配置的来源,最开始是想抓取某个应用里面的一些文本信息,自己的手机没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这个东西,确实也有一些大公司在用,所以也不太好给这个软件下结论,大概只有真正用这个软件实际开发人,才知道这个软件可以胜任什么工作。和我聊起这个的人是财务人员,整个部门还都在培训期。不过大致可以猜测一下,自动化软件做的,应该也就是跨应用文档,将一些手工的操作,变为自动化实现。

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