找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 18|回复: 1

屏幕OCR文字识别切换APP窗口

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
屏幕OCR文字识别切换APP窗口

QQ截图20261026083405.png

QQ截图20261026083244.png

QQ截图20261026083232.png

  1. /**
  2. * 案例20:屏幕OCR文字识别切换APP窗口
  3. *
  4. * 经典实用案例"屏幕内容识别/读屏监控",转换成 AIWROK 实现:
  5. *   screen.screenShotFull() 截取整屏 → img.paddleOCR(640,[0,0,1,1]) 离线识别
  6. *   → 识别文字逐条完整打印 → SQLite 入库(ocr_records 表)
  7. *   → 关键词命中打 ⚠(盯"支付/转账"这类敏感界面的雏形)
  8. *
  9. * 与前面案例的本质区别(全新数据源):
  10. *   15-18 拉外部API,19 读设备状态,20 直接**看懂屏幕像素**——
  11. *   不需要接口、不需要对方配合,屏幕上有什么就能采什么,
  12. *   这是"没有API的老APP做数据监控"的唯一路子(网页版股票软件盯盘、群消息统计等)。
  13. *   API 签名来自你工作区生产脚本(冒险岛主脚本/client.js),真机验证过。
  14. *
  15. * 新知识点:
  16. * 1. screenShotFull 截图(首次会弹"屏幕录制"授权,选允许即可);
  17. * 2. paddleOCR 离线识别,不耗流量(实测雷电模拟器上 MLKit 静默返回空,paddle 正常出字);
  18. *    **耗时计时**——你定的硬指标 OCR 必须 5 秒内,脚本每轮打印耗时,超 5 秒自动打 ⚠(达标演示);
  19. * 3. img.recycle() 用完释放截图内存,长期挂机不泄漏(经典踩坑);
  20. * 4. 屏幕去重:文字和上一轮相同=画面没变,只打提示不重复入库,省库省流量。
  21. *
  22. * 教学演示方法(自动切页,无需手动操作):
  23. *   跑起来后每轮自动 app.openApp 打开一个页面(设置→浏览器百度首页→文件管理器,循环),
  24. *   等 4 秒渲染后截屏 OCR——日志文字跟着页面一轮一变,"脚本自己翻页读屏"一目了然。
  25. */

  26. // ========== 配置区(改这里) ==========
  27. var 识别关键词列表 = ["支付", "转账", "验证码"];   // 识别文字命中任一词打 ⚠(敏感界面盯梢雏形)
  28. var OCR间隔秒 = 10;              // 每 10 秒截屏识别一轮(含OCR耗时,演示节奏刚好)
  29. // 自动切换的演示页面(按你的截图:设置/浏览器百度首页/文件管理器,轮着开)
  30. // 包名若不对打不开,日志会打出"当前前台包名",把实际包名填回来即可
  31. var 演示页面列表 = [
  32.     { 名称: "设置",           包名: "com.android.settings" },
  33.     { 名称: "浏览器(百度首页)", 包名: "com.android.browser" },
  34.     { 名称: "文件管理器",       包名: "com.cyanogenmod.filemanager" }   // 实测包名(日志+adb确认),不是小米的 com.android.fileexplorer
  35. ];
  36. var 开页等待秒 = 4;              // openApp 后等页面渲染完再截屏
  37. var OCR超时报警毫秒 = 5000;      // 你的硬指标:识别超 5 秒打 ⚠(性能演示点)
  38. var 打印最大字数 = 300;          // 单轮识别文字打印截断长度(防日志刷屏)
  39. var SQLite路径 = "/sdcard/屏幕文字.db";
  40. var 表名 = "ocr_records";

  41. // ========== 全局变量顶部声明 ==========
  42. var gStop = false;
  43. var 数据库连接 = null;
  44. var 数据库可用 = false;
  45. var 累计入库条数 = 0;
  46. var 累计识别轮数 = 0;
  47. var 上轮文字 = "";               // 屏幕去重基准

  48. // ========== importClass 只能放最外层 ==========
  49. importClass(android.database.sqlite.SQLiteDatabase);
  50. importClass(java.io.File);

  51. // ========== 工具函数(先定义后调用) ==========

  52. // 连接 SQLite(零配置;如需远程库可仿照案例14扩展 mysql 分支)
  53. function dbConnect() {
  54.     try {
  55.         数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
  56.         数据库可用 = true;
  57.         printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
  58.     } catch (e) {
  59.         数据库可用 = false;
  60.         printl("[DB] ⚠ 打开失败(只打印不入库): " + e);
  61.     }
  62. }

  63. // 建表(存在则跳过):text/char_count/matched/collected_at
  64. function dbEnsureTable() {
  65.     if (!数据库可用) return;
  66.     try {
  67.         var sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
  68.             "id INTEGER PRIMARY KEY AUTOINCREMENT," +
  69.             "text TEXT, char_count INTEGER, matched INTEGER, collected_at INTEGER)";
  70.         数据库连接.execSQL(sql);
  71.         printl("[DB] ✅ 表已就绪: " + 表名);
  72.     } catch (e) {
  73.         printl("[DB] ⚠ 建表失败: " + e);
  74.         数据库可用 = false;
  75.     }
  76. }

  77. // 屏幕文字入库(matched=1 表示命中监控词)
  78. function dbInsertText(text, charCount, matched) {
  79.     if (!数据库可用) return false;
  80.     try {
  81.         var stmt = 数据库连接.compileStatement("INSERT INTO " + 表名 +
  82.             " (text, char_count, matched, collected_at) VALUES (?,?,?,?)");
  83.         stmt.bindString(1, "" + text);
  84.         stmt.bindLong(2, charCount);
  85.         stmt.bindLong(3, matched);
  86.         stmt.bindLong(4, time.nowStamp());   // nowStamp=毫秒数字;time.now()是格式化字符串不能bindLong
  87.         stmt.executeInsert();
  88.         stmt.close();
  89.         return true;
  90.     } catch (e) {
  91.         printl("[DB] ❌ 入库失败: " + e);
  92.         return false;
  93.     }
  94. }

  95. function dbClose() {
  96.     if (数据库可用 && 数据库连接 != null) {
  97.         try { 数据库连接.close(); } catch (e) {}
  98.         数据库连接 = null;
  99.     }
  100. }

  101. // 从 ocrResult 提取全部文字(兼容三种返回:JS 数组 / Java String[] / java.lang.String)
  102. // 踩坑实录:getAllString() 在本机返回的是 java.lang.String(不是数组!),typeof 是 "object",
  103. // 它的 .length 是 Java 方法不是数字,必须走 ""+s 强转兜底,否则取值全乱
  104. function 提取OCR文字(ocr) {
  105.     if (ocr == null) return "";
  106.     var s = null;
  107.     try { s = ocr.getAllString(); } catch (e) { printl("⚠ getAllString 异常: " + e); return ""; }
  108.     if (s == null) return "";
  109.     if (typeof s === "string") return s;
  110.     if (s.join) return "" + s.join("\n");                       // JS 数组
  111.     if (typeof s.length === "number" && s[0] != null) {         // Java String[]
  112.         var 行 = [];
  113.         for (var t = 0; t < s.length; t++) 行.push("" + s[t]);
  114.         return 行.join("\n");
  115.     }
  116.     return "" + s;                                              // java.lang.String 兜底
  117. }

  118. // 截屏+OCR 一轮:返回 {文字, 耗时毫秒},失败返回 null
  119. // 引擎用 paddleOCR(640,[0,0,1,1])(SDK 官方示例写法):
  120. // 踩坑实录:雷电模拟器上 MLKitOcr('zhs') 参数正确、截图正常(非黑屏),却静默返回空串,
  121. // 谷歌 MLKit 中文模型在模拟器上缺失,换 paddle 引擎即可正常出字
  122. function 截屏识别() {
  123.     var img = null;
  124.     try {
  125.         printl("📸 正在截取整屏(首次会弹屏幕录制授权)...");
  126.         img = screen.screenShotFull();
  127.         if (!img) {
  128.             printl("[OCR] ❌ 截图返回空(检查屏幕录制权限)");
  129.             return null;
  130.         }
  131.         var 开始 = time.nowStamp();
  132.         // 存调试图:识别异常时拉 /sdcard/debug_ocr.jpg 人工核对画面(本次就是靠它排除黑屏)
  133.         try { img.save("/sdcard/debug_ocr.jpg"); } catch (eSave) { printl("[OCR] ⚠ 存调试图失败: " + eSave); }
  134.         printl("🔍 屏幕已截取(已存 /sdcard/debug_ocr.jpg),开始 paddleOCR 离线识别...");
  135.         var 文字 = 提取OCR文字(img.paddleOCR(640, [0, 0, 1, 1]));
  136.         var 耗时 = time.nowStamp() - 开始;
  137.         printl("🔍 识别完成,耗时 " + 耗时 + " 毫秒,共 " + 文字.length + " 个字符" +
  138.             (文字.length > 0 ? ",前120字: " + 文字.substring(0, 120).replace(/\n/g, " | ") : "(空)"));
  139.         if (耗时 > OCR超时报警毫秒) {
  140.             printl("⚠ 报警:OCR 耗时 " + 耗时 + "ms 超过硬指标 " + OCR超时报警毫秒 + "ms,画面太复杂建议缩小识别区域");
  141.         }
  142.         return { 文字: 文字, 耗时: 耗时 };
  143.     } catch (e) {
  144.         printl("[OCR] ❌ 识别失败: " + e);
  145.         return null;
  146.     } finally {
  147.         // 经典踩坑:截图不释放,长期挂机内存暴涨崩溃
  148.         try { if (img != null) img.recycle(); } catch (e) {}
  149.     }
  150. }

  151. // 关键词检查:命中返回关键词,否则 null
  152. function 查找命中关键词(文字) {
  153.     for (var k = 0; k < 识别关键词列表.length; k++) {
  154.         if (文字.indexOf(识别关键词列表[k]) >= 0) {
  155.             return 识别关键词列表[k];
  156.         }
  157.     }
  158.     return null;
  159. }

  160. // ========== 主流程 ==========
  161. printl("===== 案例20 屏幕OCR文字识别采集入库 启动 =====");

  162. // 步骤0:连库建表
  163. dbConnect();
  164. dbEnsureTable();
  165. printl("⚙ 配置:每 " + OCR间隔秒 + " 秒截屏识别 · 监控词(" + 识别关键词列表.join("/") + ") · OCR超时 " +
  166.     OCR超时报警毫秒 + "ms · 库: " + SQLite路径);
  167. printl("ℹ 全新数据源:不看API不看文件,直接**看懂屏幕像素**——没接口的老APP也能采数据");
  168. printl("ℹ 自动切页演示:每轮 openApp 打开【" + 演示页面列表[0].名称 + "/" + 演示页面列表[1].名称 + "/" + 演示页面列表[2].名称 + "】循环,识别结果一轮一变");
  169. printl("📱 启动时前台包名: " + app.getTopPackName() + "(排查包名用:手动打开某APP再启动脚本,日志第一行就是它的真实包名)");

  170. // 停止按钮清理(顺序:先置标志,stopAllRuning 放最后)
  171. event.onStopBtnEvent(function () {
  172.     gStop = true;
  173.     dbClose();
  174.     runTime.stopAllRuning();
  175. });

  176. // 步骤1:定时循环截屏识别
  177. var 轮次 = 0;
  178. var 页面索引 = 0;
  179. while (!gStop) {
  180.     轮次++;
  181.     累计识别轮数++;
  182.     var 页面 = 演示页面列表[页面索引 % 演示页面列表.length];
  183.     页面索引++;
  184.     printl("\n---- 第 " + 轮次 + " 轮:切换页面【" + 页面.名称 + "】→ 截屏识别 ----");

  185.     // 自动打开目标页面并校验:openApp 后对比前台包名,不对就重试一次(演示时别停在多任务概览页)
  186.     var 打开成功 = false;
  187.     for (var 尝试 = 1; 尝试 <= 2 && !打开成功 && !gStop; 尝试++) {
  188.         try {
  189.             app.openApp(页面.包名);
  190.             printl("🚪 第" + 尝试 + "次打开【" + 页面.名称 + "】(" + 页面.包名 + "),等待 " + 开页等待秒 + " 秒渲染...");
  191.             sleep.second(秒 = 开页等待秒);
  192.             var 前台 = "" + app.getTopPackName();
  193.             if (前台.indexOf(页面.包名) >= 0) {
  194.                 打开成功 = true;
  195.                 printl("📱 校验通过:前台已是 " + 前台);
  196.             } else {
  197.                 printl("⚠ 页面没切过去(前台是 " + 前台 + ")" + (尝试 < 2 ? ",重试一次..." : ",照常截当前屏,若反复失败请把日志里的实际包名填回配置区"));
  198.             }
  199.         } catch (e) {
  200.             printl("🚪 openApp 异常: " + e + ",照常截当前屏");
  201.             break;
  202.         }
  203.     }

  204.     var 结果 = 截屏识别();
  205.     if (结果 != null && 结果.文字.length > 0) {
  206.         // 逐条完整打印采集结果(演示重点:屏幕上写了啥,日志就有啥)
  207.         var 显示文字 = 结果.文字;
  208.         if (显示文字.length > 打印最大字数) {
  209.             显示文字 = 显示文字.substring(0, 打印最大字数) + "...(截断,共" + 结果.文字.length + "字)";
  210.         }
  211.         printl("📋 本轮识别到屏幕文字:");
  212.         printl("   ┌ " + 显示文字.replace(/\n/g, "\n   │ "));
  213.         printl("   └ 共 " + 结果.文字.length + " 字 | 耗时 " + 结果.耗时 + "ms");

  214.         var 命中词 = 查找命中关键词(结果.文字);
  215.         var 命中标记 = 命中词 != null ? 1 : 0;
  216.         if (命中词 != null) {
  217.             printl("⚠ 报警:屏幕内容命中关键词「" + 命中词 + "」!");
  218.         }

  219.         // 屏幕去重:画面没变不重复入库(省库空间,也是监控脚本常用手法)
  220.         if (结果.文字 === 上轮文字) {
  221.             printl("ℹ 屏幕文字与上一轮相同(画面没动),本轮不重复入库");
  222.         } else {
  223.             printl("💾 画面已变化,写入数据库(表: " + 表名 + ")...");
  224.             if (dbInsertText(结果.文字, 结果.文字.length, 命中标记)) {
  225.                 累计入库条数++;
  226.                 printl("   ✓ 已入库 [" + 累计入库条数 + "] " + 结果.文字.length + " 字" +
  227.                     (命中词 != null ? " ⚠命中" : "") + " → " + SQLite路径);
  228.             } else {
  229.                 printl("   ✗ 入库失败");
  230.             }
  231.             上轮文字 = 结果.文字;
  232.         }
  233.     } else if (结果 != null) {
  234.         printl("ℹ 屏幕无可识别文字(黑屏/纯图片页面),本轮跳过");
  235.     } else {
  236.         printl("⚠ 本轮识别失败,跳过入库(" + time.StampToTime(time.nowStamp()) + ")");
  237.     }

  238.     // 分片等待下一轮:每 5 秒打一条进度日志(画面不空,方便观察/录像)
  239.     printl("⏳ 本轮完成," + OCR间隔秒 + " 秒后自动截下一屏");
  240.     var 剩余 = OCR间隔秒;
  241.     while (剩余 > 0 && !gStop) {
  242.         var 步长 = 剩余 >= 5 ? 5 : 剩余;
  243.         printl("⏳ 等待下一轮...剩余 " + 剩余 + " 秒(期间脚本在待机,到点自动截屏→OCR→入库)");
  244.         sleep.second(秒 = 步长);
  245.         剩余 = 剩余 - 步长;
  246.     }
  247. }

  248. dbClose();
  249. printl("===== 流程结束:共 " + 轮次 + " 轮识别,画面变化入库 " + 累计入库条数 + " 条 =====");
复制代码


0

主题

2

回帖

4

积分

软件网站新手

积分
4
发表于 半小时前 | 显示全部楼层

屏幕OCR文字识别切换APP窗口

楼主这个思路可以,元素定位别全页盲扫,先抓整页文本预检命中再精确定位,效率差一个数量级。要判断值不值得投入,就看这个场景一天重复多少次,一天十次的自动化维护成本可能比手工还高。需要强模型就走官方 API,抓网页版那种方式风控随时封,还不可持续。实际操作里最花时间的往往不是写脚本,是把步骤拆到足够细,机器才能无歧义地执行。你这个一天要跑多少次?值得投入这么多精力吗?
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|

GMT+8, 2026-10-27 07:17

安卓脚本软件 · 键鼠盒子 · HID · 机械臂

快速回复 返回顶部 返回列表

随机推荐 · 热帖 / 新帖 / 随便看看

社区会员(随机展示)