likes
comments
collection
share

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

作者站长头像
站长
· 阅读数 24

0x1、引言

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

Hi,我是杰哥,上节 《学亿点有备无患的"姿势"》 带着大家学习了这些姿势:

OCR文字识别、消息推送、图片处理、获取当前页面的所有控件信息;

我们掌握的自动化姿势好像已经挺多了,下相信读者们也早已摩拳擦掌,跃跃欲试要给自动打卡脚本赋能。

但还请 桥豆麻袋,容我再提一嘴两个笔者反馈到的问题~

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"


0x2、问题1:跑脚本得一直线接电脑太麻烦

如题,执行ADB脚本时,手机需要一直用数据线连着电脑,偶尔的接触不良 + 手滑一不小心碰到,直接就断开连接,脚本也中断执行...

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

我们着实需要一种可以 无线连接 的方法,巧了,ADB本身就支持,不过Android 11前后开启方法有些差异,下面容我一一道来~


① Android 11及以上

如果你的安卓鸡手机系统是Android 11,恭喜你,全程不需要线,稍微设置下就可以无线连接电脑。

以笔者的小米9为例:打开-设置找到并点击-开发者选项找到-调试找到并启用-无线调试点击-允许

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

开启无线调试模式后,接着:点击-无线调试点击-使用配对码配对设备

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

可以看到与手机配对的 WLAN配对码IP地址:端口,接着电脑打开命令行,键入配对命令:

adb pair ip:端口号

回车后提示输入配对码,输入回车即可,出现如下所示的Successfully说明匹配成功:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

在这里可以看到 手机的IP端和端口,在 已配对设备 还可以看到电脑的设备信息:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

配对成功后,后续只要使用下述命令连接手机即可:

adb connect ip:端口号

出现如下 connected 字样说明 连接成功

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

接着键入 adb devices 查看 连接手机的序列号

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

这个ip:port的序列号就是我们的手机,如果你是Android开发,在Android Studio的Logcat中也可以看到日志输出。

断开连接 也简单,手机直接关闭 无线调试 的开关即可,不过手机状态会变为offline:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

即手机掉线了,有时因为网络波动,重启ADB也会导致这种情况,如果需要重连,可以执行:adb connect ip:port

如果不想重连,或者不想看到它,可以执行 adb disconnect 命令断开连接:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

简单得不得了,妈妈再也不用担心我跑脚本要用线连电脑了~

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

当然,上述方法仅适用于 Android 11+,Android 11以下的手机系统需要 先线连,执行一点命令,再拔线


② Android 11以下

手机打开USB调试,然后 线连电脑,接着键入:adb tcpip 5555,5555是默认端口号,也可以根据自己需求改成其它的:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

出现左侧输出结果说明成功,右侧输出结果大概率是手机没打开USB调试,或者手机没连上,自行检查下~

接着 获取手机IP,依次点击:设置关于手机状态信息IP地址

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

当然,也可以在:设置WLAN点开当前连接的WIFI → 找到IP地址:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

拿到ip后,键入:adb connect ip:5555 即可连接设备:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

此时 拔掉数据线,发现 无线调试 依旧可用,可以,也很简单~

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"


0x3、问题2:APP能检测到我们使用ADB自动化控制设备吗?

前面说过,笔者在老东家时曾用Xposed虚拟定位插件打卡,被办公软件检测到,挨了人事一顿叼。

所以有些读者担心办公软件会不会检测到,我们是 通过ADB命令打开的APP,然后重蹈我的覆辙?

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

em...怎么说呢?有可能 (世事无绝对),但也不用太担心。从开发角度来说,检测 你是不是ADB命令打开的APP不大可能,APP一天打开几十次,每次打开都得检测一下,吃太饱???另外,如果你实在觉得不稳妥,解决方法也简单,直接在 手机桌面 (Launcher启动器) 定位到办公软件APP的图标,然后 模拟点击一下图片 不就好了?

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

接着是 检测到正在使用ADB自动化控制APP,我也觉得不太可能,搜了一轮网上相关的检测方案,有这些:

检测USB调试是否开启

val enableAdb = (Settings.Secure.getInt(getContentResolver(), Settings.Secure.ADB_ENABLED, 0) > 0)

不靠谱,因为在没执行adb命令时,adb调试也是可以打开的。

netstat查看adb端口是否有TCP连接

# 查看输出结果是否有adb端口(默认5555)
netstat -an | grep -V LISTEN | awk '{print $2}' | awk -F':' '{print $NF}'

# 附:如果adb默认端口被修改,还可以用下述命令动态获取
getprop service.adb.tcp.port

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

同方案①,不靠谱,并不能证明手机正在被adb控制。而且在APP层面,执行netstat命令会提示 没有权限,还有类似 读init.svc.adbd 文件的方法,同样如此。

这就是我了解到的一些检测方案,但都没太大用,如果有知道 APP检测设备正在被ADB控制 有效方案 的胖友,欢迎在评论区补充,感谢。

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

另外,个人觉得自动化被逮到的,大概率是因为 风控,如点击行为,每次都是固定点击,操作时间固定等等,所以在写自动化脚本时,可以考虑加上 随机,比如点击位置随机偏移(1-10)个像素等。


0x4、脚本赋能up↑

终于来到这一Part、一点点优化~

① 打卡时间范围内随机

每天早上都准时8:30打卡,这也太假了吧?我们可以设置一个打卡区间 → 8:30-8:45,在这15分钟里随机。

逻辑比较简单:先写一个固定8:30执行的脚本,生成0-15的随机数,然后开启一个定时任务,具体实现代码如下:

from random import randint
from time import sleep
from subprocess import call

task_file = "auto_clock_in.py"
cmd_str = f"python %s" % task_file

if __name__ == '__main__':
    sleep(randint(0, 15) * 60)  # 1-15分钟随机休眠
    call(cmd_str)

② 消息推送简单封装

就是把Server酱调用相关封装一下,提供一个发送API给我们直接调,标题最多32个字,拿来显示告警信息够用了,没太大必要还塞到desp或short中,不然收到推送还得点看卡片才知道具体啥信息:

import requests as r

send_key = "xxx"  # SendKey,官网自行获取
send_url = "https://sctapi.ftqq.com/%s.send" % send_key


def send_wx_message(title, desp="", short="", channel=9):
    """
    发送微信消息
    :param title: 标题,必填,最大长度32
    :param desp: 消息内容,选填,最大长度为 32KB
    :param short: 消息卡片内容,选填,最大长度64,不指定会自动截图desp的前30个显示
    :param channel: 渠道,支持最多两个通道,用竖线隔开,9为方糖服务号
    :return: None
    """
    resp = r.post(send_url, data={'title': title, 'desp': desp, 'short': short, 'channel': channel})
    if resp:
        if resp.status_code == 200:
            print("消息发送成功")
            return True
        else:
            print("消息发送失败")
            return False


if __name__ == '__main__':
    send_wx_message("测试标题", "测试消息内容\n\n" * 16, "测试卡片")

③ 检测adb调试是否可用

就执行下adb devices命令,然后对执行结果进行 正则匹配,匹配规则 → 打卡手机序列号\tdevice 没匹配到说明adb调试不可用,推送需要手动打卡的告警消息。

from adb_util import *
import re
from push_msg_util import send_wx_message

clock_in_devices_pattern = re.compile(r'4c5e8fa7\tdevice', re.S)


# Android手机adb调试是否可用
def is_adb_enable():
    output_result = analysis_result(start_cmd(f"adb devices"))
    # 输出结果正则匹配打卡设备
    search_result = re.search(clock_in_devices_pattern, output_result)
    if search_result:
        print("打卡设备adb调试可用")
        return True
    else:
        send_wx_message("打卡失败:打卡设备adb调试不可用,需要手动打卡!")
        return False

拔插数据线,运行验证代码,断开连接是,确认可以收到微信消息推送:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

④ 自动登录

有读者在评论区提到:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

的确,下班不看老板发的消息不太现实,毕竟资本家恨不得我们24h都在上班~

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

所以,可以备两台手机,一个平时用,一个专门打卡用。然后打卡时判断登录状态,未登录执行自动登录,打卡后消息推送告知,然后平时用的手机再登即可。

先是 检测登录,打开APP未登录会跳转到登录页,直接调用上节 《0x5、获取当前页面所有控件信息》 里的 current_ui_xml() 方法获得所有结点:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

接着就是 筛出我们要用到的结点,有:请输入密码的输入框同意协议的CheckBox登录按钮。直接写出递归代码:

# 递归获取需要的结点
def analysis_need_node(need_node_dict, origin_node):
    for node in origin_node.nodes:
        if node.text == '请输入密码':
            need_node_dict['et_password'] = node
        elif node.text == '登录':
            need_node_dict['bt_login'] = node
        elif node.class_name == 'android.widget.CheckBox':
            need_node_dict['cb_agree'] = node
        # 往下递归
        if len(node.nodes) > 0:
            analysis_need_node(need_node_dict, node)


# 检查是否登录,没登录自动登录
def auto_login():
    node = analysis_ui_xml(current_ui_xml())
    need_node_dict = {}
    analysis_need_node(need_node_dict, node)
    print(need_node_dict)

运行输出结果如下:

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

Good,拿到需要结点,接着就是自动登录的逻辑了:

  • 点击密码输入框;
  • 输入密码;
  • 勾选同意协议;
  • 点击登录;

这里点击坐标点去起始和终点坐标的平均值 (即点击控件中间),直接写出代码:

# 检查是否登录,没登录自动登录
def auto_login():
    node = analysis_ui_xml(current_ui_xml())
    need_node_dict = {}
    analysis_need_node(need_node_dict, node)
    
    # 点击密码输入框
    et_password = need_node_dict['et_password'].bounds
    click((et_password[0] + et_password[2]) / 2, (et_password[1] + et_password[3]) / 2)
    
    # 输入密码
    input_text('密码')
    
    # 勾选同意协议
    cb_agree = need_node_dict['cb_agree'].bounds
    click((cb_agree[0] + cb_agree[2]) / 2, (cb_agree[1] + cb_agree[3]) / 2)
    
    # 点击登录
    bt_login = need_node_dict['bt_login'].bounds
    click((bt_login[0] + bt_login[2]) / 2, (bt_login[1] + bt_login[3]) / 2)

⑤ 检测是否打卡成功

自动登录完跳转,一般会执行快速打卡,当然为了保证万无一失,我们还需要检测是否真的打卡成功。核心还是关键字筛选,只需检测信息列表页是否有 极速打卡 字样。

可以简单粗暴地使用上节提到的 chineseocr_lite 直接进行OCR识别,然后筛识别结果。这里笔者偷下懒,还是直接用获取控件信息然后筛结点的方式。

# 递归获取自动打卡相关结点
def analysis_clock_in_node(need_node_dict, origin_node):
    for node in origin_node.nodes:
        if node.text == '快速打卡':
            need_node_dict['quick_clock_info'] = node
        if node.text == '智能工作助理':
            need_node_dict['work_helper_robot'] = node
        # 往下递归
        if len(node.nodes) > 0:
            analysis_clock_in_node(need_node_dict, node)


def auto_clock_in():
    node = analysis_ui_xml(current_ui_xml())
    need_node_dict = {}
    analysis_clock_in_node(need_node_dict, node)
    # 结点找到说明处于登陆状态
    if need_node_dict['quick_clock_info'] is not None:
        send_wx_message("打卡成功:手机自动快速打卡【%s】!" % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        # 结点未找到,主动发起打卡

⑥ 主动发起打卡

如果自动打卡不成功,就需要我们主动发起打卡了,流程如下:

  • 点击 智能工作助理
  • 静待片刻,查找并点击 打卡 结点;
  • 静态片刻,查找并点击 马上打卡 结点;
  • 静态片刻,查找 打卡成功 结点,存在说明打卡成功,推送打卡成功消息;
  • 不存在说明打卡失败,推送打卡失败需手动打卡的消息;

比较简单,直接给出实现代码:

def auto_clock_in():
    node = analysis_ui_xml(current_ui_xml())
    need_node_dict = {}
    analysis_clock_in_node(need_node_dict, node)
    # 结点都没找到说明处于登陆状态
    if need_node_dict['quick_clock_info'] is not None:
        send_wx_message("打卡成功:手机自动快速打卡【%s】!" % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    else:
        # 点击智能工作助理
        cb_agree = need_node_dict['work_helper_robot'].bounds
        click((cb_agree[0] + cb_agree[2]) / 2, (cb_agree[1] + cb_agree[3]) / 2)
        sleep(3)
        # 查找打卡结点
        clock_in_node(need_node_dict, node)
        if need_node_dict['clock_in'] is not None:
            sleep(2)
            # 查找马上打卡结点
            quick_clock_in_node(need_node_dict, node)
            if need_node_dict['quick_clock_in'] is not None:
                sleep(2)
                click_in_result_node(need_node_dict, node)
                if need_node_dict['clock_success_result'] is not None:
                    send_wx_message("打卡成功:主动打卡【%s】!" % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                else:
                    send_wx_message("打卡失败:主动打卡,但未检测到打卡成功结果")
            else:
                send_wx_message("打卡失败:主动打卡,为查找到快速打卡结点")
        else:
            send_wx_message("打卡失败:主动打卡时未找到打卡结点")


# 查找打卡结点
def clock_in_node(need_node_dict, origin_node):
    for node in origin_node.nodes:
        if node.text == '打卡':
            need_node_dict['clock_in'] = node
        # 往下递归
        if len(node.nodes) > 0:
            clock_in_node(need_node_dict, node)


# 查找快速打卡结点
def quick_clock_in_node(need_node_dict, origin_node):
    for node in origin_node.nodes:
        if node.text == '马上打卡':
            need_node_dict['quick_clock_in'] = node
        # 往下递归
        if len(node.nodes) > 0:
            quick_clock_in_node(need_node_dict, node)


# 查找打卡成功结点
def click_in_result_node(need_node_dict, origin_node):
    for node in origin_node.nodes:
        if node.text == '打卡成功':
            need_node_dict['clock_success_result'] = node
        # 往下递归
        if len(node.nodes) > 0:
            click_in_result_node(need_node_dict, node)

0x5、小结

【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

本节为我们的自动化脚本「赋能」就先讲解到这,应该够用了,当然,脚本代码并不太优雅 (如冗余的递归遍历结点的代码等)。因为录屏可能会泄露笔者的一些隐私信息就不放效果图了,代码基本给全了,读者可以copy下,改着玩,有问题可以在评论区提出。

在开篇 《杰哥带你玩转Android自动化】学穿:ADB》 中说到过:

所有Android自动化框架和工具中 操作Android设备的功能实现 都基于 adb无障碍服务AccessibilityService

通过这几章的学习,PC端控制Android设备自动化 对读者来说应该是随意拿捏了。凭借PC的强大性能,我们可以轻松地快速进行一些复杂运算、图片处理等操作。但也有一个很明显的短板 → 不够方便,无论跑什么脚本,无论有线还是无线,你都需要一台PC。

总不能每天背着个笔记本到处走吧...比如,有时我们想在地铁上完成一些简单的APP日常,有没有更轻量级一点的 Android端自动化方案 呢?

当然有,下节杰哥将带着大家学习 无障碍服务AccessibilityService 的详细玩法,敬请期待~


声明:自动打卡脚本只是个练手案例,如果真的拿来使用 造成的后果使用人自担,笔者自己宁愿迟到也不会用,做人还是要诚实,经常迟到的同学建议早上早点出门,早起地铁不挤还是很香的~

参考文献