简介:这份资源是一份AutoJs源码模板,主题为通过shell命令操作sqlite数据库,面向具备一定AutoJs基础、希望扩展本地数据存储与脚本自动化能力的开发者。它解决的是在AutoJs环境中直接调用shell执行sqlite语句、完成建库建表与增删改查等操作的学习需求,适合作为脚本项目中的数据库模块参考。压缩包为7z格式,仅含1个js文件,体积约775B,属于轻量级单文件脚本,便于快速阅读与移植。目前已有122人学习下载,说明该方向有一定关注度。读者可从中获取shell与sqlite结合的调用思路、命令拼接方式及脚本组织模板,用于理解AutoJs如何借助系统shell完成数据库操作,并在此基础上按自身项目需求调整表结构与执行逻辑。资源支持低版本AutoJs,安装后可直接运行查看效果,但仅供学习与参考,请勿用于商业用途,不熟悉AutoJs安装与使用的用户需谨慎下载。
1. AutoJs 里用 shell 操作 sqlite:为什么这条路径值得走
很多做 AutoJs 脚本的人,数据存储一开始都是storages或者写个 JSON 文件,量小的时候没问题,一旦要存几千条采集记录、做去重、按条件查询,JSON 就彻底不够用了。这时候最自然的想法是上 sqlite,但 AutoJs 本身没有内置 sqlite 的 JS 绑定,于是「用 shell 操作 sqlite」就成了绕不开的一条路。它的核心思路是:AutoJs 通过shell()或Runtime.exec()调用 Android 系统里的sqlite3命令行工具,把增删改查拼成 shell 命令执行,再把输出解析回 JS 对象。这条路的好处是不依赖任何第三方插件、不需要 root、不需要额外装 APK,只要设备上有sqlite3可执行文件就能跑。适合谁?适合已经能写 AutoJs 基础脚本、需要本地持久化、又不想引入复杂依赖的从业者。下面把我实际跑通过的方案拆开讲。
2. 先搞清楚 sqlite3 在 Android 上到底能不能用
2.1 Android 自带的 sqlite3 与它的限制
Android 系统底层一直用 sqlite 作为应用数据存储引擎,但「系统用了 sqlite」和「你能在 shell 里调用 sqlite3」是两回事。早期 Android(大约 4.x 到 7.x)的/system/xbin/sqlite3或/system/bin/sqlite3是普遍存在的,很多定制 ROM 也保留了这个二进制。但从 Android 8 开始,不少厂商 ROM 把sqlite3从 system 分区移除了,尤其是国内几家深度定制的系统。所以第一步不是写代码,而是确认目标设备上到底有没有这个可执行文件。
确认方法很简单,在 AutoJs 里跑一段 shell:
// 检查 sqlite3 是否可用,并打印版本 var result = shell("which sqlite3", true); toastLog("which 结果: " + result); var ver = shell("sqlite3 --version", true); toastLog("版本: " + ver);which sqlite3返回路径说明可用,返回空或者报not found就说明系统里没有。sqlite3 --version能进一步确认版本号,版本影响后面 SQL 语法的兼容性,比如UPSERT(ON CONFLICT DO UPDATE)需要 3.24 以上,JSON1扩展需要编译时开启。如果设备上没有,常见做法有两个:一是把编译好的sqlite3二进制 push 到/data/local/tmp/再chmod 755,二是干脆换用 AutoJs 的SQLite相关封装(部分版本有$sqlite或通过 Java 反射调android.database.sqlite)。但标题锁定的是 shell 路线,所以后面都假设sqlite3可用。
2.2 为什么不用 Java 反射而选 shell
AutoJs 基于 Rhino,能通过JavaAdapter、Packages调 Android 原生 API,理论上可以反射SQLiteDatabase.openOrCreateDatabase来操作数据库。我试过,能跑,但坑很多:Rhino 对 Java 重载方法匹配经常选错、Cursor的getColumnIndex在跨版本时行为不一致、异常栈被 Rhino 吞掉后极难排查。相比之下 shell 调sqlite3的边界非常清晰——输入是字符串命令,输出是字符串,中间没有类型转换的玄学。代价是每次操作都要 fork 一个进程,性能比原生 API 差,但对于脚本级的数据量(几千到几万条)完全够用。我一般会选 shell,因为可调试性高太多,出问题直接复制命令到 adb shell 里跑一遍就知道是谁的锅。
2.3 数据库文件放哪、权限怎么处理
数据库文件路径建议放在 AutoJs 自己的可访问目录,比如/sdcard/脚本名/data.db或者应用私有目录。放/sdcard的好处是方便用 DB Browser for SQLite 这类工具拉出来看,坏处是明文暴露。放私有目录更安全但导出麻烦。我一般放/sdcard/Download/xxx/data.db,调试期方便,上线前再考虑加密或迁移。
权限方面,sqlite3进程以 shell 用户身份运行,对/sdcard有读写权限,对/data/data/其他应用没有。所以数据库文件不要放在别的应用私有目录下,否则会报unable to open database file。另外首次创建数据库时,如果父目录不存在,sqlite3不会自动建目录,需要先mkdir -p。
// 确保目录存在再打开数据库 var dbPath = "/sdcard/Download/autojs/data.db"; shell("mkdir -p /sdcard/Download/autojs", true); // 打开(不存在则创建)并建表 var createSql = "CREATE TABLE IF NOT EXISTS records (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "title TEXT NOT NULL, " + "content TEXT, " + "created_at INTEGER DEFAULT (strftime('%s','now')));"; shell("sqlite3 " + dbPath + " \"" + createSql + "\"", true);这里CREATE TABLE IF NOT EXISTS保证重复执行不报错,strftime('%s','now')存 Unix 时间戳,比存字符串好排序。注意整条 SQL 用双引号包住传给 sqlite3,如果 SQL 内部本身有双引号(比如字符串值),需要转义,这是后面避坑章要重点讲的。
3. 把增删改查封装成可复用的 JS 函数
3.1 查询:把 sqlite3 的输出解析成对象数组
sqlite3默认输出是|分隔的裸文本,列之间用竖线,行之间用换行。要解析成 JS 对象,最稳的方式是用-separator和-line或者干脆用.mode json(需要 sqlite 3.33+)。我一般用-separator "\t"加-header,因为 tab 在内容里出现概率低,header 能拿到列名。
// 通用查询函数:返回对象数组 function dbQuery(dbPath, sql) { // -header 输出列名,-separator 用制表符分隔 var cmd = "sqlite3 -header -separator '\\t' " + dbPath + " \"" + sql + "\""; var out = shell(cmd, true); if (!out || out.trim() === "") return []; var lines = out.trim().split("\n"); var headers = lines[0].split("\t"); var rows = []; for (var i = 1; i < lines.length; i++) { var cols = lines[i].split("\t"); var obj = {}; for (var j = 0; j < headers.length; j++) { obj[headers[j]] = cols[j]; } rows.push(obj); } return rows; } // 用法 var list = dbQuery("/sdcard/Download/autojs/data.db", "SELECT id, title FROM records WHERE title LIKE '%关键词%' ORDER BY id DESC LIMIT 20;"); list.forEach(function(r) { log(r.id + " -> " + r.title); });逻辑说明:-header让第一行变成列名,-separator '\t'指定制表符。注意 shell 里单引号内的\t是否被解释取决于 shell,Android 的sh通常不解释,所以实际传进去的是字面\t两个字符,sqlite3 会把它当分隔符字符串处理——这里有个细节,sqlite3 的-separator接受的是字符串,\t会被它自己转义成 tab,所以能正常工作。如果发现分隔不对,改成-separator '|'更保险。参数dbPath和sql直接拼接有注入风险,脚本内部使用问题不大,但如果 SQL 里含用户输入,必须做转义。
3.2 写入:INSERT 与批量插入的拼接技巧
单条插入直接拼 SQL 就行,但要注意字符串里的单引号必须转义成两个单引号,否则 SQL 语法错误。批量插入如果一条条 fork 进程,1000 条要几十秒,正确做法是把多条 INSERT 拼成一个事务,一次 shell 调用搞定。
// 转义单引号,防止 SQL 语法错误 function esc(s) { return String(s).replace(/'/g, "''"); } // 批量插入:拼成一个事务,一次执行 function dbBatchInsert(dbPath, table, rows) { if (rows.length === 0) return; var values = rows.map(function(r) { return "('" + esc(r.title) + "','" + esc(r.content) + "')"; }).join(","); var sql = "BEGIN; INSERT INTO " + table + " (title, content) VALUES " + values + "; COMMIT;"; shell("sqlite3 " + dbPath + " \"" + sql + "\"", true); } // 用法:一次插入 500 条 var batch = []; for (var i = 0; i < 500; i++) { batch.push({ title: "标题" + i, content: "内容" + i }); } dbBatchInsert("/sdcard/Download/autojs/data.db", "records", batch);逻辑说明:esc把单个'替换成'',这是 SQL 标准转义。BEGIN...COMMIT把 500 条 INSERT 包成一个事务,sqlite 在事务里写盘只 fsync 一次,速度比逐条快几十倍。参数rows是对象数组,字段名要和表结构对应。注意 SQL 太长可能超过 shell 命令行长度限制(一般 128KB 左右),500 条通常没问题,上万条要分批,每批 500 到 1000 条比较稳。
3.3 更新与删除:条件拼接和影响行数确认
UPDATE 和 DELETE 的关键是 WHERE 条件必须写对,否则全表遭殃。我习惯在执行前先跑一次 SELECT COUNT 确认影响范围,执行后再查一次changes()。
// 安全更新:先确认影响行数 function dbUpdate(dbPath, sql, expectedMax) { // 先查会命中多少行 var countSql = sql.replace(/^UPDATE\s+\w+\s+SET[\s\S]*?WHERE/i, "SELECT COUNT(*) FROM records WHERE"); // 实际项目里更推荐单独传 countSql,这里演示思路 var out = shell("sqlite3 " + dbPath + " \"" + sql + "; SELECT changes();\"", true); var changed = parseInt(out.trim().split("\n").pop()); if (expectedMax && changed > expectedMax) { toastLog("警告:影响了 " + changed + " 行,超过预期 " + expectedMax); } return changed; } // 用法:只更新 id=5 的那条 dbUpdate("/sdcard/Download/autojs/data.db", "UPDATE records SET title='新标题' WHERE id=5;", 1);逻辑说明:SELECT changes();返回上一条语句影响的行数,拼在同一次 shell 调用里能拿到。参数expectedMax是预期最大影响行数,超过就告警,这是防止 WHERE 写漏的后悔药。注意changes()只对当前连接有效,所以必须和 UPDATE 在同一次 sqlite3 调用里执行,分两次调用会拿到 0。
4. 避坑与排查:shell 操作 sqlite 最常见的 5 个翻车点
4.1 现象:SQL 里带中文或特殊字符就报错
原因:shell 命令拼接时,中文本身没问题,但如果 SQL 字符串里含双引号、反引号、$、!这些 shell 元字符,会被 shell 先解释一遍,导致传给 sqlite3 的 SQL 已经变形。比如内容里有个$PATH,shell 会把它展开成环境变量。
解决:不要用双引号包 SQL,改用单引号包外层,SQL 内部的单引号用'\''转义。或者更彻底——把 SQL 写到临时文件,用sqlite3 db < file.sql的方式执行,完全绕开 shell 解析。
// 用临时文件执行 SQL,彻底避免 shell 转义问题 function dbExecFile(dbPath, sql) { var tmp = "/sdcard/Download/autojs/tmp.sql"; files.write(tmp, sql); var out = shell("sqlite3 " + dbPath + " < " + tmp, true); files.remove(tmp); return out; }4.2 现象:并发写入时报 database is locked
原因:sqlite 默认是库级锁,同一时刻只允许一个写事务。AutoJs 里如果开了多个线程同时写,或者上一个 shell 进程还没退出就发起下一个,就会撞锁。
解决:所有写操作串行化,用一个全局队列或者threads.lock()包住。另外在连接时设置PRAGMA busy_timeout=5000;,让 sqlite 遇到锁时等待而不是立刻报错。
// 每次写操作前设置 busy_timeout shell("sqlite3 " + dbPath + " \"PRAGMA busy_timeout=5000; " + sql + "\"", true);4.3 现象:查询结果里数字变成了字符串
原因:sqlite3命令行输出全是文本,id这种 INTEGER 列读回来也是字符串"5",直接做===比较会失败。
解决:解析时按列类型转换,或者查询时用CAST(id AS TEXT)明确,再在 JS 里parseInt。我一般建表时就记住哪些列是数字,解析后统一转。
4.4 现象:数据库文件越来越大,删了数据不释放空间
原因:sqlite 的 DELETE 只标记页面为空闲,不归还给文件系统。这是 sqlite 的设计,不是 bug。
解决:定期执行VACUUM;重建数据库文件。注意 VACUUM 需要额外磁盘空间,且执行期间会锁库,别在高峰期跑。
// 每周或数据量变化大时执行一次 shell("sqlite3 " + dbPath + " \"VACUUM;\"", true);4.5 现象:换设备后 sqlite3 命令找不到
原因:不同 Android 版本和 ROM 对sqlite3的支持差异很大,代码在 A 设备跑通,B 设备直接not found。
解决:启动时做一次能力探测,把sqlite3路径存成全局变量,找不到就降级到 JSON 存储或者提示用户。探测代码就是 2.1 节那段which sqlite3,把结果缓存起来,别每次操作都探测。
5. 进阶:用 shell 做数据库同步与可视化调试
5.1 把数据库导出成 CSV 再用 DB Browser 看
调试期最爽的方式是把表导成 CSV,拉到电脑上用 DB Browser for SQLite 打开看。sqlite3 自带.mode csv和.output命令,一条 shell 就能导。
// 导出 records 表到 CSV var csvPath = "/sdcard/Download/autojs/records.csv"; shell("sqlite3 " + dbPath + " \".mode csv\" \".output " + csvPath + "\" \"SELECT * FROM records;\"", true);逻辑说明:.mode csv切换输出格式,.output重定向到文件,最后跟 SELECT。注意这些点命令必须用双引号分别包住,因为它们是 sqlite3 的元命令,不是 SQL。导出后 CSV 可以直接用电脑上的工具打开,比在手机上看强太多。
5.2 两台设备间的数据库同步思路
热词里有人搜「数据库同步软件」,其实脚本场景下不需要那么重。最简单的同步是:设备 A 导出 SQL 文本,设备 B 执行。用sqlite3 db .dump导出整个库的 SQL,传到另一台设备sqlite3 newdb < dump.sql导入。
// 导出整个数据库为 SQL 文本 var dumpPath = "/sdcard/Download/autojs/dump.sql"; shell("sqlite3 " + dbPath + " .dump > " + dumpPath, true); // 在另一台设备导入 // shell("sqlite3 /sdcard/Download/autojs/new.db < /sdcard/Download/autojs/dump.sql", true);注意.dump导出的是完整重建脚本,包含 CREATE TABLE 和所有 INSERT,导入到已有数据的库会主键冲突。同步场景下一般导入到空库,或者先DELETE FROM再导入。如果数据量大,dump 文件可能几十 MB,传输用 AutoJs 的files.copy或者走局域网 HTTP 都行。
5.3 一个我常用的验证习惯
每次改完数据库操作代码,我不会直接跑主流程,而是先写一个最小验证脚本:建一张测试表,插 3 条,查出来打印,更新 1 条,再查,删除 1 条,再查。全程用toastLog输出每一步的changes()和结果行数。这个习惯帮我省了无数次「以为写对了其实 WHERE 漏了」的翻车。sqlite 这东西,命令行下看着简单,但 shell 拼接的转义、锁、类型这三座大山,不亲手踩一遍是记不住的。希望帮到你。
本文还有配套的精品资源,点击获取