尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
AutoJs 用 Shell 操作 SQLite:稳定兼容的本地数据方案
简介针对AutoJs环境下通过shell命令操作sqlite数据库的实际需求这是一份可直接运行的js脚本源码适合有一定AutoJs基础、希望在脚本中调用系统shell完成本地数据库增删改查的自动化开发者。压缩包内仅含1个js文件整体大小约775B体量虽小但结构清晰方便直接查看与移植。目前已有122人浏览学习。资源重点展示如何借助AutoJs的Shell模块调用sqlite3命令行工具内容覆盖连接数据库、执行SQL语句、查询结果输出等常见操作写法对理解Android自动化场景下的数据库交互有直接帮助可作为日常编写AutoJs脚本时的参考模板。1. 用 shell 操作 sqliteAutoJs 里最稳的本地数据方案做 AutoJs 脚本的人早晚会碰数据库尤其是处理采集结果、离线缓存、启动本地服务这些场景。说实话AutoJs 自带的 SQLite 封装在不同 ROM 上表现差异很大有的命令行直接不可用有的返回值乱七八糟。我更倾向的做法也是这份源码包里的核心思路直接用 Shell 执行系统自带的 sqlite3 命令行工具。好处很明显——不依赖 Java 模块、不绑定版本、任何能跑 AutoJs 的设备基本都能用。你只需要会 SQL就能把数据库读写在自动化脚本里玩出花。这篇文章我把源码拆开讲清楚原理、封装方法、参数边界和几个真正踩过的坑新手能照着写老手能直接抄封装。2. 为什么选 shell sqlite3先看原理和选型2.1 AutoJs 与 sqlite 的三种接入方式对比AutoJs 环境里操作 SQLite常见有三条路我实际对比过之后才定下 shell sqlite3 的方向。第一种是直接用 AutoJs 内置的SQLiteDatabase类。这个类提供openDatabase、executeSQL、query等方法Java 层封装调用方便。问题在于它在部分设备上会有兼容问题比如打开数据库时返回空引用、SQL 语句对中文支持不好、以及对于需要返回大数据量的查询结果集处理起来很繁琐。我遇到过一次在某个精简版系统上直接抛找不到类整段脚本瘫痪。第二种是引入第三方模块比如 sqlite 相关的 npm 包或紫袍等大神封装的库。这些库通常自带编译好的二进制体积不小而且 AutoJs 对多 dex 和 so 的加载有各种限制。集成成本高出了问题还不好排查。第三种就是 shell sqlite3。安卓系统从很早就内置了 sqlite3 命令行路径一般在/system/bin/sqlite3或/system/xbin/sqlite3。AutoJs 的Shell.exec可以直接调用系统命令把 SQL 作为参数传进去再用 stdout 拿返回结果。这个方案的兼容性极限高只要系统还能跑命令行数据库操作就能通。下表是我自己的选型记录接入方式依赖兼容性大数据量性能复杂度内置 SQLiteDatabaseAutoJs 运行时中等因 ROM 而异中等结果集转换慢低第三方模块带 so/二进制低易加载失败较高高shell sqlite3系统 sqlite3 命令高几乎所有设备高直接用原生工具中等2.2 sqlite3 命令行的执行流程与返回码理解 shell 方式的核心是知道sqlite3是一个独立的命令行程序执行一次就是一次完整的进程生命周期。基本调用格式是sqlite3 /path/to/database.db SQL语句SQL 语句执行完后sqlite3 把结果以文本形式打到 stdout如果执行出错会把错误打到 stderr然后进程退出退出码变成0或非0。在 AutoJs 里Shell.exec返回的对象大致长这样{ code: 0, // 返回码0 表示成功 result: ..., // stdout 输出 error: ... // stderr 输出 }这里的code是判断成败的核心。但有个细节sqlite3 在执行多条 SQL 时如果其中某条失败它默认会继续往下跑最终退出码可能还是 0。所以你不能只用一个code判断所有 SQL 都成功了。我一般会加上-bail让它在遇到第一条错误时立刻停止返回非 0 码。还有一种情况是查询结果的格式化。默认sqlite3输出是管道符分隔的文本表格看起来像这样id|name|value 1|张三|100如果你想在 AutoJs 里逐行处理最好加上-separator和-header控制格式。比如用|作分隔带上列名头这样后续解析会容易很多。具体封装在下一章展开。3. 封装一个可复用的 shellSqlite 模块代码与参数解析3.1 最小可用代码执行一条 SQL 并拿到结果这是源码包里最核心的一段我重写成了一个独立函数执行任意 SQL 并返回 AutoJs 的 Shell 结果对象。代码不长但每个参数都值得留意。/** * 执行 shell 命令返回结果对象 * param {string} cmd 完整命令 * returns {object} { code, result, error } */ function exec(cmd) { let res Shell.exec(cmd); if (res null) { throw new Error(Shell.exec 返回空值可能没有 shell 权限); } return res; } /** * 执行一条 SQL带上 -bail 让失败立即中断 * param {string} dbPath 数据库文件的绝对路径 * param {string} sql 要执行的 SQL可以带分号多条 * returns {object} 返回 Shell 原始结果 */ function execSql(dbPath, sql) { let cmd sqlite3 -bail quote(dbPath) quote(sql); return exec(cmd); }逻辑说明exec只负责调Shell.exec并且把空返回当成异常抛出来因为 AutoJs 在部分场景下会因权限问题或者命令太长直接返回 null。execSql把数据库路径和 SQL 分别用quote包起来这个函数我们马上讲核心就是给字符串加单引号并转义内部单引号。参数说明dbPath建议使用绝对路径用相对路径会跟随当前工作目录脚本如果在不同目录被 ctrlc 或定时任务拉起来很容易出现找不到库文件。sql可以直接传完整 SQL里面带分号多条一起执行也没问题因为-bail保证第一条失败就停了不会继续执行后面的 SQL避免脏数据扩散。3.2 处理查询结果把文本表格变成对象数组前面说了sqlite3 默认输出是文本表格。如果只拿res.result做字符串处理你会发现统计、排序、过滤都得先写拆分正则非常折磨。我习惯于把查询结果统一转换成对象数组用 AutoJs 的Array和Object直接处理。先看命令层面的格式控制/** * 执行查询返回对象数组 * param {string} dbPath * param {string} sql 查询语句 * returns {ArrayObject} 每行一个对象key 是列名 */ function query(dbPath, sql) { let cmd sqlite3 -bail -header -separator | quote(dbPath) quote(sql); let res exec(cmd); if (res.code ! 0) { throw new Error(查询失败 res.error); } let lines res.result.replace(/\r/g, ).split(\n).filter(line line.length 0); if (lines.length 1) { return []; // 只有表头没数据 } let header lines[0].split(|); let rows []; for (let i 1; i lines.length; i) { let cols lines[i].split(|); let row {}; for (let j 0; j header.length; j) { row[header[j].trim()] cols[j] || null; } rows.push(row); } return rows; }关键点-header让第一行输出列名-separator |把每列用竖线分隔。这里我特意选了|作为分隔符因为它在 SQL 查询结果里出现的概率比逗号、制表符都低。如果你查询的文本字段本身可能包含|最保险的还是换用二进制格式或者临时采用\t但\t在 shell 传递中容易丢所以我用|然后再做一层转义这是后话。逻辑说明拿到res.result后先统一去掉\r换行然后按\n拆分剔除空行。第一行作为表头后续每行拆出来的单元格数量和表头不一致时用||补空防止数组越界。多余的格子直接丢保底逻辑是cols[j] || null这样空字符串不会变成 undefined。3.3 参数化拼接与路径处理空格、中文路径怎么传shell 方式最大的坑就是引号和转义。我经常看到有人直接把路径和 SQL 字符串一拼就执行遇到带空格的路径立刻崩。正确做法是写一个quote函数把每个参数包一层单引号并把参数内部的单引号做 SQL 转义。/** * 把字符串安全包装成 sqlite3 命令行的单引号字面量 * param {string} str * returns {string} 包装后的字符串 */ function quote(str) { if (str null) { str ; } // 单引号是 sqlite 字符串字面量的定界符内部单引号翻倍即可转义 return String(str).replace(//g, ) ; }说明一下转义逻辑在 SQL 标准里字符串字面量内部如果包含单引号写成两个连续单引号代表一个单引号。比如Its a book在 SQL 里应该写Its a book。这个规则同时适用于 sqlite3 命令行的-separator参数和 SQL 文本。但要注意它不能替代 SQL 注入防护——如果你的 SQL 是动态拼接用户输入建议把变量先经过quote再组装到 SQL 里。还有一个容易忽略的点AutoJs 的Shell.exec默认不是 bash而是直接执行字符串。如果你在命令里用了、||、这些 shell 语法可能不生效。我一般不用管道需要多个操作就分成多次exec或者干脆把多条 SQL 放进一条语句里用分号连接。如果确实想用管道就给命令套一层sh -c ...但引号嵌套会把你逼疯不建议在 AutoJs 里这么做。4. 实战初始化表、批量插入、事务提交4.1 初始化数据库和表结构实际项目中我一般把表结构初始化写成一个initDb函数里面用CREATE TABLE IF NOT EXISTS保证重复调用不会报错。代码长这样/** * 初始化数据库表结构 * param {string} dbPath */ function initDb(dbPath) { let sql CREATE TABLE IF NOT EXISTS app_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, package_name TEXT NOT NULL, action_time INTEGER NOT NULL, duration INTEGER DEFAULT 0, extra TEXT ); CREATE INDEX IF NOT EXISTS idx_time ON app_records(action_time);; let res execSql(dbPath, sql); if (res.code ! 0) { throw new Error(初始化表失败 res.error); } }这个函数执行一条包含两个建表语句的 SQL建表 建索引。AUTOINCREMENT在这个表里不是必须的因为默认的rowid已经自增但我加上的原因是后续有数据清理需求要知道表的最大 idAUTOINCREMENT会多维护一张sqlite_sequence表让自增主键不会被复用避免删除后 id 混乱。action_time我设为 INTEGER 类型直接用时间戳。很多新手喜欢用DATETIME类型但 sqlite 的日期函数处理时间戳反而更方便比如strftime(%Y-%m-%d, action_time/1000, unixepoch)。这里有个小细节如果数据库文件不存在sqlite3 会自动创建新文件。所以initDb里不需要先判断文件是否存在直接执行建表语句文件就有了。4.2 批量插入数据与事务边界批量插入很容易写出慢循环一条一条 insert每条都启动一个新进程时间全部浪费在进程初始化上。正确做法是把所有插入放进同一条 sqlite3 命令里用事务包裹。这样sqlite3进程只启动一次插入数据在内存里做最后一次性提交。/** * 批量插入多条记录 * param {string} dbPath * param {ArrayObject} records 对象数组字段对应表列 */ function batchInsert(dbPath, records) { if (!records || records.length 0) { return; } let values records.map(r ( r.id , quote(r.package_name) , r.action_time , (r.duration || 0) , (r.extra ? quote(r.extra) : NULL) )).join(,); let sql BEGIN TRANSACTION; INSERT INTO app_records (package_name, action_time, duration, extra) VALUES values ; COMMIT;; let res execSql(dbPath, sql); if (res.code ! 0) { // 回滚兜底 execSql(dbPath, ROLLBACK;); throw new Error(批量插入失败 res.error); } }注意VALUES后面我没有指定列名因为id是自增主键不插入让系统自动生成。如果你插入的数据里某项为 null就传NULL不然 sqlite 会把空字符串当成空字符串存进去。这里的duration如果真有 0r.duration || 0没问题但如果它是字符串0||会把它当 truthy导致值变成字符串0类型就不对了。所以严谨点应该用typeof r.duration number ? r.duration : 0。事务为什么重要sqlite3 命令行每执行一句话默认就会自动开启事务并提交速度极慢。而BEGIN TRANSACTION和COMMIT打包在一起后所有插入在一个事务里完成速度提升几十倍而且只要有一条失败你能通过ROLLBACK回滚不会留下半截数据。4.3 带日期的分表与清理自动化数据积累起来很快我习惯按天分表比如app_records_20250321。这样清理历史数据很简单直接删表。分表逻辑在前端拼表名SQL 里记得表名不能走quote因为表名是标识符不是字符串。/** * 按天创建分表并写入数据 * param {string} dbPath * param {string} dateStr 格式如 20250321 */ function writeDailyTable(dbPath, dateStr) { let tableName app_records_ dateStr; let createSql CREATE TABLE IF NOT EXISTS tableName (id INTEGER PRIMARY KEY AUTOINCREMENT, package_name TEXT NOT NULL, action_time INTEGER); execSql(dbPath, createSql); // 每天只保留最近7天分表其余直接 drop let deleteSql SELECT name FROM sqlite_master WHERE typetable AND name LIKE app_records_%;; let tables query(dbPath, deleteSql); let oldPrefix new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10).replace(/-/g, ); for (let t of tables) { let m /app_records_(\d{8})/.exec(t.name); if (m m[1] oldPrefix) { execSql(dbPath, DROP TABLE IF EXISTS t.name); } } }这里query返回的数组每个元素是对象字段名是name直接访问t.name就行。用正则/\d{8}/提取日期字符串用字符串比较判断大小因为YYYYMMDD格式的字符串比较和日期比较结果一致。删除表时使用DROP TABLE IF EXISTS避免重复执行时残留错误信息。5. 避坑shell 操作 sqlite 最常见的 4 个翻车现场5.1 现象sqlite3 命令找不到输出空我拿到这份源码包的第一天在模拟器上跑sqlite3 -version返回结果是空字符串。然后execSql一路报错错误码 127stderr 里写着sqlite3: not found。原因安卓系统虽然内置 sqlite3但有几个常见原因导致命令不可用一是某些国产 ROM 精简了/system/xbin里的工具二是当前 shell 的 PATH 没有包含/system/xbin三是使用 adb shell 或 AutoJs 的 Shell 时环境变量与系统默认不同。解决先检测可执行文件是否存在。用exec(which sqlite3)结果为空则尝试绝对路径/system/xbin/sqlite3。如果还没有只能从系统镜像里提取或者改用 AutoJs 内置模块。我现在的做法是写一个getSqlitePath()函数启动时自动探测返回可用路径后续所有命令都拼上这个路径。function getSqlitePath() { let candidates [sqlite3, /system/bin/sqlite3, /system/xbin/sqlite3, /data/local/tmp/sqlite3]; for (let p of candidates) { let res exec(which p || echo NOT_FOUND); if (res.result.indexOf(NOT_FOUND) -1) { return p; } } return null; }5.2 现象中文和特殊字符写不进去或查询乱码我在写入extra字段时存了一段带中文的 JSON 字符串执行后查出来是完全乱码而且 json 解析直接失败。原因sqlite3 命令行会把参数按照当前 locale 的编码方式传给程序。AutoJs 进程默认可能是 UTF-8但 sqlite3 终端环境下可能按 ASCII 或者系统默认编码解析。如果 SQL 里带了中文又没有正确编码转换写入的就是坏的字节。解决在命令前面加上环境变量LANGen_US.UTF-8或LC_ALLC.UTF-8。AutoJs 的 Shell 支持把环境变量写在命令前比如let cmd LANGen_US.UTF-8 sqlite3 ...;如果这样还不行用export语法let cmd export LANGen_US.UTF-8; sqlite3 ...;注意;后面有空格这是 shell 语法不是 SQL。实测下来LANG能解决大部分中文问题。另外写入前确认你拿到的字符串是 AutoJs 的 UTF-8 字符串不要混入 GBK。5.3 现象事务没提交数据丢了我在批量插入时一开始没加COMMIT只是循环调用execSql执行单条 insert。跑完看数据只有一半进了表另一半丢了。原因每次execSql调用 sqlite3 命令都是独立进程。第一条语句执行成功自动提交第二条语句执行时因为数据库文件被上一个进程占用或者命令字符串太长被系统截断静默失败了。更可怕的是如果进程在事务中途被杀sqlite3 自动回滚你已经以为数据写入了。解决把多条 insert 放进同一条命令里并且用BEGIN TRANSACTION和COMMIT包裹。这样要么全部成功要么全部回滚不存在半截状态。如果你确实要拆多次命令那每一批都必须是一个完整的事务不要跨命令保持事务。5.4 现象数据库被锁操作超时分表清理时我拿着历史表的 DROP 语句执行偶尔会卡住几十秒最后报错database is locked。原因sqlite 是文件级锁当另一个进程正在写库比如 AutoJs 后台脚本还在插入数据你的 DROP 或者写操作会拿不到排他锁。sqlite3 默认等待 2 秒就放弃。解决sqlite3 命令支持-cmd .timeout 5000设置 busy timeout 为 5000 毫秒。在命令最前面加上这个参数让多次尝试等待锁释放。如果依然报错就在应用层做重试循环每次间隔 200 毫秒最多试 10 次。我在删除分表那个函数里就是这样做的function dropTableSafe(dbPath, tableName) { let sql DROP TABLE IF EXISTS tableName; for (let i 0; i 10; i) { let res execSql(dbPath, sql); if (res.code 0) { return; } if (res.error.indexOf(locked) -1) { sleep(200); continue; } throw new Error(删除表失败 res.error); } throw new Error(表被锁超时无法删除); }6. 进阶把 shell 操作封装成通用工具还能做增量备份很多人把 sqlite3 只当成执行 SQL 的通道忽略了它自带的点命令。.dump和.backup是两个非常好用的功能。.dump输出重建数据库的完整 SQL 脚本.backup则是 VFS 层的备份支持热备不会锁库。我在 AutoJs 脚本里做定时备份用.backup最合适。/** * 备份数据库到指定文件 * param {string} dbPath 源库 * param {string} backupPath 备份文件路径 */ function backupDb(dbPath, backupPath) { let cmd sqlite3 quote(dbPath) \.backup backupPath \; exec(cmd); }注意.backup是 sqlite3 的点命令必须放在引号里作为单个参数传给 sqlite3而不是作为 SQL。所以命令格式是sqlite3 数据库路径 .backup 备份路径。如果备份路径里有空格点命令内的路径遵循 sqlite3 的规则可以直接用单引号包住。实测在 AutoJs 里这样写是能工作的。另一个进阶技巧是把所有操作与锁检测结合起来。我写过一段通用函数传入一个函数式操作自动加上锁等待和重试。比如备份时如果正在大批量写入backup会立即成功但写的瞬间数据可能不一致所以我会先执行一次PRAGMA busy_timeout3000;再执行 backup。最后分享一个我养成的习惯所有涉及数据库写入的关键路径都不允许裸调用 shell。我会强制走一个带事务和锁重试的统一模块接口哪怕是单条 insert 也包一层BEGIN和COMMIT。有一回某项目上线临时起意直接写了一句 update 命令忘了包事务结果崩溃时留下半更新数据排查了半天。从那以后我每次写数据库操作都强制走一遍事务封装再也没翻过车。这份源码包里的封装逻辑基本就是按这个思路整理出来的希望能帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

蓝桥系统003进制转换:从原理到实战的完整解析

蓝桥系统003进制转换:从原理到实战的完整解析

1. 进制转换到底在考什么1.1 从一道题看进制转换的本质“蓝桥系统003进制转换”这个标题,乍一看像是一道普通的编程练习题,但真正做过的人都知道,它背后牵扯的东西远比“把十进制转成二进制”要深。进制转换是计算机科学里最基础、也最容易被…

📅 2026/10/9 20:08:04
微信小程序支付全链路实战:从统一下单到异步回调的避坑指南

微信小程序支付全链路实战:从统一下单到异步回调的避坑指南

1. 从一次订单流失说起:小程序支付的完整链路到底卡在哪做小程序电商的同行大概率都遇到过这种场景:用户点下"立即购买",页面转了两圈,然后弹出一句"支付失败,请稍后重试"。你去看后台日志&#x…

📅 2026/10/9 20:08:04
2026运维自动化工具对比:从全场景覆盖到合规稳定落地

2026运维自动化工具对比:从全场景覆盖到合规稳定落地

2026年如果还停在“写脚本批量执行命令”这个自动化水平,选型时基本会被直接筛掉。这一年行业里聊自动化运维,口径已经从“用了什么工具”变成了“全场景自动化覆盖到什么程度”,再叠加业务合规和稳定性的硬约束,“运维自动化工具…

📅 2026/10/9 20:08:04
MORE NEWS

更多资讯

📰

Linux下连接MySQL:头文件与库文件配置及多架构编译实战

简介:面向 Linux 下进行 C/C 开发的工程师,这套资源专门解决系统缺少 MySQL 连接头文件与库文件时的编译与链接问题。压缩包共 77 个文件,以 68 个头文件(.h)为主,配合 8 个静态库(.a&#xff0…

📰

Java DefaultTableModel构造方法Vector参数顺序详解与避坑指南

1. 问题背景与核心痛点拆解1.1 为什么一个构造方法能让人调半天做Java桌面端开发的朋友,尤其是用Swing写表格界面的,大概率都碰过DefaultTableModel这个类。它是javax.swing.table包下的默认表格模型实现,封装了行数据、列名、以及增删改查的…

📰

Python中的pathlib库使用详解

前言 在 pathlib 出现之前,处理路径靠的是 os.path 里的一堆函数:os.path.join(a, b)、os.path.splitext(p)、os.path.basename(p)……它们是把路径当作字符串来操作的,于是代码里到处是拼接、切片、判断分隔符。pathlib(PEP 428&…

📰

Python中的self用法详解

前言 self 大概是 Python 初学者最先记住、又最先误解的东西。它的误解通常集中在三点:以为 self 是关键字;以为它必须叫 self;以为「方法里的 self 会自动变成一个实例」。 真相要从描述符协议说起:函数本身就是一种非数据描述符…

📰

Python中的super().__init__()用法详解

前言 写子类时,很多人会习惯性地在 __init__ 里加一句 super().__init__(),问为什么,答「网上都这么写」。这句到底在做什么、什么时候会出错、多重继承下为什么顺序和你以为的不一样,往往说不清楚。 先纠正一个流传极广的说法&am…

📰

技术周榜深度拆解:开发工具、AI与前端项目的选型逻辑与避坑指南

1. 周榜数据背后的信号:为什么值得花时间逐项拆解每周刷一次热榜的人很多,但真正把榜单当成"技术风向标"来读的人很少。大多数人扫一眼项目名,看到几个眼熟的词,点进去瞄两眼 README,然后关掉页面&#xff0…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬