尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AutoJs 通过 shell 操作 sqlite 数据库:增删改查封装与避坑指南

AutoJs 通过 shell 操作 sqlite 数据库:增删改查封装与避坑指南 简介这份资源是一份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 语法的兼容性比如UPSERTON 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 反射而选 shellAutoJs 基于 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是否被解释取决于 shellAndroid 的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\sSET[\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; } // 用法只更新 id5 的那条 dbUpdate(/sdcard/Download/autojs/data.db, UPDATE records SET title新标题 WHERE id5;, 1);逻辑说明SELECT changes();返回上一条语句影响的行数拼在同一次 shell 调用里能拿到。参数expectedMax是预期最大影响行数超过就告警这是防止 WHERE 写漏的后悔药。注意changes()只对当前连接有效所以必须和 UPDATE 在同一次 sqlite3 调用里执行分两次调用会拿到 0。4. 避坑与排查shell 操作 sqlite 最常见的 5 个翻车点4.1 现象SQL 里带中文或特殊字符就报错原因shell 命令拼接时中文本身没问题但如果 SQL 字符串里含双引号、反引号、$、!这些 shell 元字符会被 shell 先解释一遍导致传给 sqlite3 的 SQL 已经变形。比如内容里有个$PATHshell 会把它展开成环境变量。解决不要用双引号包 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_timeout5000;让 sqlite 遇到锁时等待而不是立刻报错。// 每次写操作前设置 busy_timeout shell(sqlite3 dbPath \PRAGMA busy_timeout5000; 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 拼接的转义、锁、类型这三座大山不亲手踩一遍是记不住的。希望帮到你。本文还有配套的精品资源点击获取
返回列表