前言
說一下這個配置的來源,最開始是想抓取某個應用裏面的一些文本信息,自己的手機沒root不好抓包,所以下載了安卓模擬器,然後安裝抓包APP,直接抓,發現內容傳輸是加密的。那麼在不去研究加密方法,最簡單的方式,就是直接從屏幕控件中提取文本了,畢竟文本本身是明文顯示在屏幕上的控件裏的。
先說一下直接用adb操作安卓手機(模擬器)
在不知道uiautomator2之前,最初考慮的是直接用adb操作手機
方法如下:
- 進入安裝位置 C:/Program Files (x86)/Nox/bin/
- 輸入 nox_adb.exe connect 127.0.0.1:62001 即可以連接到adb
- 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
分支下的DontShowUI
和Disabled
選項都改成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這個東西,確實也有一些大公司在用,所以也不太好給這個軟件下結論,大概只有真正用這個軟件實際開發人,才知道這個軟件可以勝任什麼工作。和我聊起這個的人是財務人員,整個部門還都在培訓期。不過大致可以猜測一下,自動化軟件做的,應該也就是跨應用文檔,將一些手工的操作,變爲自動化實現。