关于 appium 是什么之类的不再赘述,有关的博文已经很多了,本文旨在提供截至 2022 年除夕可以在 Windows 11 上复现的操作方法
虽然这篇博文被看到的时候应该只能用在 2023 年的春节了
安装 appium desktop
安装 appium-inspector
安装 JDK
安装 MuMu 模拟器
安装好后在 系统应用 -> 设置 -> 开发者选项 中打开 USB调试
同时查看 设置 -> 关于平板电脑 -> Android版本,我的是 6.0.1
借助 Android Studio 安装 SDK
首先下载+安装+运行 Android Studio ,进入到欢迎界面,再进入 SDK 管理
安装 6.0 是因为 MuMu 模拟器是 6.0.1
进行一堆环境变量的设置
Win+R 运行 control system -> 相关链接 -> 高级系统设置 -> 环境变量
下方环境变量区中新建
变量名:ANDROID_HOME
变量值:下图所示
变量名:JAVA_HOME
变量值:刚才 JDK 的安装目录
如:C:Program FilesJavajdk-17.0.2
变量名:CLASSPATH
变量值:.;%JAVA_HOME%lib;
变量名:prog_dir
变量值:%ANDROID_HOME%platform-tools
变量名:ANDROID_SWT
变量值:%ANDROID_HOME%toolslibx86_64
已有的系统变量中找到Path -> 编辑 -> 编辑文本 -> 在文末插入
%JAVA_HOME%bin;%ANDROID_HOME%;%ANDROID_HOME%/tools;%ANDROID_HOME%/platform-tools;
测试
在 cmd 中运行
C:Usersusername> java -versionC:Usersusername> javac -versionC:Usersusername> adb devices
结果类似下图即说明设置成功
安装 appium 的 Python 驱动
pip3 install appium-python-client
如果在 cmd 下执行 adb devices 显示出有设备链接了那就万事大吉
但是 MuMu 应该是没办法被自动检测到的,如下位置 MuMu 告诉我们运行
adb connect 127.0.0.1:7555
分别打开 Appium Server GUI 和 Appium Inspector,原本他们是在一起的,现在分成了两个程序
GUI:直接 Start ,此时 Edit Configuration 里面应该已经自动填上了刚才设置的JAVA_HOME 和 ANDROID_HOME
Inspector:
如果你下载最新 Appium Desktop 时仍有如下提示,请把 Insepector 中的 Reomote Path 改成 /wd/hub ,否则会报错(见 Append:Failed to create session)
编辑 Desired Capabilities,我们直接用 JSON 格式
{ "platformName": "Android", "deviceName": "MuMu", "platformVersion": "6.0.1", "appPackage": "com.tencent.mm", "appActivity": ".ui.LauncherUI", "newCommandTimeout": 6000, "noReset":true}
有关 Desired Capabilities 的官方文档
、newCommandTimeout: 一个 session 在未接收到任何命令多久后会自动关闭,默认 60s 太短了
Start Session
看到这个大大的地球我们就成功了,至此最艰难的步骤都已经完成了
补充:包名获取
这里任意 appPackage 和 appActivity 的获取可以用以下方式,仍以微信为例:
在 MuMu 上打开微信
在 cmd 中运行 adb shell 进入模拟器操作系统
运行 dumpsys window windows | grep mFocusedApp,对照一下就懂了,其中第一次运行时是在登录界面,第二次是在联系人界面,所以 appActivity 不同
Inspector 演示
点击我们想要了解的地方就可以看到 id (即 resource_id) 和相关信息了。注意它的界面不是自动同步的,必须手动点一下上面的刷新才会同步
一些例子:
接下来我们先放下这边的工作,先构建脚本框架
from appium import webdriverfrom selenium.webdriver.support.ui import WebDriverWaitclass RedEnvelope(): def __init__(self): self.desired_caps = { "platformName": "Android", "deviceName": "MuMu", "platformVersion": "6.0.1", "appPackage": "com.tencent.mm", "appActivity": ".ui.LauncherUI", "newCommandTimeout": 6000, "noReset": True } self.driver = webdriver.Remote( 'http://127.0.0.1:4723/wd/hub', desired_capabilities=self.desired_caps) # 这里4723要改成 Appium Server GUI 启动时显示的端口 # 同时后面的 /wd/hub 也要根据 appium desktop 中的提示修改 def login(self): print('正在登陆中——————') def main(self): self.login()R = RedEnvelope()R.main()
以上代码和刚刚按的 Start Session 是一样的,观察模拟器会发现微信被打开了
使用 Insepctor 来分析 UIPython 代码运行起来后,Inspector -> Attach to Session -> 选择 Session ID,对应的 Session ID 可以通过 driver 创建好后 driver.session_id 查看
print('session_id:', self.driver.session_id)
Ui Automator Viewer Appium Inspector 其实是一个不太好的选择,一是慢,二是检测能力似乎不强,三是看不到元素层次,很多时候要点的元素被遮挡了
这里有两个现成的选择
最初的环境配置中其实已经加入了与这两个软件相关的环境变量配置,以下展示 Ui Automator Viewer 的界面
这里有一个BUG,如果用 MuMu 默认的平板分辨率,打开微信的时候在 uiautomatorviewer 显示的界面也会横过来,我的解决办法是直接把 MuMu 设成类似手机的分别率
Ui Automator Viewer 增强
用 lazyuiautomatorviewer.jar 替代 Sdktoolslib 下的对应文件,这个 Viewer 可以显示元素的 xpath,注意换的时候名字记得改成和原来的一样
from selenium.webdriver.common.by import Byfrom appium.webdriver.common.appiumby import AppiumByfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.common.exceptions import TimeoutException, NoSuchElementException'''查找元素实例'''element = driver.find_element(By.ID, 'com.tencent.mm:id/d5v')# 会返回第一个找到的,如果没找到会抛出 NoSuchElementException 错误# By.ID 是应该优先考虑的方法element = driver.find_elements(By.CLASS_NAME, 'android.widget.LinearLayout')# 返回所有符合元素的列表,如果没有符合的则返回空列表driver.find_element(By.XPATH,"//android.support.v7.widget.RecyclerView[@resource-id='com.tencent.mm:id/a5u']/android.widget.LinearLayout[1]")# By.XPATH 利用路径表达式查找元素driver.find_elements(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("'+text+'")')# 利用元素的 text 属性查找元素'''元素方法'''element.click()# 点击element.send_keys()# 输入'''元素等待'''def func(driver):return driver.find_elements(By.CLASS_NAME, 'android.widget.LinearLayout')WebDriverWait(driver = mydriver, timeout = 1, poll_frequency = 0.5).until(func)# 会在 timeout 时间内尝试调用 func(driver), 间隔 poll_frequency# 当 func 返回真时提前退出# 超时后抛出 TimeoutException 错误
完整代码2022.01.31:还没写呢,今年的除夕都已经过了,明年再写吧
2022.02.01:还是给它写了,加了个自动登录的功能,因为模拟器和手机上切换登录时总是要输密码,于是就自动化了
import timefrom typing import Listimport keyboardfrom appium import webdriverfrom appium.webdriver.common.appiumby import AppiumByfrom appium.webdriver.common.touch_action import TouchActionfrom appium.webdriver.webdriver import WebDriverfrom appium.webdriver.webelement import WebElementfrom selenium.common.exceptions import (NoSuchElementException, StaleElementReferenceException, TimeoutException)from selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitdef is_element_exist(driver, element, method='by_id', timeout=0, frequency=0.2): """ 监听 element_text 是否在 timeout 时间内存在 , 结果以列表形式返回 element_text: list 或 str 格式,当是 list 格式时表示是否存在其中任意一个 可选method有: 'by_text', 'by_id' """ def exist(driver: WebDriver) -> List[WebElement]: if method == 'by_text': def find(text): return driver.find_elements( AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("'+text+'")') elif method == 'by_id': def find(id): return driver.find_elements(By.ID, id) else: raise ValueError if isinstance(element, str): return find(element) else: for ele in element: if find(ele): return find(ele) return [] if timeout: try: WebDriverWait(driver, timeout, frequency).until(exist) except TimeoutException: return [] else: return exist(driver) else: return exist(driver)class RedEnvelope(): def __init__(self): self.desired_caps = { "platformName": "Android", "deviceName": "pzyhfagmr8nfd6hm", "platformVersion": "6.0.1", "appPackage": "com.tencent.mm", "appActivity": ".ui.LauncherUI", "newCommandTimeout": 6000, "noReset": True } self.driver = webdriver.Remote( 'http://127.0.0.1:4723/wd/hub', desired_capabilities=self.desired_caps) actions = TouchAction(self.driver) self.password = '不告诉你' self.grab_flag = True def login(self): # d5z:紧急冻结 cns:通讯录 if not is_element_exist(self.driver, ['com.tencent.mm:id/d5z', 'com.tencent.mm:id/cns'], 'by_id', 10): print('微信启动失败') if is_element_exist(self.driver, 'com.tencent.mm:id/d5z'): print('检测到微信被登出,正在重新登录') # d5v:切换验证方式 if is_element_exist(self.driver, 'com.tencent.mm:id/d5v'): self.driver.find_element( By.ID, 'com.tencent.mm:id/d5v').click() # f40:用xx登录[重复] if is_element_exist(self.driver, 'com.tencent.mm:id/f40', timeout=1): # 用密码登录 self.driver.find_element( By.XPATH, "//android.support.v7.widget.RecyclerView[@resource-id='com.tencent.mm:id/a5u']/android.widget.LinearLayout[1]").click() self.login_with_password() else: print('等待用密码登录入口超时') else: print('登录微信成功 (由保持登录记录)') def login_with_password(self): print('正在用密码登录') if is_element_exist(self.driver, 'com.tencent.mm:id/bhn', timeout=1): self.driver.find_element( By.ID, 'com.tencent.mm:id/bhn').send_keys(self.password) self.driver.find_element(By.ID, 'com.tencent.mm:id/d5n').click() else: print('等待密码输入框超时') if is_element_exist(self.driver, ['com.tencent.mm:id/cns'], 'by_id', 10): print('登录微信成功 (由键入密码)') def main(self): print('正在启动微信') self.login() def add_hotkey_stop_grab(self): def stop_grab(): self.grab_flag = False keyboard.add_hotkey('ctrl+alt+r', stop_grab) print('激活了热键 ctrl+alt+r 以中止抢红包') def grab(self): # 如果要实现稳定多会话抢红包, 得再删除没有抢到的红包(不会显示消息:你已领取xx的红包) self.grab_from_message() return '''多会话''' # b4r 微信->每个会话窗口 pers = is_element_exist(self.driver, 'com.tencent.mm:id/b4r') if pers: for per in pers: message = per.find_element(By.ID, 'com.tencent.mm:id/cyv').text if '[微信红包]' in message: per.click() self.grab_from_message() break else: self.grab_from_message() def grab_from_message(self): try: messages = is_element_exist( self.driver, 'com.tencent.mm:id/al7', timeout=1) for message in messages[::-1]: if is_element_exist(message, 'com.tencent.mm:id/ra'): # print('哇,发现一个红包!') env_app = is_element_exist( message, 'com.tencent.mm:id/r0') if env_app: pass '''气氛组''' # if env_app[0].text == '已领取': # print('哦,是领过的') # elif env_app[0].text == '已被领完': # print('啊,是没抢到的') # else: # print('咦,这是什么') else: print('抢它!') message.click() # den:开 bottom = is_element_exist( self.driver, 'com.tencent.mm:id/den', timeout=1, frequency=0.01) if bottom: bottom[0].click() # d_h 抢到xx元 dh = is_element_exist( self.driver, 'com.tencent.mm:id/d_h', timeout=2, frequency=0.01) if dh: print('抢到 '+dh[0].text+' 元') else: print('没抢到') self.driver.find_element( By.ID, 'com.tencent.mm:id/dm').click() else: print('没抢到') # dem:红包下的x self.driver.find_element( By.ID, 'com.tencent.mm:id/dem').click() '''多会话''' # self.actions.long_press(message) # self.actions.perform() # self.driver.find_element( # By.XPATH, "//android.widget.LinearLayout[@resource-id='com.tencent.mm:id/hyf']/android.widget.LinearLayout[1]").click() break except StaleElementReferenceException: return '''多会话''' # rr:又上角的返回 # rr = is_element_exist( # self.driver, 'com.tencent.mm:id/rr', timeout=1)[0] # rr.click() def grab_start(self, frequcency=0): print('开始抢红包') self.add_hotkey_stop_grab() self.grab_flag = True while self.grab_flag: self.grab() time.sleep(frequcency) print('抢红包中止')if __name__ == '__main__': R = RedEnvelope() R.main()# 建议用交互式# R.grab_start()
Append:Failed to create session 如果运行右下角 Start Session 后遇到一个报错
这里提到了原因:Default remote path should be “/wd/hub”
appium2 把 remote path 的默认设为/,此时最新版本的 Inspector 配合 appium2 也做了同步的更改,然而appium desktop 是面向新手的 GUI 版本,其内核迟迟没有更新到新的 appium2 ,解决这个问题简单的办法是下载旧版本的 Inspector,其中 2021.8.5 版本提到了这个修改