从 Windows GUI 自动化到 Android 自动化:一套双端巡检脚本的重构过程

从 Windows GUI 自动化到 Android 自动化:一套双端巡检脚本的重构过程

做自动化时,最常见的一种情况是:最初只有一个平台,脚本也围绕这个平台写得很顺;但一旦需求扩到第二个平台,原来的结构就会立刻暴露出边界。

我最近做的这个小项目就是这样。最开始它只是一个 Windows 桌面端的自动化巡检脚本,用来读取某个目标卡片上的状态信息,保存截图、记录 CSV,并在需要时生成报表。后来同样的业务流程又需要覆盖 Android 平板端,而且两个端的功能基本一致。问题也随之出现:Windows 端原本依赖的桌面 UI 自动化能力,显然不能直接照搬到 Android 上。

这篇文章记录的,就是这个项目从"单端 Windows 脚本"演进成"Windows / Android 双端自动化"的过程,以及中间几个关键的技术决策。

一、项目最初的形态

项目一开始很简单:Windows 端启动桌面客户端,连接窗口,处理登录,找到目标卡片,读取卡片中的业务文本,解析状态并保存截图和记录。

这条链路在 Windows 上完全成立,因为桌面应用的窗口和控件是可以通过 UI 自动化框架直接访问的。对应到代码里,Windows 端核心逻辑放在 app/gui_app.py,使用的是 pywinauto

python 复制代码
from pywinauto import Application

启动程序和连接窗口也是标准的 pywinauto 用法:

python 复制代码
Application(backend="uia").start(EXE_PATH)
Application(backend="uia").connect(...)

这部分在单平台阶段运行得很稳定,问题出在需求扩展之后。

二、为什么不能直接把 Windows 方案搬到 Android

一开始最直观的想法,其实是借助投屏工具把 Android 平板画面投到 Windows 上,再沿用现有的 Windows 自动化方式去操作这个投屏窗口。

这个思路看起来省事,但很快就会遇到问题:你操作到的其实只是一个"视频窗口",不是 Android App 自身的控件树。也就是说:

  • 你看到的是画面
  • 自动化框架看到的是一个桌面窗口
  • 并不能真正访问 Android 应用内部的控件信息

这种方式短期做 POC 可以,做长期可维护的自动化会非常脆弱。窗口大小、分辨率、缩放、黑边、坐标偏移,都会让脚本变得不稳定。

所以项目后面很快就放弃了这条路线,转而采用真正面向 Android UI 层的自动化方案。

三、Android 端的技术选择:Appium + UiAutomator2

Android 端最后采用的是:

  • Appium
  • UiAutomator2

对应代码在 app/android_app.py

python 复制代码
from appium import webdriver
from appium.options.android import UiAutomator2Options

配置里明确指定了 Android 平台和驱动类型:

python 复制代码
options.platform_name = "Android"
options.automation_name = "UiAutomator2"

最终通过 webdriver.Remote(...) 建立与设备的会话:

python 复制代码
webdriver.Remote(server_url, options=options)

这一点很重要。Windows 端和 Android 端最终采用的是两套完全不同的"驱动层":

  • Windows 端:pywinauto
  • Android 端:Appium + UiAutomator2

而项目后面的重构核心,也正是围绕"驱动层分开,业务逻辑复用"展开的。

四、这个项目为什么没有用 OCR 当主方案

在做这类跨平台自动化时,很多人第一反应会想到 OCR。尤其当 Android 端是 Flutter 页面或者自绘页面时,OCR 看起来很像一种"通用解法"。

但在这个项目里,OCR 最终没有成为主方案。

原因很简单:OCR 更适合作为兜底,不适合作为主链路。只要还能拿到控件树、页面源、元素属性,就应该优先走 UI 自动化,而不是图像识别。

项目里最后真正跑通的方式,不是 OCR,而是直接读取页面元素属性。也正因为如此,公共解析逻辑最终可以沉淀为平台无关模块,而不依赖图像识别结果。

五、Android 端真正的突破口:content-desc

Android 端打通的过程并不是一帆风顺的。最开始 Appium 能连上设备,也能拿到 page_source,但按可见文字定位目标元素时,结果却一直是 0。

后来做了一步非常关键的检查:把页面源里前几十个节点的关键属性打出来,看看到底暴露了什么。

结果发现,这其实是一个典型的 Flutter 页面表现:

  • 许多节点没有 text
  • 也没有明显的 resource-id
  • 但关键内容暴露在 content-desc

这一步改变了后面整个 Android 端的定位策略。

原来按 text 查找元素不通,不代表页面不可自动化;很可能只是页面没有把文字暴露到 text 字段,而是放到了 content-desc。一旦确认这一点,就可以直接按 content-desc 定位目标卡片,并从中提取完整文本内容。

也正因为这个发现,Android 端后来能够稳定识别不同状态,例如:

  • 某张目标卡片存在,但状态为"无值"
  • 某张目标卡片存在,但状态为"未连接"

这些都不是 OCR 识别出来的,而是从 UI 元素属性中直接读取的。

六、重构的核心:执行层分端,业务层共用

在明确了 Windows 和 Android 需要两套不同驱动之后,项目的重构方向就很清晰了:

  • 执行层分端
  • 业务层共用

这里的"执行层"是平台相关部分,例如:

  • 启动应用
  • 连接窗口或设备
  • 找目标卡片
  • 截图

这里的"业务层"则是平台无关部分,例如:

  • 解析卡片文本
  • 判断状态
  • 构造记录
  • 生成报表

项目最后收敛成的核心模块包括:

  • app/gui_app.py
    Windows 平台操作
  • app/android_app.py
    Android 平台操作
  • app/parser.py
    共享卡片文本解析
  • app/recorder.py
    CSV 写入
  • app/report_generator.py
    Excel 报表
  • app/run_utils.py
    统一构造记录数据

这样一来,平台差异就只剩下"怎么拿到原始文本"和"怎么截图",而状态判断、记录输出、报表生成都能共享。

七、单次执行和定时执行都做成了分端入口

为了让这套结构真正可用,项目没有只停留在"抽模块",而是进一步把入口脚本也整理成了分端形式。

单次执行入口:

  • test/windows_poc.py
  • test/android_poc.py

定时巡检入口:

  • main.py
    Windows 定时执行
  • android_main.py
    Android 定时执行

这样做的好处是非常直接的:

  • 单次验证和定时巡检职责清晰
  • Windows 和 Android 可以分别调试
  • 两端出问题时更容易定位
  • 后面做平台参数化也更自然

八、统一入口是如何补上的

有了分端入口之后,日常使用已经可行了,但从体验上说,记住多个脚本路径依然不够方便。

所以后面又补了一个统一入口 runner.py,支持两个维度:

  • platform
  • mode

例如:

powershell 复制代码
python .\runner.py --platform windows --mode once
python .\runner.py --platform android --mode once
python .\runner.py --platform windows --mode scheduled
python .\runner.py --platform android --mode scheduled

这个统一入口一开始也踩了一个很典型的坑:子脚本自己有 argparse,而统一入口导入它们之后,子脚本又会继续读取 sys.argv,导致 runner.py 的参数被误当成子脚本参数。

后面通过在调用子入口前临时清理 sys.argv,才把这个问题解决掉。这类问题很小,但也说明一个事实:统一入口的价值不只是"少敲几个命令",它实际上是把原本散落的脚本收成了一个更清晰的调度层。

九、Windows 端和 Android 端分别踩过的坑

这个项目里,两个平台各自都有几个典型问题。

Windows 端的主要问题是窗口连接时机。最初直接拿 top_window(),在程序刚启动但界面还没准备好时,很容易报"找不到窗口"。后面改成:

  • 按标题正则连接
  • 重试等待
  • 要求窗口 exists visible ready

连接稳定性就提升了很多。

另一个优化点是截图范围。最初 Windows 只截目标卡片,Android 则是整页截图,两端不一致。后来 Windows 也统一改成截整个应用窗口,这样后续分析和对比都更自然。

Android 端最大的问题则是冷启动配置。最开始 app_activity 写成了 MainActivity,但真正冷启动时会报:

  • Activity class ... does not exist

后来通过 adb 查出真实启动页,才确认正确写法应该是:

python 复制代码
ANDROID_APP_ACTIVITY = ".MainActivity"

这个问题非常有代表性:当 App 已经在前台时,错误配置可能完全被掩盖;只有真正被杀死后重新启动,问题才会暴露。

十、这个项目最值得总结的经验

第一,平台驱动一定要和业务逻辑分开。

一个脚本在单平台时看起来怎么写都行,但一旦扩展到第二个平台,边界立刻会变得很重要。

第二,不要把 OCR 当默认答案。

能走 UI 自动化,就优先走 UI 自动化。OCR 更适合兜底,而不是主链路。

第三,Flutter 页面不要过早下结论说"不可自动化"。

先检查它暴露了什么属性,特别是 content-desc。很多时候突破口就在这里。

第四,统一入口不是可有可无。

当脚本数量开始变多时,一个调度层会极大改善使用体验。

第五,冷启动一定要测。

很多 Android 配置问题,只有在 App 被杀掉之后才会显现出来。

相关推荐
15&30游山_玩水69☆156♀252 小时前
高性能自动化核心配件选型:西门子 S7-1500 CPU 与博世力士乐 IndraDrive M 伺服驱动器详解
运维·自动化
Full Stack Developme2 小时前
Hutool XML 操作教程
xml·windows·python
码农小旋风2 小时前
2026 最新 Claude Code Windows 安装教程:Node、Git Bash、命令检查一步步配好
windows·git·bash·claude
BY组态2 小时前
《工业4.0时代的智能组态解决方案:打造高效自动化控制系统》
运维·信息可视化·自动化
embrace_the_sunhaha2 小时前
MATLAB->WinC-UnbuntuC
windows·硬件工程
winfredzhang2 小时前
Android中安装模拟器失败,清理安装失败的模拟器
android·清理·模拟器
ganshenml2 小时前
Android PopupWindow 在老年模式下定位偏移问题分析与解决(showAtLocation / 字体缩放 / 抖动)
android
石榴树下的七彩鱼2 小时前
电商订单 OCR 识别实战:如何自动提取订单信息并实现发货自动化(附 Python / Java 示例)
人工智能·python·自动化·ocr·电商·电商自动化·api 接入
七夜zippoe2 小时前
OpenClaw 浏览器自动化实战
运维·chrome·自动化·浏览器·playwright·openclaw