从0开始的appium+Android+python自动抢红包世界生活

前言

关于 appium 是什么之类的不再赘述,有关的博文已经很多了,本文旨在提供截至 2022 年除夕可以在 Windows 11 上复现的操作方法
虽然这篇博文被看到的时候应该只能用在 2023 年的春节了

环境配置

  1. 安装 appium desktop
    从0开始的appium+Android+python自动抢红包世界生活_第1张图片

  2. 安装 appium-inspector
    从0开始的appium+Android+python自动抢红包世界生活_第2张图片

  3. 安装 JDK
    从0开始的appium+Android+python自动抢红包世界生活_第3张图片

  4. 安装 MuMu 模拟器
    安装好后在 系统应用 -> 设置 -> 开发者选项 中打开 USB调试
    同时查看 设置 -> 关于平板电脑 -> Android版本,我的是 6.0.1
    从0开始的appium+Android+python自动抢红包世界生活_第4张图片
    从0开始的appium+Android+python自动抢红包世界生活_第5张图片

  5. 借助 Android Studio 安装 SDK
    首先下载+安装+运行 Android Studio ,进入到欢迎界面,再进入 SDK 管理
    从0开始的appium+Android+python自动抢红包世界生活_第6张图片
    从0开始的appium+Android+python自动抢红包世界生活_第7张图片
    安装 6.0 是因为 MuMu 模拟器是 6.0.1

  6. 进行一堆环境变量的设置
    Win+R 运行 control system -> 相关链接 -> 高级系统设置 -> 环境变量

    1. 下方环境变量区中新建

      变量名:ANDROID_HOME
      变量值:下图所示从0开始的appium+Android+python自动抢红包世界生活_第8张图片
      变量名:JAVA_HOME
      变量值:刚才 JDK 的安装目录
      如:C:\Program Files\Java\jdk-17.0.2从0开始的appium+Android+python自动抢红包世界生活_第9张图片
      变量名:CLASSPATH
      变量值:.;%JAVA_HOME%\lib;

      变量名:prog_dir
      变量值:%ANDROID_HOME%\platform-tools

      变量名:ANDROID_SWT
      变量值:%ANDROID_HOME%\tools\lib\x86_64

    2. 已有的系统变量中找到Path -> 编辑 -> 编辑文本 -> 在文末插入
      %JAVA_HOME%\bin;%ANDROID_HOME%;%ANDROID_HOME%/tools;%ANDROID_HOME%/platform-tools;
      从0开始的appium+Android+python自动抢红包世界生活_第10张图片

    3. 测试
      在 cmd 中运行

      C:\Users\username> java -version
      C:\Users\username> javac -version
      C:\Users\username> adb devices
      

      结果类似下图即说明设置成功
      从0开始的appium+Android+python自动抢红包世界生活_第11张图片

  7. 安装 appium 的 Python 驱动
    pip3 install appium-python-client

链接Android模拟器

如果在 cmd 下执行 adb devices 显示出有设备链接了那就万事大吉
但是 MuMu 应该是没办法被自动检测到的,如下位置 MuMu 告诉我们运行

adb connect 127.0.0.1:7555
从0开始的appium+Android+python自动抢红包世界生活_第12张图片
从0开始的appium+Android+python自动抢红包世界生活_第13张图片

appium 的准备工作

分别打开 Appium Server GUI 和 Appium Inspector,原本他们是在一起的,现在分成了两个程序

GUI:直接 Start ,此时 Edit Configuration 里面应该已经自动填上了刚才设置的JAVA_HOME 和 ANDROID_HOME
从0开始的appium+Android+python自动抢红包世界生活_第14张图片
Inspector

  1. 如果你下载最新 Appium Desktop 时仍有如下提示,请把 Insepector 中的 Reomote Path 改成 /wd/hub ,否则会报错(见 Append:Failed to create session)从0开始的appium+Android+python自动抢红包世界生活_第15张图片
    从0开始的appium+Android+python自动抢红包世界生活_第16张图片

  2. 编辑 Desired Capabilities,我们直接用 JSON 格式
    从0开始的appium+Android+python自动抢红包世界生活_第17张图片

    {
        "platformName": "Android",
        "deviceName": "MuMu",
        "platformVersion": "6.0.1",
        "appPackage": "com.tencent.mm",
        "appActivity": ".ui.LauncherUI",
        "newCommandTimeout": 6000,
        "noReset":true
    }
    

    有关 Desired Capabilities 的官方文档
    . newCommandTimeout: 一个 session 在未接收到任何命令多久后会自动关闭,默认 60s 太短了

  3. Start Session
    从0开始的appium+Android+python自动抢红包世界生活_第18张图片
    看到这个大大的地球我们就成功了,至此最艰难的步骤都已经完成了

  4. 补充:包名获取
    这里任意 appPackage 和 appActivity 的获取可以用以下方式,仍以微信为例:
    在 MuMu 上打开微信
    在 cmd 中运行 adb shell 进入模拟器操作系统
    运行 dumpsys window windows | grep mFocusedApp,对照一下就懂了,其中第一次运行时是在登录界面,第二次是在联系人界面,所以 appActivity 不同
    在这里插入图片描述

  5. Inspector 演示
    点击我们想要了解的地方就可以看到 id (即 resource_id) 和相关信息了。注意它的界面不是自动同步的,必须手动点一下上面的刷新才会同步
    在这里插入图片描述
    一些例子:
    从0开始的appium+Android+python自动抢红包世界生活_第19张图片
    从0开始的appium+Android+python自动抢红包世界生活_第20张图片
    从0开始的appium+Android+python自动抢红包世界生活_第21张图片
    接下来我们先放下这边的工作,先构建脚本框架

脚本编写

from appium import webdriver
from selenium.webdriver.support.ui import WebDriverWait


class 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 来分析 UI

Python 代码运行起来后,Inspector -> Attach to Session -> 选择 Session ID,对应的 Session ID 可以通过 driver 创建好后 driver.session_id 查看

print('session_id:', self.driver.session_id)

从0开始的appium+Android+python自动抢红包世界生活_第22张图片

Ui Automator Viewer

Appium Inspector 其实是一个不太好的选择,一是慢,二是检测能力似乎不强,三是看不到元素层次,很多时候要点的元素被遮挡了
这里有两个现成的选择

  1. sdk\tools\bin 下的 uiautomatorviewer.bat ,但是 Java 版本必须是 Java 8
  2. sdk\tools 下的 monitor.bat

最初的环境配置中其实已经加入了与这两个软件相关的环境变量配置,以下展示 Ui Automator Viewer 的界面
从0开始的appium+Android+python自动抢红包世界生活_第23张图片

这里有一个BUG,如果用 MuMu 默认的平板分辨率,打开微信的时候在 uiautomatorviewer 显示的界面也会横过来,我的解决办法是直接把 MuMu 设成类似手机的分别率

Ui Automator Viewer 增强
用 lazyuiautomatorviewer.jar 替代 Sdk\tools\lib 下的对应文件,这个 Viewer 可以显示元素的 xpath,注意换的时候名字记得改成和原来的一样

Appium 光速入门

from selenium.webdriver.common.by import By
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from 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 time
from typing import List

import keyboard
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver.common.touch_action import TouchAction
from appium.webdriver.webdriver import WebDriver
from appium.webdriver.webelement import WebElement
from selenium.common.exceptions import (NoSuchElementException,
                                        StaleElementReferenceException,
                                        TimeoutException)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


def 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 后遇到一个报错
从0开始的appium+Android+python自动抢红包世界生活_第24张图片
这里提到了原因:Default remote path should be “/wd/hub”
appium2 把 remote path 的默认设为/,此时最新版本的 Inspector 配合 appium2 也做了同步的更改,然而appium desktop 是面向新手的 GUI 版本,其内核迟迟没有更新到新的 appium2 ,解决这个问题简单的办法是下载旧版本的 Inspector,其中 2021.8.5 版本提到了这个修改
从0开始的appium+Android+python自动抢红包世界生活_第25张图片

你可能感兴趣的:(没啥用的东西,python,android,appium)