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

您所在的位置:网站首页 国术宗师免费 【杰哥带你玩转Android自动化】为自动打卡脚本 "赋能"

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

2023-12-23 16:22| 来源: 网络整理| 查看: 265

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

0x1、引言

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

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

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

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

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

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

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

① Android 11及以上

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

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

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

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

adb pair ip:端口号

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

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

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

adb connect ip:端口号

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

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

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

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

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

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

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

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

② Android 11以下

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

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

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

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

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

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

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

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

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

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

接着是 检测到正在使用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

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

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

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

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

④ 自动登录

有读者在评论区提到:

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

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

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

接着就是 筛出我们要用到的结点,有:请输入密码的输入框、同意协议的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)

运行输出结果如下:

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、小结

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

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

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

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

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

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

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

参考文献:

手把手教你用AirtestIDE无线连接手机!


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3