外卖App短信验证码自动登录与登录态保活方案
做登录自动化这件事,最容易被低估的就是“短信验证码登录”这一环。账号密码登录好歹是一段固定的表单提交,接上接口就能跑;而短信验证码登录天然带着三个不确定性:验证码要等、验证码只能用一次、验证码还可能被风控盯上。我前后在几个外卖类 App 的自动化场景里磨过这套流程,从最早的 Appium 硬点,到后来拆成驱动层加短信层再做登录态持久化,中间踩的坑够写一篇长文。这篇就把“外卖平台 App 手机短信验证码自动登录”这套方案完整摊开讲:它解决的是重复登录、反复收码、会话过期后手工补登的问题,适合做自动化测试、个人效率工具、以及需要长期维持登录态的自用脚本的同学参考。不管你是刚接触 UI 自动化,还是已经写过几版脚本但一直被元素定位和掉登录折磨,下面的内容应该都能直接抄走一部分。
1. 需求拆解:短信登录自动化到底难在哪
1.1 短信验证码登录和账号密码登录的本质差异
很多人第一反应是“不就是多点几下吗”,真写起来才发现完全不是一个量级。账号密码登录的输入是确定的,你把用户名密码塞进输入框,点提交,拿返回值,结束。整个过程是幂等的,同一天跑一百次结果都一样。短信验证码登录不是,它是一条带时间窗口和状态机的链路:请求下发、等待到达、解析提取、回填提交,每一步都有失败可能,而且失败之后不能简单重试——你重试一次就得重新请求一次验证码,请求太频繁又会触发平台的频控策略。
我在第一版脚本里就犯过这个错,把“点获取验证码”和“填验证码”写成了一条直线,任何一步超时就整段重跑。结果跑十次有六次卡在“验证码发送过于频繁,请稍后再试”。后来才想明白,这套流程的核心不是“点得准”,而是状态可恢复:每个环节都要能判断当前处于哪一步,失败了从当前步继续,而不是从头再来。
再往深一层,账号密码登录你可以完全脱离 UI 走接口,短信登录绕不开“拿到那条短信”这个物理动作。这条短信在你的手机上、在你运营商的网络里,脚本看不见。所以整套方案必须多出一个“短信层”,这是它和普通登录自动化最大的结构差异。
1.2 三种登录入口的差异与选型判断
同一个平台,通常有三条登录路径:原生 App、内嵌 H5、以及各类小程序容器。三条路的自动化成本差别很大,选错了后面全是苦活。
| 入口类型 | 驱动方案 | 短信读取方式 | 登录态保存 | 维护成本 |
|---|---|---|---|---|
| 原生 App | Appium / UIAutomator2 | 设备短信或转发服务 | 设备本地会话,难导出 | 高,随版本变动 |
| 内嵌 H5 | Playwright / Selenium | 同上,走 HTTP 回调 | storage_state 一键导出 | 中,随页面改版 |
| 小程序容器 | 官方自动化工具 | 同上 | 容器内缓存 | 偏高,调试不便 |
如果只是自己用、追求稳定,我的建议是优先走 Web 或 H5 端。原因很实在:Playwright 的storage_state能把 cookie 和 localStorage 一把梭导出成 JSON,下次直接new_context(storage_state=...)就是登录态,根本不用重新走一遍短信流程。而 App 端的登录态藏在应用私有目录里,不 root 基本拿不出来,你能做的只有“保持这台设备不退出登录”,一旦掉登录就得重来。
不过现实里很多功能只在 App 端有,那就只能硬啃 Appium。这时候策略要反过来:把登录频率降到最低,用长周期保活换稳定性,而不是每次都重新登。
1.3 合规边界:哪些能做,哪些碰不得
这块必须说清楚,不然方向一开始就歪了。自动登录这件事本身是中性的,用在自动化测试、个人自用工具上完全没问题,但有几条线不能碰。
第一,只操作你自己实名持有的账号。给别人账号做自动登录、代收验证码,性质就变了。第二,不做批量注册和批量养号。这类行为明确违反平台规则,技术上也必然要跟风控对抗,属于无底洞。第三,遇到图形验证码、滑块、行为验证时,正确做法是中断流程转人工,而不是接打码服务去硬破。我见过太多脚本死在这一步,本来只是偶尔弹一次验证,非要自动化处理,最后把账号搞进高风险名单。
提示:项目启动前先想清楚用途。凡是要“绕过”点什么才能跑通的方案,都不是可持续的方案,早晚会因为对方策略升级而全线崩塌。
2. 方案整体设计与技术选型
2.1 分层架构:把不确定性关进笼子里
我最终的架构是四层,这个划分不是拍脑袋,是被失败率逼出来的。第一版脚本是一坨面条式代码,出问题完全不知道卡在哪,后来按职责切开,排查效率直接翻倍。
控制层负责状态机调度。它维护一个当前步骤的枚举:IDLE → LAUNCHED → PHONE_FILLED → CODE_REQUESTED → CODE_RECEIVED → SUBMITTED → LOGGED_IN,每一步都有前置条件和超时。好处是任何时刻异常退出,重启后读一下持久化的状态就知道从哪继续。
驱动层是真正操作界面的部分,Appium 或者 Playwright 都归这里。它的职责单一:找元素、点击、输入、截图。所有跟“怎么点”相关的脏活都封在这一层,上层不需要知道是 App 还是浏览器。
短信层负责拿到验证码。它对外只暴露一个方法wait_for_code(phone, timeout),内部怎么实现(读设备短信、查转发服务、还是弹窗让人工填)上层不关心。这层抽象特别关键,因为你的短信获取方式大概率会换好几次。
会话层管登录态的保存、加载和有效性检测。它是整个系统的性能关键——只要这层判断“当前已登录”,前面三层全部跳过。
2.2 驱动层选型:为什么我最后留下两个方案
Appium 和 UIAutomator2 我都长期用过。Appium 的优势是生态成熟、支持跨平台、调试工具链完整,缺点是启动慢,每次find_element都要走一遍 WebDriver 协议,单次操作百毫秒级。UIAutomator2 快得多,它直接在设备上跑一个 RPC 服务,操作延迟能压到几十毫秒,但绑定 Android,iOS 用不了。
我的实际选择是:主力用 UIAutomator2,需要跨端或者复杂手势时切 Appium。判断标准很简单,如果脚本一天要跑几十次登录流程,延迟差异会累积成分钟级的时间成本,那就值得上 UIAutomator2;如果只是每天几次保活检查,Appium 的稳定性更省心。
Web 端没得选,Playwright 在这个场景下碾压 Selenium:自带等待机制、storage_state开箱即用、对移动端 H5 的触摸事件模拟更自然。Selenium 也不是不行,但你需要自己写一堆WebDriverWait,而且 cookie 的导入导出要手动拼,麻烦。
2.3 验证码获取的三条路径与取舍
这是整套方案里最需要提前定的事,因为它决定了你的部署形态。
路径一:设备短信直读。在 Android 设备上拿短信权限,监听短信到达并正则提取验证码。优点是延迟最低,通常 2 到 5 秒就能拿到。缺点是权限敏感,新系统版本对短信权限的管控越来越严,而且只适合一台设备一个号码的自用场景。
路径二:短信转发服务。手机上装一个转发应用,把短信内容 POST 到你本地的一个 HTTP 服务,脚本去这个服务轮询。好处是解耦,脚本和手机不用在同一台机器上,也不受权限限制。代价是多了一个中间环节,网络抖动会影响延迟。
路径三:人工介入。脚本跑到需要验证码时暂停,弹个通知,人手动填。听起来很土,但我在生产环境里一直保留这条兜底路径,因为前两条都可能挂,而人几乎不会挂。
我的实际配置是主用转发服务,辅以人工兜底,直读作为可选加速。转发服务用一个十几行的 HTTP 服务就够了,脚本侧写个轮询函数,timeout设 90 秒,因为短信偶尔会因为网络原因延迟到这个量级。
3. 环境搭建与基础配置
3.1 移动端环境:设备、驱动与那些必开的开关
先说设备。Android 建议用真机而不是模拟器,原因很直接:很多平台的 App 在模拟器上会走不同的风控分支,登录成功率明显偏低。如果实在没有真机,至少选一个带完整 Google 服务框架的模拟器镜像,指纹看起来正常一点。
设备上必须打开的开关有三个。开发者选项里的 USB 调试,这是 adb 通信的前提。允许安装未知来源应用,UIAutomator2 需要在设备上装一个 agent apk。关闭电池优化,否则系统休眠时会杀掉 agent 进程,表现为脚本跑着跑着突然连不上设备。
驱动侧,我一般是这样一套:
# 安装核心依赖 pip install uiautomator2 appium-python-client playwright # 初始化设备(首次连接) python -m uiautomator2 init # 确认设备在线 adb devicesuiautomator2 init这一步会在设备上安装 ATX-agent,装完之后设备重启也不需要重连,agent 会自启动。这一步失败率不低,常见原因是设备上装了某些安全软件拦截了安装,或者 USB 授权弹窗没点确认。
Playwright 的初始化更简单:
playwright install chromium装完就可以用chromium.launch()了。移动端 H5 调试时记得开--device参数指定机型,因为不同的 UA 和视口会影响页面渲染,进而影响元素定位。
3.2 项目目录结构与配置项设计
目录结构我踩过一个坑:早期所有配置硬编码在代码里,换个手机号要改七八处。后来统一收到一个配置文件,改一次全局生效。
project/ ├── config/ │ ├── device.yaml # 设备信息、包名、应用入口 │ └── account.yaml # 手机号等账号信息 ├── core/ │ ├── driver.py # 驱动封装 │ ├── sms.py # 短信层 │ └── session.py # 会话层 ├── pages/ │ └── login_page.py # 登录页元素定义 ├── logs/ └── run.pydevice.yaml里我最看重的几个字段:
device: platform: android udid: "自设备序列号" app_package: "目标应用包名" launch_activity: ".MainActivity" implicit_wait: 15 screenshot_on_fail: true login: phone: "138****0000" code_timeout: 90 max_retry: 2 retry_interval: 65 # 注意这个数,必须大于验证码倒计时retry_interval这个参数特别容易翻车。很多脚本重试时只等 3 秒,结果按钮还在倒计时状态,点了没反应,然后报“元素不可点击”。把重试间隔设成大于平台倒计时(通常是 60 秒)是一个成本极低但有效的保护。
3.3 登录页元素的采集方法
元素定位不准是新手最大的时间黑洞。我的经验是:先在设备的开发者工具里把元素树完整抓出来,再决定用什么定位策略,而不是边试边改。
UIAutomator2 可以用内置的查看器:
import uiautomator2 as u2 d = u2.connect("设备序列号") d.app_current() # 确认当前前台应用 print(d.dump_hierarchy()) # 打印完整元素树Playwright 侧更简单,打开PWDEBUG=1环境变量启动,直接用内置检查器点选元素。
采集完元素树之后,定位策略的优先级我固定成这样:资源 ID 优先,其次是内容描述,最后才是文本和坐标。资源 ID 在版本升级时最稳,文本会随文案改版而变,坐标更是换个分辨率就废。这个顺序不是理论,是我被改版坑过三四次之后定下来的规矩。
4. 核心实现:从唤起登录页到拿到登录态
4.1 稳定定位的三个实战技巧
先说一个反直觉的结论:登录页最不稳定的元素往往不是输入框,而是挡在前面的各种弹层。你脚本一启动,迎面而来可能是定位权限申请、通知权限申请、青少年模式提示、运营商一键登录弹窗、以及“发现新版本”的升级提示。这几个东西不处理,后面所有步骤都是白费。
我写了一个dismiss_interruptions函数,每次进页面先扫一遍可能的弹层,用exists加短超时快速判断,有就关,没有就跳过。关键是每个判断的超时都要小,比如 1 到 2 秒,不然五六个弹层扫一遍就耗掉十几秒。
def dismiss_interruptions(d, max_round=3): """循环清理遮挡弹层,最多扫 max_round 轮""" handled = [] for _ in range(max_round): found = False # 一键登录弹层的关闭按钮(文案各不相同,用多种可能) for text in ["关闭", "其他方式登录", "以后再说", "暂不更新"]: el = d(text=text) if el.exists(timeout=1): el.click() handled.append(text) found = True break if not found: break return handled第二个技巧是分格验证码输入框的处理。很多平台的验证码是 4 到 6 个独立小格子,你往第一个格子set_text只会填一位。解决办法是先找到隐藏的完整输入框(通常在分格容器外层,是一个透明或零宽的 EditText),直接把完整验证码塞进去,界面会自动分发到各格。找不到的话就逐个格子点击输入,但那样失败率高得多。
第三个技巧是协议勾选框。这个坑我踩得最惨——所有步骤都对,就是登录不成功,查了半天发现是“我已阅读并同意用户协议”没勾。而且这个框的位置经常在按钮上方,尺寸很小,click容易点偏。稳妥做法是先判断选中状态(checked属性),没选中再点,点完再校验一次。
4.2 手机号填写与获取验证码的节奏控制
手机号输入有个隐藏细节:输入框里可能预填了上次登录的号码,或者被“一键登录”自动带出了本机号码。如果你直接set_text,某些实现会把新内容追加而不是替换。我的做法是先清空再输入:
phone_input = d(resourceId="com.xxx.xxx:id/phone_input") phone_input.click() phone_input.clear_text() phone_input.set_text(phone)清空之后还要校验一遍,读回get_text()确认内容确实等于目标号码。这个校验看着多余,但确实救过我一次——那次是输入法被切换成了中文全键盘,输入数字时带进了全角字符,肉眼看不出来,提交后平台报“手机号格式不正确”。
点击“获取验证码”之前的节奏控制,是我认为最值得写的一段。不要页面一加载就立刻点。原因有两层:一是页面可能还在渲染,按钮实际还没挂上点击事件;二是不少平台会采集点击时间间隔做行为特征,从页面出现到点击只隔 50 毫秒这种机械节奏,正常人的操作是不会出现的。我现在的做法是:等按钮clickable变为 true,再随机延迟 300 到 800 毫秒,然后点击。
这个延迟不是为了对抗什么,就是让流程更接近正常操作,减少被误判为异常导致的登录失败。我实测把这个随机延迟加上之后,同样的账号同样的网络,登录成功率从七成多提到了九成以上。
4.3 验证码的等待、提取与回填
短信层的核心是一个带超时的轮询,加上去重逻辑。去重这块必须做,因为我遇到过不止一次:脚本请求了一次验证码,但手机因为网络延迟,把两条不同时间发送的短信几乎同时推过来,脚本取到了旧的那条,填进去自然失败。
import re import time import requests def wait_for_code(phone, timeout=90, since_ts=None): """从本地转发服务拉取验证码,只接受 since_ts 之后的短信""" since_ts = since_ts or time.time() deadline = time.time() + timeout seen = set() while time.time() < deadline: try: r = requests.get( "http://127.0.0.1:8899/sms", params={"phone": phone, "since": int(since_ts)}, timeout=5, ) for item in r.json().get("items", []): key = item["ts"] if key in seen: continue seen.add(key) m = re.search(r"(\d{4,6})", item["content"]) if m: return m.group(1) except requests.RequestException: pass time.sleep(2) return None这里几个参数都值得说说。timeout=90是因为我统计过短信到达时间的分布,95 分位大概在 40 秒左右,90 秒足够覆盖异常情况又不至于卡太久。since_ts是请求验证码那一刻的时间戳,用来过滤过期短信,这个参数是修复“拿到旧验证码”问题的关键。轮询间隔 2 秒是个平衡点,太密会给本地服务压力,太疏会拖慢整体流程。
拿到验证码之后别急着填。我一般会先确认验证码输入框已经可交互(有些页面在短信发送成功后才把输入框从禁用状态解开),再填。填完先不提交,读回内容做一次比对,确认没被输入法吞字符。
提交按钮的点击有个小细节:点击后不要立刻判断结果,先等一个明确的信号。最可靠的信号是页面 URL 变化(Web 端)或者某个登录后才会出现的元素出现(App 端),而不是等固定时长。我早期用sleep(5),结果有次网络慢,5 秒后页面还没跳转,脚本以为失败了去重试,反而把已经成功的登录状态搞乱。
4.4 登录态的提取、保存与复用
这部分是整个方案能不能省时间的关键。做得好,后续所有操作都不需要再碰短信。
Web 端最舒服,Playwright 的上下文状态可以直接序列化:
from playwright.sync_api import sync_playwright import json STATE_FILE = "auth_state.json" def ensure_login(page, context): context.storage_state(path=STATE_FILE) if not os.path.exists(STATE_FILE) else None # 复用时 # context = browser.new_context(storage_state=STATE_FILE) def save_state(context): context.storage_state(path=STATE_FILE)storage_state把 cookie、localStorage、sessionStorage 一起打包成 JSON,下次新建上下文时传进去,页面打开就是登录态。这里有个坑:部分平台把关键凭证存在 sessionStorage 里,而 sessionStorage 的生命周期绑定标签页,新建上下文时虽然能恢复,但如果页面有“刷新即失效”的逻辑就要额外处理。判断方法很简单,登录成功后导出状态,看看关键 token 在不在里面。
App 端麻烦一些。token 通常存在应用的私有目录,比如shared_prefs下的 XML 或者某个数据库文件。不 root 的设备读不到,能做的替代方案有两个:一是用adb backup备份应用数据(很多应用主动禁用了这个能力),二是干脆不提取,靠保持设备不退出登录+定期保活。
我最后选的是第二条。具体做法是写一个check_session函数,每次运行前先启动 App,看是否直接进到首页而不用登录,是就跳过整个登录流程。
def is_logged_in(d, package): d.app_start(package) time.sleep(3) # 登录后才会出现的元素,比如"我的订单" if d(text="我的订单").exists(timeout=5): return True # 出现登录页特征元素则判定未登录 if d(text="获取验证码").exists(timeout=2): return False return False这段逻辑的核心是双向判断:既要能找到登录后的特征,也要能找到未登录的特征,两者都不匹配时保守地返回 False。我见过只判登录后元素的写法,页面加载慢一点就误判成未登录,然后开始跑登录流程,把好好的会话给顶掉了。
4.5 会话保活与失效检测的节奏设计
保活不是越勤越好。我以前设过每小时检查一次,结果某段时间账号频繁出现异常提示,改成每 6 小时之后反而稳了。频率这件事,遵循“够用就行”的原则。
保活方式分两种。被动保活是每次执行实际业务操作时顺带检查,不额外发请求,成本为零。主动保活是定期访问一个需要登录态的轻量页面(比如个人中心),确认返回正常。我一般用被动为主,主动保活设成每天一次,只在实际有任务时触发。
失效检测要区分三种情况。明确失效是跳转到登录页,这个好处理,直接重登。软失效是页面能打开但接口返回未授权,这种最难发现,需要脚本在业务请求失败时检查返回码,如果连续多次失败且状态码指向鉴权问题,才判定需要重登。风控软封是能打开但功能受限,这种情况千万不要反复重登,越登越糟,正确做法是停下来,等一段时间再说。
5. 常见问题与排查实录
5.1 元素找不到、点击无效的排查路径
这是报错频率最高的一类。我的排查顺序是固定的四步:截图看现场 → dump 元素树看结构 → 判断是否被遮挡 → 检查是否在正确的页面。
第一步截图最重要,很多问题看一眼截图就明白了,比如页面卡在启动广告、或者弹了个你没预料到的权限框。所以我在find_element失败时自动截图存到logs/下,文件名带时间戳和步骤名,事后回看效率极高。
第二步 dump 元素树。常见的意外是元素存在但没有resource-id,或者 ID 是动态生成的(带一串数字后缀),这种就不能硬编码,得用前缀匹配或者文本定位。UIAutomator2 支持正则:
d(resourceId="com.xxx.xxx:id/btn_login", className="android.widget.Button")用多个属性组合定位能大幅提高准确性,代价是版本升级时维护点变多,需要权衡。
第三步判断遮挡。有时候元素确实存在且可见,但被一个透明的覆盖层挡住,点击事件被拦截。判断方法是取元素的bounds中心点,查一下那个坐标上的实际元素是不是目标元素。如果不是,说明有东西盖着。
第四步最容易被忽略:你以为在登录页,其实早就跳走了。比如点击手机号输入框时误触了某个链接,页面已经变成注册页或者活动页。所以在每个关键步骤前加一次app_current()或者 URL 校验,能省下大量莫名其妙的调试时间。
5.2 验证码收不到、延迟、串号的应对
验证码问题我归成三类,处理方式完全不同。
收不到:先确认运营商侧是否正常(换个人工请求一次试试),再确认手机信号和拦截软件。我遇到过手机自带的骚扰拦截把平台短信过滤到了“垃圾箱”,转发应用读的是主收件箱,自然拿不到。解决办法是把发送号码加白名单。
延迟:短信延迟到 60 秒以上时,脚本的轮询窗口要足够长。我把超时从 45 秒加到 90 秒之后,这类失败基本消失了。另外轮询的日志要打出来,能看到“第几秒收到的”,长期看数据就知道该设多少。
串号:如果一台设备绑了多个平台,或者同一平台重发过多次,短信会混在一起。解决办法是两条:正则里带上平台的标识词做初筛,以及用since_ts严格过滤时间。还有个小技巧是记录每次请求验证码的序号,短信内容里如果有“您本次验证码为”,日志里把请求时间和短信到达时间都打出来,排查时一目了然。
5.3 登录后被判定异常的处理思路
这个问题最让人头疼,因为它没有明确报错,表现是登录成功但功能用不了,或者过一会儿就掉登录。
我总结的经验是先排查自己的行为特征,而不是一上来就怀疑对方策略。常见的原因有这么几个:登录频率过高,短时间内多次登录退出;设备环境异常,比如模拟器、多开分身、修改过系统属性;网络环境跳变,比如一会儿用蜂窝一会儿切无线,IP 频繁变化;以及操作节奏过于机械。
对应的调整:把重登间隔拉到小时级,尽量在固定网络环境下运行,加上前面说的随机延迟。我做过一个对比,同一台设备同一个账号,只把点击延迟从固定 0 毫秒改成 300 到 800 毫秒随机,连续运行三天没有出现异常提示;而固定延迟那版第二天就开始掉登录。这个对比不一定普适,但方向是对的——让流程更像人在操作,是所有自动化里成本最低的稳定性投入。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查动作 | 解决方式 |
|---|---|---|---|
| 找不到手机号输入框 | 弹层遮挡或页面未切换 | 截图 + dump 元素树 | 补齐弹层清理逻辑 |
| 获取验证码点不动 | 按钮仍在倒计时 | 读按钮文本看是否含秒数 | 重试间隔设为大于倒计时 |
| 填入验证码后提示错误 | 取到了旧短信 | 对比请求时间与短信时间 | 加 since_ts 过滤 |
| 登录成功但很快掉线 | 操作节奏机械或网络跳变 | 看日志时间间隔 | 加随机延迟,固定网络 |
| 验证码只填进去一位 | 分格输入框未处理 | 查看输入框结构 | 定位隐藏完整输入框 |
| 登录按钮点击无响应 | 协议未勾选 | 检查勾选框状态 | 先判断 checked 再点击 |
| 脚本跑一半连不上设备 | agent 被系统清理 | adb devices看状态 | 关闭电池优化,重启 agent |
| 页面加载超时 | 网络慢或包体大 | 看网络面板加载时序 | 提高等待超时,改用条件等待 |
6. 稳定性与工程化:让这套东西能长期跑
6.1 失败重试的正确姿势
重试这件事,写错比不写更糟。我最早的实现是无脑for i in range(3)包住整个登录流程,结果一次失败会连续发三次验证码,直接把账号送进频控。后来改成分层重试:驱动层的单次操作(点击、输入)可以重试 2 次,间隔 1 秒;状态机的单步(填手机号、填验证码)失败不重试,直接上报;整个登录流程最多重来 1 次,且必须间隔 65 秒以上。
还有一个细节是重试前必须回到已知状态。比如填验证码失败要重试,得先把输入框清空,而不是在原有内容上追加。我写了个reset_step函数专门干这个,每个需要重试的步骤先调一下,把该清的空、该退的页面退回去。这个习惯养成之后,脚本的诡异问题少了一大半。
6.2 日志与现场取证
自动化的调试成本,八成耗在“不知道当时发生了什么”。所以我把日志分成三层:步骤日志记录每个状态迁移和时间戳,操作日志记录每次点击和输入的坐标与结果,异常日志在失败时自动截图并 dump 元素树。
import logging, time, os logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", handlers=[ logging.FileHandler("logs/run.log", encoding="utf-8"), logging.StreamHandler(), ], ) def step(name): logging.info("STEP >>> %s", name) def capture(d, tag): path = f"logs/{tag}_{int(time.time())}.png" d.screenshot(path) logging.error("screenshot saved: %s", path)截图文件我一般保留最近 7 天,超过就清理,不然几天下来几百兆。这个自动清理不是洁癖,是磁盘满了之后脚本莫名崩溃,排查半天才发现是写日志失败。
6.3 我踩过的几个印象深刻的坑
第一个坑是输入法的锅。有一次连续三天登录失败,所有元素定位都对,最后发现是测试设备上装了第三方输入法,set_text走的是输入法通道,而那个输入法在某些页面会弹出候选词浮层,把提交按钮挡住。解决办法有两个:一是把set_text换成直接设置文本属性(UIAutomator2 的set_text底层走的是 accessibility,一般没问题,但配合输入法时会有差异),二是干脆把设备输入法统一设成一个干净的默认输入法。我选了后者,一劳永逸。
第二个坑是时间同步。短信转发服务和我跑脚本的机器不在同一台,两边时钟差了将近两分钟,导致since_ts过滤直接把有效短信过滤掉了。表现是“明明收到了短信,脚本说没收到”。后来我给两边都加了 NTP 同步,并且把since_ts往前提了 5 秒的容差。
第三个坑是并发跑多个任务。我想着不同账号可以并行,结果几个实例同时操作同一台设备,adb 通道互相抢占,频繁出现“设备离线”。结论很明确:一台设备同一时间只能跑一个自动化实例,要并行就得多设备,这是硬约束。
第四个坑跟保活有关。我曾经把保活检查设成每 30 分钟一次,跑了两天一切正常,第三天开始频繁掉登录。查下来是保活请求本身太规律,每次都是整点整分。把间隔改成 30 分钟上下浮动 5 分钟之后,情况明显好转。这件事让我对“规律性”这个词有了新的认识——人类的行为没有绝对规律的,任何精确到秒的周期都值得打散一下。
我现在维护这套东西的整体思路,其实已经从“把每一步做对”转向了“让失败变得便宜”。元素改版了、短信延迟了、会话掉了,这些都会发生,挡不住。能做的是让每次失败都能在几分钟内定位、自动恢复、并把现场留下来。这套东西跑顺之后,日常基本不需要我盯着,偶尔收到一条失败告警,看一眼截图就知道该改哪一行。如果你也在做类似的流程,建议先把状态机、日志和截图这三件事做扎实,再回头优化定位和节奏,收益会大得多。