挂机脚本、辅助工具、传奇类手游的自动化,这几个词凑在一起,几乎每个月都会有人来问我一次。上周又有个做前端的哥们甩了张截图过来,满屏的怪、血条、技能冷却圈,问我能不能照着写一套,让角色在他上班的时候自己升级、自己捡装备。他的诉求特别朴素,朴素到我一眼就能看出他没想过后面会发生什么。这类需求我见得多了,问的人背景也杂,有刚学Python三个月的,也有做了五六年后端的老手,但大家最初的想象都差不多——觉得挂机工具嘛,不就是让程序代替手指点屏幕?真动手之后才会发现,难点从来不在“怎么点”,而在“点什么、什么时候点、点错了怎么办、被封了怎么算”。
这篇东西我不打算给你一段拿来就能跑的成品,那种东西生命周期通常按天算,而且对谁都没好处。我想干的是把这类工具的技术骨架彻底拆开:它到底由哪几块组成,每块用什么方案实现,为什么大多数人的第一版活不过三天,以及同一套技术能力还能挪到什么更长久的地方去。全文偏实战,代码是可运行的通用片段,跑在我自己的测试环境和自建Demo上,你可以直接抄去练手,但请别往别人运营的服务里塞——用户协议里写着呢,账号是你自己的,风险也是。
1. 拆开看,挂机工具其实只有“看、判、动”三步
很多人一上来就想写代码,写了两百行发现跑不通,然后开始怀疑人生。问题往往出在没做拆解。任何一套自动化工具,不管你叫它辅助脚本、挂机工具还是RPA机器人,剥到最里面都只有三个动作在循环:看当前是什么状态,判断该做什么,执行这个动作。人打游戏也是这三步,只是人的“看”和“判”在脑子里完成,快到你意识不到。
1.1 从人的操作习惯反推程序需要的能力
你先回忆一下自己手动刷本的流程。眼睛扫一眼屏幕,确认血条还够、技能好了、面前有怪;大脑判断现在该放技能还是该喝药;手指落在屏幕某个位置。整个过程大概三百毫秒,其中真正花在“看”上的时间极少,因为你的视觉系统会自动过滤掉无关信息,只盯着血条和技能图标。
程序没有这个能力。它看到的是一张两百万像素的完整截图,怪物、地面、UI、飘字、光效全在里面,没有任何东西告诉它“血条在这里”。所以第一件事必须是缩小关注范围——要么靠控件树精确定位,要么靠图像匹配找出目标区域,要么干脆把截图裁成几块分别处理。这一步做不好,后面全是白费力气。
判断环节的难点在于状态是有歧义的。人看到血条剩三分之一会喝药,程序看到的是什么?是一个红色像素占比。你得定义清楚“占比低于多少算危险”,还得考虑不同分辨率下这个占比会不会变。我见过太多脚本卡在这里:写死了一个坐标去点血条区域,换个手机就点到了技能上,角色当场表演原地罚站。
动作环节反而是最简单的,ADB一条命令、控件一个click()就完事。但也别高兴太早,动作有个隐藏要求——幂等性。也就是说,你点了一次没生效,重试一次不能变成点两次。技能连点两下可能进冷却,商店连点两下可能买两份。这个坑我在早期项目里踩过,一晚上刷掉了一堆资源,心疼得很。
1.2 为什么“循环”比“单次点击”难十倍
单次点击是脚本,循环才是工具。这两者的差距不在代码量,在异常处理。
单次点击的假设是:界面在预期位置、网络通畅、没有弹窗、没有断线重连、没有活动公告糊你一脸。循环跑一千次,上面每一条都会至少发生一次。弹窗是最常见的杀手,一个“今日签到”提示框盖住半个屏幕,你的脚本还在傻乎乎地找怪,找了一晚上什么都没找到,日志里干干净净,因为它“没报错”,只是每次匹配都失败而已。
所以一个能长时间跑的循环,必须包含四样东西:超时机制(这一步最多等多久)、重试上限(失败几次就换策略)、兜底动作(实在不行就退回主界面重来)、状态快照(失败时把截图存下来,事后能复盘)。这四样东西加起来,代码量通常是你核心逻辑的三倍,但正是它们决定了你的工具能跑一小时还是能跑一晚上。
经验之谈:我写这类循环有个固定习惯,主循环里任何一步只要超过设定时限没拿到预期状态,立刻截图存盘并打印当前控件树。后面排查问题时,这些快照比重跑十次都管用。
2. 三条技术路线的成本账:截屏识别、控件树、内存与协议
同样是“看”,实现方式差了十万八千里。业内常见的有三条路,成本、稳定性、门槛完全不是一个量级。选错路线,你会在错误的坑里挖很久。
2.1 截屏加图像识别:门槛最低,也最脆
这条路的核心是:定时截一张图,用模板匹配或特征点匹配找出目标图标的位置,然后算出坐标去点。工具链就是 ADB(或系统截图接口)加 OpenCV,Python 环境下十行代码就能跑通第一个 Demo。
它的好处太明显了:与目标应用完全解耦。你不需要知道对方用什么引擎、什么框架,甚至不需要它能被调试。屏幕上有这个图标,我就能找到它。这对学习来说是最友好的入口,因为你可以拿任何一个App练手,不涉及任何侵入性操作。
坏处同样明显。第一,它只认像素不认语义。图标换个皮肤、加个描边、透明度变了,匹配度就从 0.95 掉到 0.6,你的阈值一卡,直接失效。第二,速度受截图限制。一次全屏截图在安卓上通常要 100 到 300 毫秒,加上匹配计算,一个完整循环轻松超过半秒。这在快节奏场景里是致命的——你判断出来该放技能的时候,怪已经被别人打死了。第三,它对动态画面极其敏感。技能特效、飘字、粒子光效全都会干扰匹配结果,你截十张图能匹配上六张就算不错。
我个人的定位很明确:这条路线适合做低频、界面稳定、容错高的自动化,比如每日签到、固定流程的页面巡检。指望它跑高强度战斗,属于用螺丝刀砍树。
2.2 控件树读取:稳,但被大量应用主动封死
安卓的 Accessibility 体系会把界面上的可交互元素组织成一棵树,每个节点带文本、描述、类名、坐标。拿到这棵树,你就不用猜了——直接按文本或ID定位,稳得一批。uiautomator2 这类库就是干这个的,一行d(text="确定").click()比图像匹配优雅太多。
问题在于,这套体系对渲染型界面基本无效。如果一个应用是用游戏引擎或者自绘控件把整个界面画在一张画布上,控件树里往往只有一个巨大的 SurfaceView 节点,什么信息都没有。这不是谁故意为难你,纯粹是渲染方式决定的。另一个现实是,出于安全考虑,很多应用会主动关闭无障碍支持,或者对关键页面做屏蔽处理。你打开控件树一看,空空如也。
所以这条路线的适用面其实比想象中窄:它擅长的是原生控件构成的标准界面,比如系统设置、表单、列表页。这也正是它在自动化测试领域混得风生水起的原因——被测应用就是自己写的,控件树当然是完整的。
2.3 内存与协议层:效率最高,代价也最高
第三条路是直接读进程内存或者重放网络协议。这条路的技术含量最高,效果也最“真”——因为它拿到的不是屏幕上的像素,而是应用内部的状态值,血量、坐标、冷却时间全是精确数字,没有任何识别误差。
但我必须把话说透:这条路的风险等级和前面两条完全不在一个层面。它需要突破进程隔离、需要处理数据加密与校验、需要面对随时可能变化的内部结构,任何一个环节都涉及对他人系统的实质性侵入。在别人的服务上做这件事,轻则账号不保,重则惹上麻烦。而且从纯技术角度看,它的维护成本高得离谱——对方一次版本更新,你的偏移量全部作废,一夜回到解放前。
我把这条路摆出来,是为了让技术版图完整,不是为了推荐。真正值得学的部分在别处:理解协议分层的思路、理解数据校验的意义,这些知识放到接口测试、协议分析、自己的服务端调试里,都是硬通货。
| 路线 | 实现门槛 | 稳定性 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| 截屏 + 图像识别 | 低 | 中低 | 中 | 界面稳定、节奏慢、容错高的流程 |
| 控件树读取 | 中 | 高 | 低 | 原生控件构成的标准界面 |
| 内存/协议层 | 高 | 视实现而定 | 极高 | 仅限自有系统的调试与分析 |
3. 为什么照抄坐标的脚本活不过三天
新手最容易犯的错,是把第一次成功跑通的坐标当成真理写死。tap(540, 1200)在自己手机上跑得好好的,换台设备、换个分辨率、甚至只是系统更新改了状态栏高度,全线崩塌。这不是运气问题,是必然。
3.1 分辨率、DPI与刘海屏的三重错位
安卓设备的屏幕参数差异大得惊人。同样是 1080 宽的屏幕,有的逻辑分辨率是 1080,有的是 720 缩放上来的;状态栏高度从 24dp 到 48dp 不等;还有挖孔屏、曲面屏、手势条,每一种都会让实际可用区域发生偏移。
解决办法只有一个:永远不要用绝对坐标,用相对坐标或者语义定位。具体做法有三种,按推荐程度排序。
第一种是按比例换算。记录坐标时同时记录当时的屏幕宽高,运行时按当前屏幕宽高等比缩放。这个方法简单,但对非等比变化(比如宽高比不同)会失效。
第二种是限定搜索区域(ROI)。不要求全屏匹配,先在固定的相对区域里找。比如技能栏永远在屏幕下方 15% 的范围内,那就只截这一块去匹配。搜索范围小了,误匹配率骤降,速度也快了好几倍。这是性价比最高的一招。
第三种是多尺度匹配。模板图按 0.8 到 1.2 的比例生成一系列缩放版本,逐个匹配取最高分。代价是耗时翻几倍,一般只在对精度要求极高、且设备分辨率不确定的场景下用。
屏幕 1080x2340 → 技能按钮中心 (540, 2100) 换到 720x1560 → 等比缩放 (360, 1400) 换到 1080x2400(长了一截)→ 按宽度缩放得 (540, 2154),纵向偏差 54px最后那 54 像素的偏差,就是很多人脚本“看起来能跑但偶尔点空”的根源。用 ROI 加相对定位能吃掉大部分误差,剩下的靠点击区域放大——按钮中心点不准没关系,只要落点在按钮范围内就行。
3.2 界面的随机化设计与它带来的连锁反应
很多产品会在界面上做位置扰动:同一个按钮,这次在左边,下次在右边;弹窗的出现时机随机;活动入口的位置每天变。这些设计本来是为了运营和防误触,副作用是让基于固定坐标的自动化彻底失效。
应对办法不是硬刚,而是把定位逻辑从“位置”换成“特征”。图像匹配本身就是特征定位,天然免疫位置扰动,只要你匹配的是按钮本身而不是它的坐标。同理,控件树定位也是按文本和属性找,位置变了照样能找到。
真正的麻烦在于目标不存在。你今天要点的按钮,明天可能因为活动下线直接消失了。这时候脚本会一直等,等到超时,然后重试,然后超时,循环往复。所以每个查找动作都必须有“找不到”的分支,而不是死等。我的习惯是:找不到就降级,降级再找不到就退出当前流程回到主界面,主界面认不出来就整段重启。三级兜底,能救回九成的意外。
3.3 行为节律:机器比人“太完美”反而是破绽
这一点纯从技术角度聊聊,因为它解释了为什么很多脚本跑得越久越危险。
人的操作有三个特征:间隔不均匀、位置有抖动、有疲劳和中断。点击间隔服从一个偏态分布,大多数在 200 到 500 毫秒之间,偶尔会有几秒的停顿(喝水、看消息);落点在按钮范围内随机散布,不会每次都精准命中中心;连续操作一两个小时后,节奏会明显变慢。
机器如果按固定间隔、精准坐标、24 小时不间断地跑,这三个特征全都不符合。从数据角度看,这种模式和人类行为的距离非常远,识别起来毫无难度。这也是为什么“加随机延时”“加坐标抖动”成了这类工具的标配——不是为了效果更好,是为了看起来没那么假。
但我得说清楚:这是一场注定不对称的博弈。你加的扰动是有限度的,稍有常识的检测手段都能穿过表层特征去看更深层的东西——设备指纹、操作序列的语义合理性、异常时段的活跃度。你把延时调得再像人,一个连续 72 小时没下线、每秒精确处理一次状态的角色,依然一眼就能被挑出来。所以与其研究怎么更像人,不如早点想明白这条路的天花板在哪。
4. 用公开工具链搭一套能跑的自动化骨架
前面讲的都是判断,这一节讲怎么动手。我用一套完全公开、合法的工具链搭个骨架,跑在安卓模拟器和我自己写的一个 Demo App 上。代码你可以直接复现,跑通之后你会对“看、判、动”有实感,这比看十篇文章都值。
4.1 环境准备:模拟器、ADB与两个Python库
准备四样东西。一台装了安卓模拟器的电脑(自带 ADB 就行),Python 3.9 以上,pip install uiautomator2 opencv-python numpy,以及一个用来练手的目标应用——建议自己写个简单的列表页,或者拿系统设置练手。
模拟器启动后先确认连接:
adb devices # 应该能看到 emulator-5554 device python -m uiautomator2 init # 给模拟器装 atx-agent,成功后 u2.connect() 才能用 python -c "import uiautomator2 as u2; d=u2.connect(); print(d.info)"最后一行能打印出设备信息,说明链路通了。这一步最容易卡住的地方是端口占用和多设备冲突,如果你开了两个模拟器,u2.connect()不带参数会随机连一个;稳妥做法是显式传序列号u2.connect("emulator-5554")。
我选 uiautomator2 而不是 Appium,理由很实际:前者是纯 Python 库,装上就能用,启动开销小,适合写短平快的自动化脚本;Appium 走 WebDriver 协议,功能更全、生态更规范,但启动服务和会话管理对新手来说太重了。做测试用例管理选 Appium,做一次性自动化任务选 uiautomator2,这个取舍我用了几年没变过。
4.2 图像匹配的几个参数,决定成败
OpenCV 的matchTemplate有六种算法,实际用下来TM_CCOEFF_NORMED最稳,因为它做的是归一化相关匹配,对整体亮度变化有一定容忍度,结果落在 0 到 1 之间,阈值好设。
import cv2 import numpy as np def locate(screen_path, template_path, threshold=0.85, roi=None): """在屏幕截图中定位模板,返回中心坐标和匹配分数""" img = cv2.imread(screen_path) tpl = cv2.imread(template_path) if img is None or tpl is None: return None # 限定搜索区域,大幅降低误匹配 if roi is not None: x1, y1, x2, y2 = roi img = img[y1:y2, x1:x2] else: x1 = y1 = 0 res = cv2.matchTemplate(img, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) if max_val < threshold: return None h, w = tpl.shape[:2] cx = x1 + max_loc[0] + w // 2 cy = y1 + max_loc[1] + h // 2 return {"x": cx, "y": cy, "score": round(float(max_val), 3)}三个参数需要你亲自调。
threshold(阈值):0.8 是个起点,不是标准答案。背景干净、图标固定,0.85 到 0.9 都行;背景杂乱、有半透明遮罩,可能得降到 0.75。阈值定太高会漏检,定太低会误检,而误检比漏检可怕得多——漏检只是没动作,误检是点错地方,可能引发一连串连锁反应。我的习惯是从 0.9 往下试,找到第一个能稳定命中的值,再减 0.03 当运行阈值。
模板图的选择:千万别直接截一大块图当模板。模板越小越精确,但太小又容易误匹配。经验尺寸是目标元素的 60% 到 80%,并且只保留特征最明显的部分——比如按钮上的文字或图标,把边框和背景裁掉。模板图最好从多张截图里裁同一块区域,挑差异最小的那张。
ROI 的设置:这个前面提过,但要强调它的收益。全屏 1080x2340 的匹配耗时可能是限定区域的三到五倍。把搜索范围压到目标可能出现的区域内,速度快了,误匹配率也降了,是双赢。
4.3 一个带超时与重试的主循环长什么样
下面这个骨架把“看、判、动”和四件套(超时、重试、兜底、快照)串起来。它不针对任何具体应用,你可以把decide()换成自己的判断逻辑。
import time import os class Runner: def __init__(self, device, shot_dir="./shots"): self.d = device self.shot_dir = shot_dir os.makedirs(shot_dir, exist_ok=True) self.fail_count = 0 def snap(self, tag="now"): path = os.path.join(self.shot_dir, f"{tag}_{int(time.time())}.png") self.d.screenshot(path) return path def safe_click(self, x, y, max_retry=2): """点击带重试,但不盲目连点""" for i in range(max_retry): self.d.click(x, y) time.sleep(0.35) # 给界面反应时间 if self.verify(): # 必须验证结果 return True return False def verify(self): """子类覆写:确认上一步动作是否生效""" return True def run_once(self): try: state = self.observe() # 看 action = self.decide(state) # 判 if action is None: time.sleep(1.0) return ok = self.safe_click(*action) # 动 if not ok: self.fail_count += 1 self.snap(f"fail_{self.fail_count}") if self.fail_count >= 5: self.recover() # 兜底 self.fail_count = 0 except Exception as e: self.snap("exception") print(f"[异常] {e}") def recover(self): """兜底:回到已知的初始状态""" self.d.press("back") time.sleep(0.5) self.d.press("home") time.sleep(0.5)这里有两个设计我坚持了很多年,值得单独说。
点击后必须验证。safe_click里的verify()不是可选项。点击是一个请求,不是结果,界面可能没响应、可能响应延迟、可能被弹窗拦截。少了验证这一步,你的脚本就是在盲操作,错了也不知道。
重试不等于连点。很多人写重试就是for i in range(3): click(),中间不等待。这在游戏里是灾难,三次点击可能触发三个不同的东西。正确做法是每次点击后等待一个合理的反应时间,再检查状态,状态对了就退出重试。
4.4 日志、截图快照与失败回放
这一节讲的东西不带技术含量,但决定了你的调试效率是十分钟还是一整天。
日志不是print("点击成功")。有效的日志至少包含:时间戳、当前动作、耗时、结果、关键参数。格式不用花哨,一行一条,方便 grep:
def log(action, cost, result, extra=""): ts = time.strftime("%H:%M:%S") print(f"{ts} | {action:<16} | {cost*1000:>5.0f}ms | {result:<8} | {extra}")截图快照要分两类,别混在一起。定期快照(比如每三十秒一张)用来观察整体趋势,异常快照用来定位具体问题。后者才是重点,一定要在失败发生的当下截,不能等循环结束再截,那时候界面早变了。我的做法是异常快照文件名里带上失败类型和重试次数,比如fail_5_match_none_1723456789.png,事后扫一眼文件名就知道当时卡在哪个环节。
还有个小技巧:把失败时的控件树也 dump 一份。d.dump_hierarchy()能拿到完整 XML,配合截图一起看,基本能还原现场。这个习惯帮我省掉的排查时间,按小时算都不止。
5. 这套技术真正的用武之地
写到这里,我猜有人要问:那到底能不能做?我的回答是——技术本身是中性的,但落地场景差别巨大。你在这套技术里练出来的能力是真金白银:截图、图像识别、状态机、异常处理、日志体系,这些东西换个场景就是生产力。下面两个方向,是我这些年真正在用的。
5.1 应用的UI回归测试
这是最正统的落点,也是这套技术最舒服的地方。你写的应用,你自己测,控件树完整,界面可控,不需要跟任何检测对抗。
具体怎么落地?把本文第 4 节那套骨架稍微改一下,把“无限循环”换成“用例列表”,就变成了一个可用的冒烟测试框架。
CASES = [ {"name": "启动页加载", "steps": ["等待首页元素出现", "断言标题文本正确"]}, {"name": "列表页滚动", "steps": ["滑动到底部", "断言加载更多按钮出现"]}, {"name": "表单提交", "steps": ["填入三个字段", "点击提交", "断言提示语为成功"]}, ]关键在于断言。自动化测试和自动化操作的区别就在这里:测试必须能判定“对还是错”。元素找不到算失败,文本不匹配算失败,流程走不通算失败。有了断言,它才是一套测试,而不是一个只会点屏幕的机器人。
我维护过的一套用例集大概两百多条,跑一遍十二分钟,接在每次提测前自动触发。它的价值不在于替代人工,而在于把人从重复点击里解放出来,去做真正需要判断力的事情——比如设计边界用例、分析偶发失败的原因。
5.2 重复性人工操作的自动化
第二个方向更贴近个人效率。凡是“每天固定时间、对着固定界面、点固定几个按钮”的活,都可以交给程序。比如每天早上导出某个后台的报表、批量给图片打水印、定时把某些数据从A系统搬到B系统。
这类场景的特点是:频率低、流程固定、出错容忍度高。正好是图像识别路线的舒适区。你甚至不需要写得多精巧,一天跑一次的任务,跑五秒和跑三十秒没区别,只要结果对就行。
我自己的习惯是给这类自动化加一个**“先试运行再执行”**的开关。试运行时只截图、只匹配、只打印将要点击的坐标,不真正点击。确认一遍日志没问题,再切到执行模式。这个习惯救过我不少次——有些后台的按钮位置会因为窗口大小变化而移动,试运行一眼就能发现。
5.3 给自己的技术栈做个取舍建议
如果你刚入门,我的建议顺序是这样的。
先学 uiautomator2 或 Appium,用自己写的 Demo App 练,把它跑通、跑稳。这一步你会学到状态机的设计、超时重试的写法、日志的组织方式,这些是通用能力。
再学 OpenCV 的图像匹配,重点是理解模板选择、阈值调优、ROI 裁剪三件事。这一步不需要多深的数学功底,但需要你亲手调几十次参数,建立手感。
最后再去看协议和内存的东西,而且要带着“理解原理”的目的去看,别急着动手。理解了数据是怎么在客户端和服务端之间流转的、校验是怎么做的,你对整个系统的认知会上一个台阶。这部分知识用到接口调试、性能分析、自己的服务端优化上,价值远超一个挂机脚本。
至于要不要真的做一个跑在别人服务上的工具,这是你的选择,我不替你决定,但事实得摆在桌面上:绝大多数商业运营的手游,用户协议里都明确禁止第三方自动化工具,检测手段在持续升级,收益和代价完全不成比例。我见过太多人花两周写出来的东西,跑了两天,号没了,脚本也没心思维护了。
我自己现在的做法是:这套技术只跑在两种地方——我自己写的应用,和明确允许自动化的内部工具。其他场景,我会直接告诉你,性价比太低。
最后分享一个我觉得最值钱的小习惯。不管你做什么自动化,第一次跑通之后,先别急着优化,把整个流程完整录一遍屏幕,然后一帧一帧看回放。你会看到很多你以为不存在的问题:点了没反应、界面闪了一下、有个提示框一闪而过。这些问题在正常运行时不报错,但会在某个随机的时刻要你的命。录屏十分钟,胜过调试十小时——这话我逢人就讲。