|
|
屏幕OCR文字识别切换APP窗口
- /**
- * 案例20:屏幕OCR文字识别切换APP窗口
- *
- * 经典实用案例"屏幕内容识别/读屏监控",转换成 AIWROK 实现:
- * screen.screenShotFull() 截取整屏 → img.paddleOCR(640,[0,0,1,1]) 离线识别
- * → 识别文字逐条完整打印 → SQLite 入库(ocr_records 表)
- * → 关键词命中打 ⚠(盯"支付/转账"这类敏感界面的雏形)
- *
- * 与前面案例的本质区别(全新数据源):
- * 15-18 拉外部API,19 读设备状态,20 直接**看懂屏幕像素**——
- * 不需要接口、不需要对方配合,屏幕上有什么就能采什么,
- * 这是"没有API的老APP做数据监控"的唯一路子(网页版股票软件盯盘、群消息统计等)。
- * API 签名来自你工作区生产脚本(冒险岛主脚本/client.js),真机验证过。
- *
- * 新知识点:
- * 1. screenShotFull 截图(首次会弹"屏幕录制"授权,选允许即可);
- * 2. paddleOCR 离线识别,不耗流量(实测雷电模拟器上 MLKit 静默返回空,paddle 正常出字);
- * **耗时计时**——你定的硬指标 OCR 必须 5 秒内,脚本每轮打印耗时,超 5 秒自动打 ⚠(达标演示);
- * 3. img.recycle() 用完释放截图内存,长期挂机不泄漏(经典踩坑);
- * 4. 屏幕去重:文字和上一轮相同=画面没变,只打提示不重复入库,省库省流量。
- *
- * 教学演示方法(自动切页,无需手动操作):
- * 跑起来后每轮自动 app.openApp 打开一个页面(设置→浏览器百度首页→文件管理器,循环),
- * 等 4 秒渲染后截屏 OCR——日志文字跟着页面一轮一变,"脚本自己翻页读屏"一目了然。
- */
- // ========== 配置区(改这里) ==========
- var 识别关键词列表 = ["支付", "转账", "验证码"]; // 识别文字命中任一词打 ⚠(敏感界面盯梢雏形)
- var OCR间隔秒 = 10; // 每 10 秒截屏识别一轮(含OCR耗时,演示节奏刚好)
- // 自动切换的演示页面(按你的截图:设置/浏览器百度首页/文件管理器,轮着开)
- // 包名若不对打不开,日志会打出"当前前台包名",把实际包名填回来即可
- var 演示页面列表 = [
- { 名称: "设置", 包名: "com.android.settings" },
- { 名称: "浏览器(百度首页)", 包名: "com.android.browser" },
- { 名称: "文件管理器", 包名: "com.cyanogenmod.filemanager" } // 实测包名(日志+adb确认),不是小米的 com.android.fileexplorer
- ];
- var 开页等待秒 = 4; // openApp 后等页面渲染完再截屏
- var OCR超时报警毫秒 = 5000; // 你的硬指标:识别超 5 秒打 ⚠(性能演示点)
- var 打印最大字数 = 300; // 单轮识别文字打印截断长度(防日志刷屏)
- var SQLite路径 = "/sdcard/屏幕文字.db";
- var 表名 = "ocr_records";
- // ========== 全局变量顶部声明 ==========
- var gStop = false;
- var 数据库连接 = null;
- var 数据库可用 = false;
- var 累计入库条数 = 0;
- var 累计识别轮数 = 0;
- var 上轮文字 = ""; // 屏幕去重基准
- // ========== importClass 只能放最外层 ==========
- importClass(android.database.sqlite.SQLiteDatabase);
- importClass(java.io.File);
- // ========== 工具函数(先定义后调用) ==========
- // 连接 SQLite(零配置;如需远程库可仿照案例14扩展 mysql 分支)
- function dbConnect() {
- try {
- 数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
- 数据库可用 = true;
- printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
- } catch (e) {
- 数据库可用 = false;
- printl("[DB] ⚠ 打开失败(只打印不入库): " + e);
- }
- }
- // 建表(存在则跳过):text/char_count/matched/collected_at
- function dbEnsureTable() {
- if (!数据库可用) return;
- try {
- var sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
- "id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "text TEXT, char_count INTEGER, matched INTEGER, collected_at INTEGER)";
- 数据库连接.execSQL(sql);
- printl("[DB] ✅ 表已就绪: " + 表名);
- } catch (e) {
- printl("[DB] ⚠ 建表失败: " + e);
- 数据库可用 = false;
- }
- }
- // 屏幕文字入库(matched=1 表示命中监控词)
- function dbInsertText(text, charCount, matched) {
- if (!数据库可用) return false;
- try {
- var stmt = 数据库连接.compileStatement("INSERT INTO " + 表名 +
- " (text, char_count, matched, collected_at) VALUES (?,?,?,?)");
- stmt.bindString(1, "" + text);
- stmt.bindLong(2, charCount);
- stmt.bindLong(3, matched);
- stmt.bindLong(4, time.nowStamp()); // nowStamp=毫秒数字;time.now()是格式化字符串不能bindLong
- stmt.executeInsert();
- stmt.close();
- return true;
- } catch (e) {
- printl("[DB] ❌ 入库失败: " + e);
- return false;
- }
- }
- function dbClose() {
- if (数据库可用 && 数据库连接 != null) {
- try { 数据库连接.close(); } catch (e) {}
- 数据库连接 = null;
- }
- }
- // 从 ocrResult 提取全部文字(兼容三种返回:JS 数组 / Java String[] / java.lang.String)
- // 踩坑实录:getAllString() 在本机返回的是 java.lang.String(不是数组!),typeof 是 "object",
- // 它的 .length 是 Java 方法不是数字,必须走 ""+s 强转兜底,否则取值全乱
- function 提取OCR文字(ocr) {
- if (ocr == null) return "";
- var s = null;
- try { s = ocr.getAllString(); } catch (e) { printl("⚠ getAllString 异常: " + e); return ""; }
- if (s == null) return "";
- if (typeof s === "string") return s;
- if (s.join) return "" + s.join("\n"); // JS 数组
- if (typeof s.length === "number" && s[0] != null) { // Java String[]
- var 行 = [];
- for (var t = 0; t < s.length; t++) 行.push("" + s[t]);
- return 行.join("\n");
- }
- return "" + s; // java.lang.String 兜底
- }
- // 截屏+OCR 一轮:返回 {文字, 耗时毫秒},失败返回 null
- // 引擎用 paddleOCR(640,[0,0,1,1])(SDK 官方示例写法):
- // 踩坑实录:雷电模拟器上 MLKitOcr('zhs') 参数正确、截图正常(非黑屏),却静默返回空串,
- // 谷歌 MLKit 中文模型在模拟器上缺失,换 paddle 引擎即可正常出字
- function 截屏识别() {
- var img = null;
- try {
- printl("📸 正在截取整屏(首次会弹屏幕录制授权)...");
- img = screen.screenShotFull();
- if (!img) {
- printl("[OCR] ❌ 截图返回空(检查屏幕录制权限)");
- return null;
- }
- var 开始 = time.nowStamp();
- // 存调试图:识别异常时拉 /sdcard/debug_ocr.jpg 人工核对画面(本次就是靠它排除黑屏)
- try { img.save("/sdcard/debug_ocr.jpg"); } catch (eSave) { printl("[OCR] ⚠ 存调试图失败: " + eSave); }
- printl("🔍 屏幕已截取(已存 /sdcard/debug_ocr.jpg),开始 paddleOCR 离线识别...");
- var 文字 = 提取OCR文字(img.paddleOCR(640, [0, 0, 1, 1]));
- var 耗时 = time.nowStamp() - 开始;
- printl("🔍 识别完成,耗时 " + 耗时 + " 毫秒,共 " + 文字.length + " 个字符" +
- (文字.length > 0 ? ",前120字: " + 文字.substring(0, 120).replace(/\n/g, " | ") : "(空)"));
- if (耗时 > OCR超时报警毫秒) {
- printl("⚠ 报警:OCR 耗时 " + 耗时 + "ms 超过硬指标 " + OCR超时报警毫秒 + "ms,画面太复杂建议缩小识别区域");
- }
- return { 文字: 文字, 耗时: 耗时 };
- } catch (e) {
- printl("[OCR] ❌ 识别失败: " + e);
- return null;
- } finally {
- // 经典踩坑:截图不释放,长期挂机内存暴涨崩溃
- try { if (img != null) img.recycle(); } catch (e) {}
- }
- }
- // 关键词检查:命中返回关键词,否则 null
- function 查找命中关键词(文字) {
- for (var k = 0; k < 识别关键词列表.length; k++) {
- if (文字.indexOf(识别关键词列表[k]) >= 0) {
- return 识别关键词列表[k];
- }
- }
- return null;
- }
- // ========== 主流程 ==========
- printl("===== 案例20 屏幕OCR文字识别采集入库 启动 =====");
- // 步骤0:连库建表
- dbConnect();
- dbEnsureTable();
- printl("⚙ 配置:每 " + OCR间隔秒 + " 秒截屏识别 · 监控词(" + 识别关键词列表.join("/") + ") · OCR超时 " +
- OCR超时报警毫秒 + "ms · 库: " + SQLite路径);
- printl("ℹ 全新数据源:不看API不看文件,直接**看懂屏幕像素**——没接口的老APP也能采数据");
- printl("ℹ 自动切页演示:每轮 openApp 打开【" + 演示页面列表[0].名称 + "/" + 演示页面列表[1].名称 + "/" + 演示页面列表[2].名称 + "】循环,识别结果一轮一变");
- printl("📱 启动时前台包名: " + app.getTopPackName() + "(排查包名用:手动打开某APP再启动脚本,日志第一行就是它的真实包名)");
- // 停止按钮清理(顺序:先置标志,stopAllRuning 放最后)
- event.onStopBtnEvent(function () {
- gStop = true;
- dbClose();
- runTime.stopAllRuning();
- });
- // 步骤1:定时循环截屏识别
- var 轮次 = 0;
- var 页面索引 = 0;
- while (!gStop) {
- 轮次++;
- 累计识别轮数++;
- var 页面 = 演示页面列表[页面索引 % 演示页面列表.length];
- 页面索引++;
- printl("\n---- 第 " + 轮次 + " 轮:切换页面【" + 页面.名称 + "】→ 截屏识别 ----");
- // 自动打开目标页面并校验:openApp 后对比前台包名,不对就重试一次(演示时别停在多任务概览页)
- var 打开成功 = false;
- for (var 尝试 = 1; 尝试 <= 2 && !打开成功 && !gStop; 尝试++) {
- try {
- app.openApp(页面.包名);
- printl("🚪 第" + 尝试 + "次打开【" + 页面.名称 + "】(" + 页面.包名 + "),等待 " + 开页等待秒 + " 秒渲染...");
- sleep.second(秒 = 开页等待秒);
- var 前台 = "" + app.getTopPackName();
- if (前台.indexOf(页面.包名) >= 0) {
- 打开成功 = true;
- printl("📱 校验通过:前台已是 " + 前台);
- } else {
- printl("⚠ 页面没切过去(前台是 " + 前台 + ")" + (尝试 < 2 ? ",重试一次..." : ",照常截当前屏,若反复失败请把日志里的实际包名填回配置区"));
- }
- } catch (e) {
- printl("🚪 openApp 异常: " + e + ",照常截当前屏");
- break;
- }
- }
- var 结果 = 截屏识别();
- if (结果 != null && 结果.文字.length > 0) {
- // 逐条完整打印采集结果(演示重点:屏幕上写了啥,日志就有啥)
- var 显示文字 = 结果.文字;
- if (显示文字.length > 打印最大字数) {
- 显示文字 = 显示文字.substring(0, 打印最大字数) + "...(截断,共" + 结果.文字.length + "字)";
- }
- printl("📋 本轮识别到屏幕文字:");
- printl(" ┌ " + 显示文字.replace(/\n/g, "\n │ "));
- printl(" └ 共 " + 结果.文字.length + " 字 | 耗时 " + 结果.耗时 + "ms");
- var 命中词 = 查找命中关键词(结果.文字);
- var 命中标记 = 命中词 != null ? 1 : 0;
- if (命中词 != null) {
- printl("⚠ 报警:屏幕内容命中关键词「" + 命中词 + "」!");
- }
- // 屏幕去重:画面没变不重复入库(省库空间,也是监控脚本常用手法)
- if (结果.文字 === 上轮文字) {
- printl("ℹ 屏幕文字与上一轮相同(画面没动),本轮不重复入库");
- } else {
- printl("💾 画面已变化,写入数据库(表: " + 表名 + ")...");
- if (dbInsertText(结果.文字, 结果.文字.length, 命中标记)) {
- 累计入库条数++;
- printl(" ✓ 已入库 [" + 累计入库条数 + "] " + 结果.文字.length + " 字" +
- (命中词 != null ? " ⚠命中" : "") + " → " + SQLite路径);
- } else {
- printl(" ✗ 入库失败");
- }
- 上轮文字 = 结果.文字;
- }
- } else if (结果 != null) {
- printl("ℹ 屏幕无可识别文字(黑屏/纯图片页面),本轮跳过");
- } else {
- printl("⚠ 本轮识别失败,跳过入库(" + time.StampToTime(time.nowStamp()) + ")");
- }
- // 分片等待下一轮:每 5 秒打一条进度日志(画面不空,方便观察/录像)
- printl("⏳ 本轮完成," + OCR间隔秒 + " 秒后自动截下一屏");
- var 剩余 = OCR间隔秒;
- while (剩余 > 0 && !gStop) {
- var 步长 = 剩余 >= 5 ? 5 : 剩余;
- printl("⏳ 等待下一轮...剩余 " + 剩余 + " 秒(期间脚本在待机,到点自动截屏→OCR→入库)");
- sleep.second(秒 = 步长);
- 剩余 = 剩余 - 步长;
- }
- }
- dbClose();
- printl("===== 流程结束:共 " + 轮次 + " 轮识别,画面变化入库 " + 累计入库条数 + " 条 =====");
复制代码
|
|