Python+uiautomator2+夜神模擬器,實現安卓自動化操作的一些嘗試

前言

說一下這個配置的來源,最開始是想抓取某個應用裏面的一些文本信息,自己的手機沒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指令

功能 代碼
模擬輸入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上有 快速開始指南
還是很好懂的,抽出幾分鐘看一遍基本就可以寫東西了,比如簡單的

d.click(x,y)                      # 點擊座標
d.xpath(xp).click()               # 點擊控件
d.xpath(xp).wait(timeout=3)       # 等待控件
d.press('back')                   # 按鍵/返回

至於xpath的寫法,最常用的大概是這句了xp = "//*[re:match(@text, '^正則語句')]",用正則匹配查找對應文本的控件。

有了通過文本正則查找控件、獲取文本、點擊、返回等等,剩下的就是將這些動作組合循環,實現規律性抓取了,具體怎麼寫結合需要自行組合實現。

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

接下來說一下過程中遇到的問題:
在實際執行的時候,每跑幾個小時,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這個東西,確實也有一些大公司在用,所以也不太好給這個軟件下結論,大概只有真正用這個軟件實際開發人,才知道這個軟件可以勝任什麼工作。和我聊起這個的人是財務人員,整個部門還都在培訓期。不過大致可以猜測一下,自動化軟件做的,應該也就是跨應用文檔,將一些手工的操作,變爲自動化實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章