Appium是一個開源的,適用於原生或混合移動應用( hybrid mobile apps )的自動化測試工具,Appium應用WebDriver: JSON wire protocol驅動安卓和iOS移動應用。
也可以用來爬取App數據,本文將以Android為例。
* 通信
· 由程式設計師編寫的自動化程序(Appium客戶端庫)通過HTTP協議與Appium Server進行通信
· 再由Appium Server與設備的自動化代理進行通信
· 設備的自動化代理接收到指令後執行指令,返回執行後的結果給Appium Server
* 自動化程序
· 自動化程序由程式設計師開發,實現具體的自動化功能
· 要發出具體指令控制設備,需要使用到客戶端庫
· 調用這些庫向設備發送自動化指令
* Appium Server
· Appium Server負責管理設備的自動化環境
· 轉發程序發來的指令給設備,轉發設備被發來的響應給程序
* 設備
· 設備由手機、平板、手邊等組成 (Android、IOS)
· 需要在設備上安裝自動化代理程序,代理程序會等待自動化指令,並執行指令
* 環境下載
在公眾號後臺回復"Appium"獲取全部環境的下載地址
pip install appium-python-client
下載地址
https://github.com/appium/appium-desktop/releases/tag/v1.21.0
https://github.com/appium/appium-desktop/releases/download/v1.21.0/Appium-windows-1.21.0.exe
環境變量
將Appium的安裝路徑添加到系統變量Path中即可
下載地址
在官網下載需要註冊登錄 可在後臺回復 「JDK」獲取下載連結
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
環境變量
創建新的系統變量,變量名為: JAVA_HOME 變量值為: JDK的安裝路徑
將下方字符添加到系統變量Path中
%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin打開cmd輸入 java 有反饋則環境安裝完成
下載
在公眾號後臺回復"SDK"獲取下載連結
(直接放連結怕過期,官方安裝方法相對複雜)
環境配置
創建系統變量 ANDROID_HOME
將下載到的壓縮包解壓並將解壓後的目錄作為變量值保存
將下方字符添加到系統變量Path中
%ANDROID_HOME%\platform-tools
手機端設置
這裡以小米手機為例
在設置 > 我的設備 > 全部參數 中找到MIUI版本 連續點擊5次進入開發者模式
在設置 > 更多設置 > 開發者選項 中打開USB調試
電腦端配置
將手機通過數據線連接到電腦
打開cmd輸入 adb devices 列出設備
手機端如彈出窗口要點擊確定或允許
手機端配置
這裡以夜神模擬器為例
在設置 > 關於平板電腦 中找到版本號 連續點擊5次進入開發者模式
在開發者選項中打開USB調試即可
電腦端配置
將夜神模擬器安裝路徑下的bin目錄添加到系統變量Path中
刪除該目錄下的 nox_adb.exe
將 androidsdk目錄下的platform-tools\adb.exe
複製到夜神模擬器的bin目錄中並重命名為nox_adb.exe
使用powershell進行如下操作(記得打開模擬器)
cd D:\install\Nox\bin nox_adb.exe connect 127.0.0.1:62001 nox_adb.exe devices重啟模擬器 再打開cmd輸入 adb devices 列出設備
軟體基本配置
在Appium高級設置中有三個需要注意的參數
伺服器地址為0.0.0.0時表示本機的所有IP
伺服器埠為Appium Server部署的埠 一般默認就行
啟動埠在多設備並發的時候會用到
如果你的環境配置沒有問題,在編輯配置中可以看到JDK與SDK的環境變量值
基本配置
啟動會話後點擊右上角的啟動檢查器會話
再配置完指定的參數後可以看到App的布局與控制項信息
這裡以嗶哩嗶哩為例,先在模擬器中打開軟體
打開cmd輸入下方命令獲取軟體包名與入口
adb shell dumpsys activity | findstr "mResume"將會話信息配置為如下示例
配置完成後啟動會話即可
{ "platformName": "Android", # 系統名稱(Android/IOS) "platformVersion": "10", # 系統版本號(安卓10) "deviceName": "k30pro", # 設備名稱隨意 "appPackage": "tv.danmaku.bili", # APP包名 "appActivity": "MainActivityV2", # APP入口 "noReset": true, # 為false時每次啟動都會重置APP,為true時保留Session不重置 "unicodeKeyboard": true, # 使用unicodeKeyboard的編碼方式來發送字符串 "resetKeyboard": true # 執行完成後恢復到原來的輸入法}
獲取元素信息
啟動會話後會重啟App並在Appium檢查器會話中顯示App的布局與控制項信息
通過滑鼠選擇某個元素後可以看到元素的屬性與值
可以使用元素的accessibility id 、id 、xpath 等屬性進行定位
目前記住元素的一些屬性信息大概在什麼的地方可以看到就行了
在下方會進行詳細的介紹
除了使用Appium獲取元素信息,還可以使用AndroidSDK中自帶的工具
androidsdk\tools\bin\uiautomatorviewer.bat
常用的一些方法
click() tap() send_keys("Hello") swipe(start_x=x, start_y=y1, end_x=x, end_y=y2, duration=800) press_keycode() open_notifications() quit() x=driver.get_window_size()['width'] y=driver.get_window_size()['height']更多的按鍵信息可以到下放網站中查看
https://github.com/appium/python-client/blob/master/appium/webdriver/extensions/android/nativekey.pyfrom appium.webdriver.extensions.android.nativekey import AndroidKey
press_keycode(AndroidKey.ENTER) # 回車press_keycode(AndroidKey.BACK) # 返回press_keycode(AndroidKey.HOME) # home鍵press_keycode(AndroidKey.VOLUME_UP) # 音量+press_keycode(AndroidKey.SPACE) # 空格press_keycode(AndroidKey.DEL) # 刪除可以通過下方的方法定位到指定元素,對其進行點擊、輸入、獲取文本等操作
具體的使用方法與Selenium類似 (建議先學Selenium)
find_elements_by_id() find_elements_by_xpath() find_elements_by_class_name() find_elements_by_accessibility_id()
find_elements find_element獲取元素的屬性 (如文本、坐標、大小的等)
text tag_name size location get_attribute("name") get_attribute(屬性名)- 它可以通過命令行與安卓設備進行交互,如安裝調試應用、傳輸文件等 - 命令參考: https://developer.android.google.cn/studio/command-line/adb.html - 使用os.system() 來調用 - 使用subprocess 來調用 adb devices -l adb shell dumpsys activity | findstr "mResume" adb shell ls /sdcard adb push wv.apk /sdcard/wv.apk adb pull /sdcard/new.txt adb install xx.apk adb shell adb shell screencap /sdcard/screen.png adb shell screenrecord /sdcard/demo.mp4
獲取App基本信息
通過 命令獲取App的包名與入口,將信息填寫到配置文件中
adb shell dumpsys activity | findstr "mResume"from appium import webdriverfrom appium.webdriver.extensions.android.nativekey import AndroidKeyimport time
caps = { "platformName": "Android", "platformVersion": "7", "deviceName": "moniqi", "appPackage": "tv.danmaku.bili", "appActivity": "MainActivityV2", "newCommandTimeout":1800, "noReset": "true", "unicodeKeyboard": "true", "resetKeyboard": "true" }
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)獲取App元素的信息
啟動Appium中的檢查器會話,在配置完成後啟動會話
這邊以搜索框為例,可以看到搜索框的id為 tv.danmaku.bili:id/expand_search
可以簡寫為 expand_search
driver.find_element_by_id("expand_search")可以看到搜索框的content-desc值為"搜索"
find_elements_by_accessibility_id("搜索")也可以通過xpath定位
driver.find_element_by_xpath(//android.widget.TextView[@content-desc="搜索"])與元素進行交互
使用 click() 方法點擊搜索框
driver.find_element_by_id("expand_search").click()通過手動點擊搜索框後彈出一個新的頁面,這個才是真正的搜索框
接下來使用id定位到搜索框(也可以使用其它方法定位)
driver.find_element_by_id("search_src_text")定位到搜索框後使用send_keys()方法輸入字符
driver.find_element_by_id("search_src_text").send_keys("Hello")driver.press_keycode(AndroidKey.ENTER) # 回車獲取App信息
定位到你要獲取的元素後使用text屬性輸出
# 注意這邊使用的是elements
title_list = driver.find_elements_by_id("title") # 定位到視頻標題(返回列表)if title_list: # 如果返回的數據不為空時 for title in title_list: print(title.text) # 將其輸出出來完整代碼
from appium import webdriverfrom appium.webdriver.extensions.android.nativekey import AndroidKeyimport time
caps = { "platformName": "Android", "platformVersion": "7", "deviceName": "k30pro", "appPackage": "tv.danmaku.bili", "appActivity": ".MainActivityV2", "newCommandTimeout":1800, "noReset": "true", "unicodeKeyboard": "true", "resetKeyboard": "true" }
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
driver.find_element_by_id("expand_search").click() driver.find_element_by_id("search_src_text").send_keys("Hello") driver.press_keycode(AndroidKey.ENTER) time.sleep(2) title_list = driver.find_elements_by_id("title") if title_list: for title in title_list: print(title.text)x1 y1為起始點 x2 y2 為終點,t為起點到終點所花費的時間
注意: 原點坐標(0,0)位於屏幕左上角
swipe(x1, y1, x2, y1, t)
swipe(810,1135,810,2000,1) # x軸不變y軸向上運動先獲取屏幕大小再進行滑動
將其封裝方便調用
class Swip: def __init__(self,driver,xy): self.driver = driver self.xy = xy self.x = self.xy["width"] self.y = self.xy["height"]
def swip_Lift(self,t=500,n=1): x1 = self.x * 0.75 y1 = self.y * 0.5 x2 = self.x * 0.25
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向左滑動{i+1}次完成")
def swip_Right(self,t=500,n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 x2 = self.x * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向右滑動{i+1}次完成")
def swip_Up(self,t=500,n=1): x1 = self.x * 0.25 y1 = self.y * 0.75 y2 = self.y * 0.5
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t) print(f"[+]向上滑動{i+1}次完成")
def swip_Down(self,t=500,n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 y2 = self.y * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t) print(f"[+]向下滑動{i+1}次完成")from appium import webdriverfrom appium.webdriver.extensions.android.nativekey import AndroidKeyimport time
caps = { "platformName": "Android", "platformVersion": "7", "deviceName": "moniqi", "appPackage": "tv.danmaku.bili", "appActivity": "MainActivityV2", "newCommandTimeout":1800, "noReset": "true", "unicodeKeyboard": "true", "resetKeyboard": "true" }
class Swip: def __init__(self, driver, xy): self.driver = driver self.xy = xy self.x = self.xy["width"] self.y = self.xy["height"]
def swip_Lift(self, t=500, n=1): x1 = self.x * 0.75 y1 = self.y * 0.5 x2 = self.x * 0.25
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向左滑動{i + 1}次完成")
def swip_Right(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 x2 = self.x * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向右滑動{i + 1}次完成")
def swip_Up(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.75 y2 = self.y * 0.5
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t)
def swip_Down(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 y2 = self.y * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t) print(f"[+]向下滑動{i + 1}次完成")
if __name__ == "__main__": print("***程序執行開始***") driver = webdriver.Remote("http://localhost:4723/wd/hub", caps) print("[+]基本環境啟動完成") xy = driver.get_window_size() print("[+]屏幕尺寸獲取完成", xy) driver.find_element_by_id("expand_search").click() print("[+]搜索框點擊完成") in_data = input("[*]要爬取的內容 >>>") time.sleep(2) driver.find_element_by_id("search_src_text").send_keys(in_data) print("[+]搜索內容輸入完成") driver.press_keycode(AndroidKey.ENTER) print("[+]搜索執行完成")
swip = Swip(driver, xy) time.sleep(2) num = 1 data = {}
while num <= 5: title_list = driver.find_elements_by_id("title") upuser_list = driver.find_elements_by_id("upuser")
if title_list and upuser_list: for title, upuser in zip(title_list, upuser_list): data[title.text] = upuser.text
swip.swip_Up(n=2) time.sleep(1) print(f"[+]第{num}頁爬取完成") num += 1 else: print("[*]沒有數據") break
print("\n**********數據爬取結束**********") print(f"[*]一共獲取到數據{len(data.keys())}條\n") if data: for title,upuser in data.items(): print(upuser + " --> " + title)
if input("\n輸入 q 退出程序 >>>") == "q": driver.quit()
理論
· 每個Appium Server只能支持一個設備,所以要開啟多個Appium
· 將Appium的服務埠與Android啟動埠進行修改
· 利用UDID參數進行區分多個設備
· 多設備並發一般用於模擬操作
Appium Server
啟動多個Appium後將其伺服器埠與Android啟動埠進行修改
這裡啟動兩個Appium Server
伺服器埠 4723安卓啟動埠 4724
伺服器埠 4725安卓啟動埠 4726配置參數
啟動兩臺模擬器後查看設備的UDID
在代碼中主要修改的兩個地方如下
caps = { "udid":127.0.0.1:62001, "systemPort": 4724 # Android啟動埠}driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
# 設備2caps = { # 省略部分代碼 "udid":127.0.0.1:62025, "systemPort": 4726}driver = webdriver.Remote("http://127.0.0.1:4725/wd/hub", caps)完整代碼
利用多進程實現並發
from appium import webdriverfrom appium.webdriver.extensions.android.nativekey import AndroidKeyimport timeimport multiprocessing
class Swip: def __init__(self, driver, xy): self.driver = driver self.xy = xy self.x = self.xy["width"] self.y = self.xy["height"]
def swip_Lift(self, t=500, n=1): x1 = self.x * 0.75 y1 = self.y * 0.5 x2 = self.x * 0.25
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向左滑動{i + 1}次完成")
def swip_Right(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 x2 = self.x * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x2, y1, t) print(f"[+]向右滑動{i + 1}次完成")
def swip_Up(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.75 y2 = self.y * 0.5
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t)
def swip_Down(self, t=500, n=1): x1 = self.x * 0.25 y1 = self.y * 0.5 y2 = self.y * 0.75
for i in range(n): time.sleep(0.2) self.driver.swipe(x1, y1, x1, y2, t) print(f"[+]向下滑動{i + 1}次完成")
def auto(udid,bport,sport): caps = { "platformName": "Android", "platformVersion": "7", "deviceName": "moniqi", "udid": udid, "systemPort": bport, "appPackage": "tv.danmaku.bili", "appActivity": "MainActivityV2", "newCommandTimeout": 1800, "noReset": "true", "unicodeKeyboard": "true", "resetKeyboard": "true" }
print("***程序執行開始***") driver = webdriver.Remote(f"http://localhost:{sport}/wd/hub", caps) print("[+]基本環境啟動完成") xy = driver.get_window_size() print("[+]屏幕尺寸獲取完成", xy) driver.find_element_by_id("expand_search").click() print("[+]搜索框點擊完成") time.sleep(2) driver.find_element_by_id("search_src_text").send_keys("Hello") print("[+]搜索內容輸入完成") driver.press_keycode(AndroidKey.ENTER) print("[+]搜索執行完成")
swip = Swip(driver, xy) time.sleep(2) num = 1 data = {}
while num <= 5: title_list = driver.find_elements_by_id("title") upuser_list = driver.find_elements_by_id("upuser")
if title_list and upuser_list: for title, upuser in zip(title_list, upuser_list): data[title.text] = upuser.text
swip.swip_Up(n=2) time.sleep(1) print(f"[+]第{num}頁爬取完成") num += 1 else: print("[*]沒有數據") break
print("\n**********數據爬取結束**********") print(f"[*]一共獲取到數據{len(data.keys())}條\n") if data: for title, upuser in data.items(): print(upuser + " --> " + title)
if __name__ == "__main__": multiprocessing.Process(target=auto,args=("127.0.0.1:62001","4724","4723")).start() multiprocessing.Process(target=auto,args=("127.0.0.1:62025","4726","4725")).start()