☰
离线桌面自动化实战:录制鼠标键盘、图像识别与变量填表落地指南
2026/10/31 1:10:34 网站建设 项目流程

简介:这是一款面向办公人群与自动化入门用户的Windows桌面流程自动化工具,主打零代码、纯本地离线运行,无需联网也无需编程基础。它通过一键录制鼠标点击与键盘输入生成可执行流程,再以可视化拖拽方式调整步骤与参数,并内置图像识别能力,可应对按钮位置不固定的动态界面,配合变量批量填表与JSON配置映射,适合批量录入客户信息、按编号查表填充等重复性办公场景。资源包共156个文件,约99.95MB,以86个dll与58个pyd运行库文件为主,另含2个exe主程序、6个json配置及少量说明与日志文件,解压后即可在本地直接使用。目前已有49人学习下载。对于不想被代码门槛劝退、又希望快速搭建自动化流程的普通用户,这份资源提供了开箱即用的完整工具与配置,可帮助理解录制回放、图像定位与变量填表三类核心能力的实际落地方式。

1. 录制鼠标键盘动作图像识别与变量填表:这套离线桌面自动化到底怎么落地

每天重复几十遍同样的操作——打开内部系统、按固定路径点菜单、把 Excel 里的单号一个个填进表单、截图存档——这种活干久了人都会麻。AutoFlow 这类桌面自动化工具要解决的就是这件事:把你手动操作的鼠标轨迹、键盘输入录下来,回放时靠图像识别找到按钮位置,再把外部数据(比如表格里的变量)填进去,全程不联网、不调云端接口,纯本地跑。它适合两类人:一类是完全不想写代码的业务岗,靠录制就能把流程固化下来;另一类是会点脚本但懒得维护选择器的工程师,用图像识别兜住那些没有稳定控件 ID 的老系统。核心就三件事:录制、识别、填表。下面按这个顺序拆开讲,每一步都给能直接抄的配置和命令。

2. 录制与回放:从鼠标键盘事件到可复用流程

2.1 录制到底录了什么,为什么不能直接拿来用

很多人第一次用录制功能,觉得点一下「开始录制」然后操作一遍就完事了。实际回放十有八九会翻车。原因是录制工具默认记录的是绝对坐标加时间戳,比如「在 (842, 367) 位置按下左键,间隔 320ms 后在 (1024, 210) 输入文本」。这套数据在你自己机器上跑没问题,换台分辨率不同的显示器、或者窗口位置挪了一下,坐标全废。

所以录制阶段真正要做的不是「录完就用」,而是录完之后把绝对坐标转成相对锚点。常见做法是:录制时同时截取每次点击周围的图像区域,回放时先用图像识别在当前屏幕找到这个区域,再算出点击偏移。AutoFlow 这类工具一般会在录制时自动存一份小截图作为锚点,但默认不一定开启,需要手动勾选「记录图像锚点」之类的选项。

我一般会按下面的流程走一遍,确保录制的数据是可迁移的:

# 假设工具提供命令行录制入口(多数桌面自动化工具都有 CLI 模式) # 1. 以图像锚点模式启动录制,指定输出目录 autoflow record --mode image-anchor --output ./flows/login_flow.json --capture-region 200x200 # 2. 录制完成后,检查生成的流程文件里是否包含 anchor 字段 cat ./flows/login_flow.json | python -m json.tool | grep -A3 "anchor" # 3. 如果 anchor 为空,说明录制时没开图像锚点,需要重新录

这里--capture-region 200x200指的是每次点击时截取以点击点为中心的 200×200 像素区域作为锚点图。这个值不能太小,太小了特征不够,识别容易误匹配;也不能太大,太大了识别慢且容易把无关元素框进来。经验值是 150×150 到 250×250 之间,具体看界面元素密度。如果按钮很小、周围留白多,200×200 够用;如果界面很挤,建议缩到 120×120 减少干扰。

录制文件里每个步骤应该包含这几类字段:动作类型(click / input / key)、锚点图路径、相对偏移、输入内容或按键码、等待条件。如果工具生成的流程文件里只有坐标没有锚点,那这套录制数据基本只能在本机用,迁移性为零。

2.2 回放时的等待策略:固定延时为什么必翻车

录制回放第二个大坑是等待。新手最容易犯的错是每个步骤之间设一个固定 sleep,比如 500ms。本地测试跑得挺顺,一到生产环境——系统响应慢一点、弹窗多转了两秒——直接点空。

正确的做法是「条件等待 + 超时兜底」。条件可以是图像出现、窗口标题变化、某个区域颜色变化。AutoFlow 这类工具通常支持在步骤里配置 wait_for 条件。我一般会这样配:

{ "step": 3, "action": "click", "anchor": "./anchors/btn_submit.png", "wait_for": { "type": "image_appear", "target": "./anchors/btn_submit.png", "timeout_ms": 8000, "poll_interval_ms": 200 }, "retry": { "max_attempts": 3, "on_fail": "screenshot_and_abort" } }

timeout_ms给 8000 是留足余量,正常界面 1 秒内就出来了,但遇到系统卡顿给到 8 秒能兜住大部分情况。poll_interval_ms设 200 是因为图像识别本身有耗时,轮询太密没意义还吃 CPU。retry里max_attempts: 3是防止偶发的识别抖动,三次都失败就截图存证然后中止,不要无限重试——无限重试在填表场景里可能导致重复提交,这是生产事故。

提示:等待条件里不要用「固定延时 + 图像识别」混搭。要么纯条件等待,要么在条件等待外面套一层总超时。混搭的结果是延时叠加,一个流程跑下来光等待就耗掉几分钟。

2.3 流程参数化:把硬编码变成可传入变量

录制出来的流程如果里面写死了「输入 A001」这种内容,那只能跑一次。要复用就必须参数化。AutoFlow 一般支持在流程文件里用占位符,回放时从外部传入。常见语法是{{variable_name}}或${variable_name},具体看工具。

我一般会把流程里所有需要变化的值抽出来,放到一个单独的变量文件里:

{ "flow": "order_entry", "variables": { "order_id": "", "customer_name": "", "submit_flag": "true" }, "steps": [ { "action": "input", "anchor": "./anchors/input_order_id.png", "text": "{{order_id}}" }, { "action": "input", "anchor": "./anchors/input_customer.png", "text": "{{customer_name}}" } ] }

回放时通过命令行传入实际值:

autoflow play ./flows/order_entry.json \ --var order_id="SO20250101-001" \ --var customer_name="张三" \ --var submit_flag="true"

这样同一个流程文件可以跑不同数据。变量名建议用下划线命名,和流程文件里的占位符严格对应,大小写敏感。如果工具支持默认值,给每个变量设一个空默认值,这样即使不传也不会报错,只是填入空内容——但填表场景里空内容可能触发校验失败,所以更稳妥的做法是在流程开头加一个「变量非空检查」步骤。

3. 图像识别:模板匹配、特征点与多尺度策略怎么选

3.1 模板匹配的适用边界与参数调优

图像识别在桌面自动化里最常用的方法是模板匹配,说白了就是拿一张小图(锚点图)在大图(当前屏幕截图)里滑窗找最相似的位置。OpenCV 的matchTemplate是标准实现,AutoFlow 这类工具底层多半也是调它。

模板匹配的优点是快、实现简单、对刚性界面效果好。缺点是扛不住缩放、旋转、透明度变化。桌面场景里缩放其实很常见——不同 DPI 设置下同一个按钮大小不一样。所以直接用原始锚点图去匹配,换台机器就可能匹配不上。

我一般会做两件事:一是录制锚点图时统一在 100% 缩放下截取,回放时如果检测到系统 DPI 不是 100%,先把屏幕截图缩放到 100% 再匹配;二是对锚点图做多尺度金字塔,匹配时在几个缩放比例下各跑一遍,取最高分。

import cv2 import numpy as np def multi_scale_match(screen_bgr, template_bgr, scales=(0.8, 0.9, 1.0, 1.1, 1.2), threshold=0.85): """ 多尺度模板匹配 screen_bgr: 当前屏幕截图 (BGR) template_bgr: 锚点模板图 (BGR) scales: 缩放比例列表 threshold: 匹配阈值,低于此值视为未找到 返回: (最佳匹配位置(x,y), 最佳得分, 最佳缩放比例) 或 (None, 0, None) """ best = {"score": -1, "loc": None, "scale": None} screen_gray = cv2.cvtColor(screen_bgr, cv2.COLOR_BGR2GRAY) template_gray = cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY) for s in scales: # 按比例缩放模板 tw = int(template_gray.shape[1] * s) th = int(template_gray.shape[0] * s) if tw < 8 or th < 8: continue resized = cv2.resize(template_gray, (tw, th), interpolation=cv2.INTER_AREA) # 如果缩放后模板比屏幕还大,跳过 if resized.shape[0] > screen_gray.shape[0] or resized.shape[1] > screen_gray.shape[1]: continue result = cv2.matchTemplate(screen_gray, resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) if max_val > best["score"]: best = {"score": max_val, "loc": max_loc, "scale": s} if best["score"] >= threshold: # 返回模板中心点坐标 tw = int(template_gray.shape[1] * best["scale"]) th = int(template_gray.shape[0] * best["scale"]) center = (best["loc"][0] + tw // 2, best["loc"][1] + th // 2) return center, best["score"], best["scale"] return None, best["score"], None

TM_CCOEFF_NORMED是归一化相关系数匹配,得分在 -1 到 1 之间,越接近 1 越相似。阈值设 0.85 是经验值:低于 0.85 误匹配明显增多,高于 0.95 又太严,界面稍有渲染差异就匹配不上。scales列表覆盖 ±20% 的缩放范围,步长 0.1。如果目标机器 DPI 差异大,可以把范围拉到 0.6 到 1.4,但计算量会上去。实际跑的时候,如果屏幕分辨率是 1920×1080,模板 200×200,单尺度匹配大概 20-50ms,五个尺度就是 100-250ms。这个耗时在自动化流程里可以接受,但如果一个流程有几十步,累积起来就明显了。优化方向是先用低分辨率快速筛一遍,只在候选区域做精细匹配。

3.2 特征点匹配什么时候比模板匹配更靠谱

模板匹配搞不定的场景,比如按钮有轻微形变、背景半透明、或者界面主题换了颜色,就得换特征点匹配。ORB 或 SIFT 提取关键点和描述子,然后做匹配。优点是抗形变、抗旋转、抗亮度变化。缺点是慢,而且对纹理少的区域(比如纯色按钮)提取不到足够特征点。

我一般只在模板匹配连续失败两次之后才切到特征点匹配,作为兜底。因为特征点匹配的误匹配率在桌面场景里其实不低——界面里重复元素多(比如一排同样的图标),特征点很容易匹配到错误的位置。

import cv2 def feature_match(screen_bgr, template_bgr, min_good=10, ratio=0.75): """ ORB 特征点匹配 ratio: Lowe's ratio test 阈值 min_good: 最少有效匹配点数,低于此值认为匹配失败 """ orb = cv2.ORB_create(nfeatures=500) kp1, des1 = orb.detectAndCompute(template_bgr, None) kp2, des2 = orb.detectAndCompute(screen_bgr, None) if des1 is None or des2 is None or len(kp1) < min_good or len(kp2) < min_good: return None, 0 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) matches = bf.knnMatch(des1, des2, k=2) good = [] for m, n in matches: if m.distance < ratio * n.distance: good.append(m) if len(good) < min_good: return None, len(good) # 用匹配点估算模板在屏幕中的位置 src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) M, mask = cv2.estimateAffinePartial2D(src_pts, dst_pts, method=cv2.RANSAC) if M is None: return None, len(good) # 模板中心点变换到屏幕坐标 h, w = template_bgr.shape[:2] center = np.float32([[w / 2, h / 2]]).reshape(-1, 1, 2) transformed = cv2.transform(center, M) return (int(transformed[0][0][0]), int(transformed[0][0][1])), len(good)

nfeatures=500是控制提取特征点数量上限,桌面小图 500 够用,多了反而慢。ratio=0.75是 Lowe's ratio test 的经典值,小于这个比例才认为是好匹配。min_good=10是最少好匹配点数,低于 10 个基本不可信。estimateAffinePartial2D用 RANSAC 剔除误匹配,比直接算平均偏移稳得多。

实际用的时候,特征点匹配的耗时通常是模板匹配的 3-5 倍。所以策略是:默认走模板匹配,连续失败两次后切特征点匹配,特征点也失败就截图报错。不要一上来就用特征点,没必要。

3.3 识别失败时的排查顺序

识别失败是最高频的问题。我一般按这个顺序排查:

第一,看截图。工具一般会在失败时存一张当前屏幕截图,先肉眼确认目标元素在不在屏幕上。不在的话,是流程逻辑问题,不是识别问题。

第二,看锚点图。把锚点图和当前屏幕截图放一起对比,看颜色、大小、周围环境有没有变化。常见的是系统主题从浅色变深色,锚点图还是浅色背景,匹配得分直接掉到 0.5 以下。

第三,看匹配得分。如果得分在 0.7-0.85 之间,说明找到了但不够确信。这时候可以临时把阈值降到 0.75 看能不能匹配上,如果能,说明是阈值太严,需要重新截锚点图或者调整阈值。

第四,看多尺度。如果目标元素大小变了,单尺度匹配肯定失败。确认 scales 列表覆盖了实际缩放比例。

第五,看屏幕缩放。Windows 的显示缩放设置(125%、150%)会直接影响截图尺寸。如果工具没有自动处理 DPI,需要手动在流程开头加一步「设置进程 DPI 感知」。

注意:排查时不要同时改多个参数。一次只改一个,改完跑一遍看效果。同时改阈值、改 scales、改锚点图,最后好了也不知道是哪个起的作用,下次遇到同样问题还是不会调。

4. 变量填表:从数据源到表单的完整链路

4.1 数据源接入:Excel、CSV 与数据库的读取方式

变量填表的第一步是拿到数据。最常见的来源是 Excel 和 CSV。AutoFlow 这类工具一般支持直接读文件,但格式要求比较严格。我一般会先把数据整理成标准 CSV,避免 Excel 的合并单元格、公式、格式问题。

import csv def load_rows(csv_path, encoding="utf-8-sig"): """ 读取 CSV 数据源 encoding 用 utf-8-sig 是为了兼容 Excel 导出的带 BOM 的 CSV 返回: list of dict """ rows = [] with open(csv_path, "r", encoding=encoding, newline="") as f: reader = csv.DictReader(f) for row in reader: # 去掉首尾空白,空字符串转 None cleaned = {k.strip(): (v.strip() if v and v.strip() else None) for k, v in row.items()} rows.append(cleaned) return rows # 使用示例 data = load_rows("./data/orders.csv") for i, row in enumerate(data): print(f"第 {i+1} 行: order_id={row.get('order_id')}, name={row.get('customer_name')}")

utf-8-sig这个编码很关键。Excel 另存为 CSV 默认带 BOM,用普通utf-8读第一列列名会多一个不可见字符,导致row['order_id']取不到值。这个坑我踩过不止一次,血泪经验就是:只要数据源可能来自 Excel,一律用utf-8-sig。

如果数据在数据库里,一般先导出成 CSV 再走上面的流程。直接在自动化工具里连数据库不是不行,但增加了依赖和故障点,而且数据库连接信息管理也麻烦。导出成文件再读,简单可靠。

4.2 填表时的输入模拟:键盘事件与剪贴板粘贴的取舍

填表有两种方式:模拟键盘逐字输入,或者把内容写到剪贴板再 Ctrl+V 粘贴。两种方式各有适用场景。

逐字输入的好处是兼容性好,几乎所有输入框都认。坏处是慢,一个 20 位的单号要模拟 20 次按键,每次按键间隔就算 30ms,也要 600ms。而且有些输入框有输入法干扰,模拟按键可能触发中文输入法候选框。

剪贴板粘贴的好处是快,一次复制一次粘贴就完事。坏处是有些输入框禁用了粘贴(比如密码框、某些安全控件),而且剪贴板是全局资源,多线程跑的时候会互相覆盖。

我一般默认用剪贴板粘贴,遇到粘贴无效的输入框再切逐字输入。切换逻辑可以做成配置:

{ "action": "input", "anchor": "./anchors/input_order_id.png", "text": "{{order_id}}", "method": "clipboard", "fallback_method": "keystroke", "keystroke_interval_ms": 30 }

method: clipboard是首选,fallback_method: keystroke是粘贴失败后自动切逐字输入。keystroke_interval_ms: 30是逐字输入的按键间隔,30ms 是经验值——太快了有些老系统反应不过来会丢字符,太慢了流程耗时太长。如果目标系统特别老,可以调到 50ms。

剪贴板方式在实现上要注意:粘贴前先清空剪贴板,粘贴后等一小段时间再继续,确保粘贴完成。有些工具粘贴后立即执行下一步,结果输入框还没收到内容就往下走了。

4.3 填表后的校验:怎么确认数据真的填进去了

填完不校验,等于没填。我见过太多流程跑完显示成功,实际表单里是空的或者填错了。校验方式有三种:

第一种,填完后截图,用图像识别确认输入框区域显示了预期文本。这种方式最直观,但需要为每个输入框准备一张「预期结果」的锚点图,维护成本高。

第二种,填完后用 OCR 读取输入框区域,和预期值比对。这种方式灵活,不需要预存锚点图,但 OCR 本身有误差,数字和字母容易混。

第三种,填完后触发一个「读取」动作,如果目标系统支持从输入框读值(比如通过无障碍接口),直接读出来比对。这种方式最准,但依赖系统支持。

我一般用第二种加第三种组合:优先走无障碍接口读值,读不到就 OCR。OCR 用轻量级的本地引擎,不联网。比对时做归一化处理——去掉空格、统一大小写、数字和字母的常见混淆(0/O、1/l)做映射。

def normalize_text(s): """归一化文本用于比对""" if s is None: return "" s = s.strip().lower() # 常见 OCR 混淆映射 trans = str.maketrans({"o": "0", "l": "1", "i": "1", "|": "1"}) # 注意:这个映射只用于比对,不用于实际填值 return s.translate(trans).replace(" ", "") def verify(expected, actual, strict=False): """ 校验填值结果 strict=True 时要求完全一致,False 时允许 OCR 混淆 """ if strict: return expected == actual return normalize_text(expected) == normalize_text(actual)

这个normalize_text只在比对时用,不要拿归一化后的值去填表。填表必须用原始值。校验失败时的处理策略:重试填值一次,再失败就截图存证并跳过该行,继续下一行。不要因为一行失败就中止整个流程——批量填表场景里,一行失败中止意味着后面的全没跑,损失更大。

5. 避坑与排查:离线桌面自动化的五个真实翻车现场

5.1 屏幕缩放导致识别全挂

现象:在自己电脑上跑得好好的流程,换到同事电脑上每一步都识别失败,截图看目标元素明明在屏幕上。

原因:同事电脑的 Windows 显示缩放是 150%,截图出来的像素尺寸和你 100% 缩放时不一样。锚点图是按 100% 截的,直接拿去匹配 150% 的屏幕,尺寸对不上,模板匹配得分极低。

解决:在流程启动时检测系统 DPI 缩放比例,把屏幕截图缩放到 100% 再匹配。或者在录制锚点图时就在目标机器上录,保证锚点图和运行环境一致。更彻底的做法是用多尺度匹配,让 scales 覆盖 0.6 到 1.6 的范围,但计算量会明显增加。

5.2 输入法状态导致文本填错

现象:填英文单号时正常,填中文客户名时输入框里出现的是拼音字母,或者候选框挡住了后续操作。

原因:模拟键盘输入时,系统当前输入法处于中文状态,按键被输入法拦截。剪贴板粘贴方式一般不受影响,但逐字输入方式必翻车。

解决:填值前先发一个切换输入法的快捷键(通常是 Shift 或 Ctrl+Space),确保切到英文状态。或者统一用剪贴板粘贴方式,绕开输入法。如果必须逐字输入中文,用 Unicode 编码方式发送按键,不经过输入法。

5.3 弹窗遮挡导致点击偏移

现象:流程跑到某一步突然点到了错误的位置,或者点击无效。

原因:目标元素上方弹出了一个提示框、广告窗或者系统通知,图像识别在弹窗里找到了相似的图案,误匹配了。

解决:在关键步骤前加一个「弹窗检测」步骤,识别常见弹窗的关闭按钮并点击关闭。或者在匹配时限定搜索区域,只在目标窗口的客户区内匹配,排除弹窗区域。更稳妥的做法是每一步点击前先确认目标窗口是激活状态,不是激活状态就先激活。

5.4 流程中断后状态残留

现象:流程跑到一半因为某步失败中止了,重新跑的时候发现表单里已经有半截数据,或者系统提示「重复提交」。

原因:中止时没有做清理,已经填了一半的表单没关,或者已经提交了一次但流程没记录。

解决:流程设计时加一个「异常清理」分支,中止前先截图、关闭当前表单、记录已处理的行号。重新跑的时候从记录的行号继续,不要从头跑。如果系统有重复提交校验,清理时要把已提交的记录标记出来。

5.5 离线环境缺少依赖

现象:在开发机上跑得好好的,拷到生产机上报错,提示缺少某个 DLL 或运行库。

原因:开发机上装了完整的运行环境,生产机是干净的离线环境,缺少图像处理库、OCR 引擎或者 .NET 运行库。

解决:打包时把所有依赖一起带上,做成绿色版。图像处理库一般有静态编译版本,OCR 引擎选轻量级的本地版本。部署前在一台干净机器上完整跑一遍,确认没有遗漏。离线环境最怕的就是「在我机器上能跑」,所以测试环境一定要和产线环境一致。

6. 进阶技巧:用录制片段拼装复杂流程与识别结果缓存

录制的流程一长,维护就头疼。我的做法是把流程拆成片段,每个片段只做一件事——登录、打开菜单、填一行、提交——然后按需拼装。AutoFlow 这类工具一般支持流程引用,一个主流程里调多个子流程。

{ "flow": "batch_entry", "steps": [ {"call": "./fragments/login.json"}, {"call": "./fragments/open_order_page.json"}, { "loop": { "source": "./data/orders.csv", "steps": [ {"call": "./fragments/fill_one_order.json", "vars": {"order_id": "{{order_id}}", "name": "{{customer_name}}"}}, {"call": "./fragments/submit_and_verify.json"} ] } }, {"call": "./fragments/logout.json"} ] }

片段化的好处是:登录逻辑改了只改登录片段,填表逻辑改了只改填表片段,互不影响。而且片段可以单独测试,不用每次跑全流程。

另一个技巧是识别结果缓存。同一个界面元素在流程里可能被识别多次,比如「提交」按钮在每一行填完后都要找一次。如果界面没变,没必要每次都重新匹配。可以在第一次识别成功后,把位置缓存起来,后续步骤直接复用,只在点击失败时才重新识别。

class AnchorCache: def __init__(self, ttl_frames=10): self.cache = {} self.ttl = ttl_frames self.counter = 0 def get(self, anchor_path): """获取缓存的锚点位置,过期返回 None""" entry = self.cache.get(anchor_path) if entry and (self.counter - entry["frame"]) < self.ttl: return entry["pos"] return None def put(self, anchor_path, pos): self.cache[anchor_path] = {"pos": pos, "frame": self.counter} def tick(self): """每执行一步调用一次,推进计数器""" self.counter += 1

ttl_frames=10表示缓存最多存活 10 步。超过 10 步没用到就失效,重新识别。这个值不能太大——界面可能在这期间变了,缓存的位置就错了。也不能太小——太小了缓存没意义。10 步是个折中。实际用的时候,如果流程里界面切换频繁,调到 5;如果界面很稳定,调到 20 也行。

缓存命中时直接点击缓存位置,但点击后要校验结果(比如截图确认点击生效了)。如果点击无效,清掉缓存重新识别。这样既省了识别时间,又不会因为缓存错误导致流程跑偏。

最后说一个我自己的习惯:每做一个新流程,先手动跑三遍,把每步的耗时和识别得分记下来。得分低于 0.9 的步骤重点标记,这些就是后续最容易翻车的地方。流程上线后,每次失败都截图存档,攒够一批就看规律——是某类界面元素识别不稳,还是某个环节等待不够。自动化流程的稳定性不是一次调出来的,是跑出来、看出来、改出来的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询