简介:AutoJs源码-用shell操作sqlite数据库,是一份以JavaScript编写、可在AutoJs环境中直接运行的脚本模板,主要面向自动化脚本初学者,以及需要在本地实现数据库读写操作的安卓开发爱好者。脚本演示了通过命令行工具访问SQLite数据库的常见流程,包括建表、插入、查询、修改等操作思路,同时兼顾了低版本AutoJs的兼容性,运行门槛较低。整个资源以7z格式压缩,包内仅一个js文件,体积约775字节,结构简洁,便于对照学习或二次修改;虽然体量很小,但代码从准备命令、调用外部程序到读取返回结果均有体现,能够帮助理解自动化脚本与系统命令协同工作的基本方式。当前已有121人学习或下载,对于迷你型模板资源来说,参考价值较为直接。作者为m0_56069948,使用时请遵守资源描述中的学习与参考声明,不可用于商业用途。
1. 为什么放着现成的SQLite库不用,非要去调Shell
先交代下背景。这个方案的核心思路,听名字就清楚了:在AutoJs里跑JavaScript脚本时,不再调用内置的sqlite模块去操作数据库,而是通过拾取系统的sqlite3命令,利用Shell去执行建表、查询、更新这些SQL语句。说直白点,就是让你的JS代码变成一个小调度器,真正干活的是Android系统里的命令行数据库客户端。
很多人第一反应是:AutoJs明明自带SQLite API,直接require('sqlite')然后openDatabase('xxx.db')不就完事了吗?折腾Shell是不是有点脱了裤子放屁?一开始我也这么想,直到被现实教育了几次。
先说下AutoJs内置SQLite的使用方式,它确实方便,但坑也不少:
const db = sqlite.open("/sdcard/data/test.db"); db.execute("create table if not exists user (id INTEGER PRIMARY KEY, name TEXT)"); db.insert("user", { name: "zhangsan" }); db.close();有没有发现问题?这套API的出发点是通过纯JavaScript驱动,本质上是编译进AutoJs应用内部的SQLite支持库在工作。它能解决的问题是——如果你只需要操作一个位于应用私有目录或者其他可写目录的简单数据库,那确实够用。但是当你的脚本开始牵扯到下面这些场景,内置API就很难受了:
- 数据库文件被其他进程长时间占用,AutoJs的SQLite模块拿不到锁,干脆报错
- 你需要跨应用去读另外一款App的数据库,比如微信聊天记录备份脚本、王者荣耀对局数据解析这类东西,这些库通常在
/data/data/包名/databases/下面,权限不够就不可能用JavaScript API直接打开;可一旦用root权限执行Shell命令,sqlite3作为独立进程就能以高权限读取,完全绕开包名限制 - 你手上的数据库是用某些老版本SQLite生成的,文件格式或者索引页大小有兼容性问题,AutoJs内嵌的SQLite版本太新或者太旧都会拒绝打开,而系统的
sqlite3二进制一般跟ROM深度绑定,兼容性更稳
说白了,Shell方式把你的操作从AutoJs进程里剥离出来,变成了独立的命令行操作。数据库锁、进程权限、并发访问这些问题,都不再跟AutoJs的运行时绑定在一起。遇到“这个库只能通过命令行打开”的情况,这条道就是唯一解。
另外,如果你还没有越狱手机的环境,先确认一下自己是否有root权限,因为下文的很多操作都建立在Shell能拿到足够权限的基础上。没有root的话,很多目录读不了,思路也要跟着调整。
2. 准备阶段:确认sqlite3可执行,以及第一个Hello查询
Shell操作SQLite的整个地基,就是一个能在Android设备上跑起来的sqlite3可执行文件。这个东西在很多国产ROM上不一定预装,尤其是在那些精简了系统组件的定制固件上。所以第一步不是写脚本,是确认环境。
2.1 怎么判断设备上有没有这个命令
用AutoJs做一个最简单的检测:
function checkSqlite() { const result = exec("which sqlite3"); return result.code === 0 && result.result.indexOf("sqlite3") !== -1; } log("sqlite3 available: " + checkSqlite());which命令返回非零时,说明库里没有这个工具。大多数情况下你需要安装一个独立的sqlite3二进制,或者用BusyBox挂上。BusyBox这种“嵌入式瑞士军刀”也内置了sqlite3的实现,操作方式基本一致。
2.2 用BusyBox补环境
比较通用的做法是直接安装BusyBox,然后使用它的完整版命令。很多反编译工具包或者Android优化工具里都有现成的BusyBox二进制,你可以把它丢到/data/local/tmp/或者/system/xbin/下,然后给执行权限:
adb push busybox /data/local/tmp/ adb shell chmod 755 /data/local/tmp/busybox adb shell /data/local/tmp/busybox --install /data/local/tmp/装完之后再执行which sqlite3,就能发现它指向了/data/local/tmp/sqlite3。后续所有脚本里可以直接写完整路径,比如exec("/data/local/tmp/sqlite3 /data/user.db ..."),这样就不依赖环境变量了,遇到PATH被精简的情况也不慌。
2.3 第一条shell查询命令
先把最基础的用法跑通。sqlite3命令的标准格式是:
sqlite3 [选项] [数据库文件] [SQL语句]比如建数据库并插入一条记录:
sqlite3 /sdcard/test/hello.db "CREATE TABLE IF NOT EXISTS demo(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO demo(name) VALUES('hello'); SELECT * FROM demo;"在AutoJs里调用Shell执行这个命令的写法是这样:
const result = exec("sqlite3 /sdcard/test/hello.db \"SELECT * FROM demo;\""); log(result.result);输出默认是管道分隔的文本行。插入和查询同名不同效果,你可以在一条命令里用分号分隔多个SQL,shell会按顺序执行。这是sqlite3命令行工具的默认交互模式——你执行命令时传的是整个字符串。
2.4 关于表头和列格式的一个易忽略的点
默认输出里不显示列名,只有数据行。如果需要带列名,加参数:
sqlite3 -header -column /sdcard/test/hello.db "SELECT * FROM demo;"-header会打印出列名,-column会按列对齐排版。在AutoJs脚本里解析对齐后的文本反而更麻烦,我更推荐用默认的文本管道输出,配合自己的解析逻辑处理。
提示:当你用
-column参数时,输出的对齐空格是排版用的,程序解析时要先split再trim,不然会带出一堆空白字符。我踩过这个坑。
3. AutoJs调用Shell的三种姿势,以及完整实战脚本
这一节把最核心的东西放出来。AutoJs里执行Shell命令不只是exec()一个选择,不同场景最好用不同方式。直接上代码。
3.1 方式一:exec()——一次性命令,适合短查询
AutoJs的exec()会阻塞等待命令执行完毕,返回标准输出、错误输出、退出码三个信息。适合执行那种几秒钟内能出结果的命令。
function queryByExec(dbPath, sql) { const res = exec(`sqlite3 "${dbPath}" "${sql.replace(/"/g, '\\"')}"`); if (res.code !== 0) { throw new Error("sqlite error: " + res.error); } const lines = res.result.split("\n").filter(line => line.length > 0); const rows = lines.map(line => line.split("|")); return rows; }注意这里对SQL里的双引号做了转义,避免SQL本身包含引号时把命令行搞乱。比如查询SELECT * FROM tbl WHERE name = "a",外层传参时会冲突,必须转义内部的双引号。
3.2 方式二:Shell对象——交互式Shell,适合长任务和后续维护
AutoJs里有new Shell()能起一个真正的Shell会话,长期驻留后台,多条SQL塞进去不用反复起进程,频繁操作时性能优势很明显:
const shell = new Shell(true); // true表示root权限 shell.exec("sqlite3 /sdcard/test/hello.db .tables", (code, output) => { log("code: " + code); log("output: " + output); }); shell.exit();Shell对象会持续存活直到你手动调用exit()。如果脚本是一堆循环插入操作,每隔几分钟就调一次exec()起新进程,和用Shell对象维护单个进程比,开销完全不同。后者明显更适合做长任务。
3.3 方式三:直接用sh脚本文件——最复杂的逻辑也不怕
把一堆SQL和Shell逻辑打包成一个.sh文件,丢到设备上,然后AutoJs里只负责执行它。适合那种SQL特别复杂、包含事务、包含Shell变量替换的场景。
const script = ` #!/system/bin/sh DB_PATH="/sdcard/test/hello.db" sqlite3 $DB_PATH <<EOF BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS log(id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, msg TEXT); INSERT INTO log(ts, msg) VALUES(${Date.now()}, 'start'); INSERT INTO log(ts, msg) VALUES(${Date.now()}, 'process'); COMMIT; EOF `; files.write("/sdcard/test/run.sh", script); exec("sh /sdcard/test/run.sh");这里用了heredoc(<<EOF)方式,把SQL整体传给sqlite3的标准输入。好处是不用再考虑SQL里引号转义,坏处是SQL中如果包含heredoc结束符EOF这种字符串,也会出问题,需要换一个特殊的分隔符。这个技巧在执行批量的初始化脚本时特别有用。
3.4 完整版:一个自动备份联系人信息的实战脚本
把以上几种方式结合起来,写一个稍微完整点的例子:假设你的App在/data/data/com.example/data.db存了用户表,你要每隔一段时间把数据导出到外部存储,并且把状态记录到另一个日志库。
"ui"; toast("开始执行备份"); const result = exec("sqlite3 /data/data/com.example/data.db \"SELECT * FROM user;\""); if (result.code !== 0) { toast("备份失败:" + result.error); return; } const lines = result.result.split("\n").filter(line => line.length > 0); const users = lines.map(line => { const cols = line.split("|"); return { id: cols[0], name: cols[1], age: cols[2] }; }); const logDb = "/sdcard/backup/log.db"; users.forEach(user => { exec(`sqlite3 ${logDb} "INSERT INTO backup_log(uid, name, backup_time) VALUES('${user.id}', '${user.name}', ${Date.now()});"`); }); // 还要把导出结果存成json,给别的脚本用 files.write("/sdcard/backup/users.json", JSON.stringify(users)); toast("备份完成,共 " + users.length + " 条记录");这个脚本把两种壳都用了:查询用的是独立的exec()进程,写入日志时反复调用exec(),如果数据量大建议改成new Shell()一次会话内批量操作。关键在于——整个流程都是在AutoJs的控制流里做,但数据库的实际读写全发生在shell层,互不干扰。
4. 权限、扩充与伪装:数据库操作的安全检查清单
“用Shell操作sqlite”比用内置API多出来的风险,几乎都来自于“它跨出了AutoJs沙箱”。一旦你的脚本变成独立进程操作文件,Android的权限模型、SELinux策略、文件锁竞争都会来凑热闹。下面列几个我实际被坑过的点。
4.1 权限检查和SELinux导致的诡异错误
有root权限的设备上,并不是root了就能读写任何文件。从Android 4.4开始,SELinux一直处于enforcing模式,除非你把它调成permissive或者干脆是设备默认关闭。
当你试图执行sqlite3 /data/data/com.target/db/database.db时,你会遇到unable to open database file。很多人第一反应是权限不够,实际上不是权限问题,是SELinux domain限制导致的。
遇到这个问题,先别急着chmod 777整个目录。优先方案是把数据库文件复制到应用自己的可写目录:
cp /data/data/com.target/db/database.db /sdcard/tmp/database.db chmod 644 /sdcard/tmp/database.db然后再用sqlite3操作这个副本。如果你确实需要直接无痕修改原文件,那就要把SELinux设置为宽松模式:
adb shell setenforce 0不过要注意,setenforce 0改成永久生效需要修改内核参数,重启就恢复,所以别指望一条命令能解决所有未来问题。更稳妥的软件开发方式是写一个带setenforce检查的开关开关,在脚本开头临时关闭,脚本结束再恢复setenforce 1。
4.2 数据库文件被AutoJs进程锁住的情况
内置SQLite模块打开数据库后,如果连接没有关闭,数据库文件会被锁住。此时再用Shell命令去写它,大概率会返回database is locked。
这个坑最气人的地方在于——你以为是你主动关掉了数据库,但实际上Java层持有的SQLiteDatabase对象可能还存在缓存连接,或者某个查询Cursor没关。解决方案是:
- 在调用Shell前,确保所有
db.close()执行了,最好再延时几百毫秒 - 如果还不行,直接用
adb shell kill杀掉AutoJs进程再执行 - 尽量不要在同一脚本里混合使用内置SQLite API和Shell方式操作同一个库文件,二选一
后者真的是血泪教训,混合使用容易产生文件锁冲突和WAL日志不一致的问题,轻则“数据库已损坏”,重则整个库直接打不开。
4.3 中文乱码和特殊字符的坑
sqlite3命令行工具的输出编码默认跟随当前locale。如果Android系统环境设置成了UTF-8,通常没问题;但某些定制ROM会强制使用GBK或者GB2312,这就麻烦了。
最稳妥的做法是给sqlite3命令强制指定编码,虽然sqlite3没有直接的编码参数,但它会按UTF-8输出。如果遇到乱码,我建议在AutoJs里做转码,先把字节当作UTF-8解析,乱码就转GBK再解析一遍。不过说实话,这种来回转码非常消耗性能,最省心的方案是root设备上通过mount -o remount,rw /system改系统的default.prop,把persist.sys.locale设置成正确的语言区。
其实多数情况下,SQL文本本身是UTF-8就不会有乱码问题。真正乱的是你从数据库读出来再显示的部分。AutoJs的日志面板对UTF-8支持很好,如果你发现log里显示乱码,多半是数据库里存的编码格式就不是UTF-8,或者在SQL里用了||连接字符串时被默认字符集影响了。
4.4 命令注入:SQL参数要防一手
AutoJs脚本里如果用字符串拼接的方式把变量塞进SQL,一旦变量里包含特殊字符(单引号、分号、注释符),就可能把命令搞坏。
// 不安全:用户输入直接拼进SQL const sql = "SELECT * FROM users WHERE name = '" + userInput + "'"; // 安全:去掉危险字符 function safeSql(value) { return value.replace(/'/g, "''").replace(/;/g, "").replace(/--/g, ""); } const sql = "SELECT * FROM users WHERE name = '" + safeSql(userInput) + "'";单引号替换成两个单引号是SQLite标准的转义方法,--是SQL注释符号,去掉能防一部分注入。但你要清楚,这只是在命令行环境里的防呆措施,不代表真正的安全。如果脚本要开放给别人用,建议还是走参数绑定的方式,只是命令行下确实没有内置API那么方便做绑定。
5. 索引、事务与临时表:Shell下如何做批量数据处理
操作单一记录,Shell和内置API差异不大。但当我们处理的是几万条甚至几十万条数据时,SQLite的写入性能差距会被放大到肉眼可见。我在平时做数据清洗脚本时,积累了几个针对Shell场景的提速习惯。
5.1 事务包裹批量写入
sqlite3默认是自动提交模式,每一条INSERT都是一次磁盘fsync。批量插入10万条记录时,这个开销会让人崩溃。
用heredoc方式跑一整段事务是效率最高的:
sqlite3 /sdcard/test/big.db <<EOF BEGIN; CREATE TABLE IF NOT EXISTS t(id INTEGER, val TEXT); -- 假设你生成了一堆INSERT语句 INSERT INTO t VALUES(1, 'aaa'); INSERT INTO t VALUES(2, 'bbb'); ... COMMIT; EOF注意SQLite对事务嵌套有特殊处理,Shell里不能直接嵌套BEGIN,你只需要保证整个dml段被一个事务包住就行。
5.2 没有索引的查询,数据量一大就等着发呆
如果你的数据是批量导入后再做查询,导入期间先不要建索引,导入完成后再一次性创建。道理很简单:每插入一行,SQLite都要更新一次索引B+树,代价极高。
# 先建表,不建索引,导入分批事务 sqlite3 data.db "CREATE TABLE IF NOT EXISTS raw_data(id INTEGER, val TEXT);" # 执行一堆批量insert... # 数据导完,最后建索引 sqlite3 data.db "CREATE INDEX IF NOT EXISTS idx_raw_id ON raw_data(id);"5.3 PRAGMA语句来一套
sqlite3命令行直接支持点命令和PRAGMA,建议在所有批量操作前执行下面这几个PRAGMA调优指令:
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA cache_size=-20000; PRAGMA temp_store=MEMORY;WAL模式的好处是读操作不会阻塞写操作,Shell多进程并发访问时能大幅减少database is locked的报错。synchronous=NORMAL在WAL模式下,只有checkpoint时才会fsync,写入性能提升非常明显,代价是断电时可能丢最近几帧的同步,但日志库里可接受。
5.4 临时表和内存库
有些数据清洗属于拆了重组,我会选择先把数据import到一个:memory:临时库,处理完再导出结果,而不是在一个大库里反复UPDATE。
sqlite3 :memory: <<EOF ATTACH DATABASE '/sdcard/source.db' AS src; ATTACH DATABASE '/sdcard/result.db' AS dst; CREATE TEMP TABLE tmp AS SELECT ... FROM src.main_table WHERE ...; INSERT INTO dst.result SELECT * FROM tmp; DETACH DATABASE src; DETACH DATABASE dst; EOF内存库不需要磁盘IO,数据量在百万级别以下,这个方式比在磁盘库里跑UPDATE要快一个数量级。而且ATTACH DATABASE可以同时挂多个库,跨库查询也能一条SQL搞定。AutoJs调用时把这些SQL塞进heredoc就行。
6. 和AutoJs内置SQLite的深度对比:什么时候选哪个
到此Shell方案的优劣势基本都露出来了。做张表给你一个直观的选择依据。
| 对比维度 | 内置SQLite模块 | Shell + sqlite3 |
|---|---|---|
| 使用便捷性 | 简单直接,API封装到位 | 需要自己处理命令行参数和解析输出 |
| 跨应用访问 | 几乎不可能 | root权限下可以读其他应用数据库 |
| 事务控制 | 有事务API,但需要显式调用 | 可以用heredoc批量跑一整个脚本,很方便 |
| 并发能力 | 受AutoJs进程锁限制 | 独立进程,可多进程同时读 |
| 错误信息可读性 | 异常堆栈比较友好 | 退回错误码和stderr,需要自己翻译 |
| 对锁冲突规避 | 内置库长时间占用时容易锁死 | 可以立即断开进程释放锁 |
| SQL方言支持 | 取决于内嵌SQLite版本 | 取决于系统命令版本,通常可以自己换busybox版本 |
| 适合场景 | 小型工具、脚本内部存储 | 数据库量大、跨应用、批量处理、外部运维 |
我的建议很简单:你是自用脚本、逻辑单一、只操作自己脚本私有目录下的库,那就用内置模块,省事;你的脚本要跑去读其他App的数据,或者你经常用adb shell手动查库、反复调整SQL,那就直接用shell操作方式,留一份.sh脚本,以后维护时一个命令行就能直接测SQL,不用打开AutoJs跑整套流程。
7. 几条摸爬滚打后的避坑心得
聊几个我在实际使用中踩过几次坑之后的体会。
第一,shell操作sqlite的一大禁忌是“换行符不干净”。从AutoJs的文本编辑框复制SQL到Shell里跑,经常会莫名其妙报语法错误,因为编辑框里每个换行实际上带了一个\r回车符,Windows的CRLF跟UNIX的LF混在一起了。你写SQL时最好全部在同一行内写完,或者用sed -i 's/\r$//'处理一下脚本文件。
第二,不要在一条exec()里塞一个特别大的SQL文件。命令行参数长度有限制,Android的exec()返回结构也有大小限制,超过几MB的输出会导致Shell截断。遇到超大查询结果,建议让sqlite3直接输出到文件:
sqlite3 data.db "SELECT * FROM huge_table;" > /sdcard/output.txt然后AutoJs只负责读取这个文件,不再走命令行输出通道。
第三,备份是底线。用Shell直接修改数据库的杀伤力巨大,一条错误的UPDATE可能瞬间刷掉几千行数据。我无论如何都要在改库前先执行一次.backup或者直接复制原始db文件,熟练之后这也只是多花半秒钟的事:
sqlite3 /sdcard/data.db ".backup '/sdcard/backup-before-change-时间戳.db'"比cp命令好在它生成的是一个可用的一致快照,不用担心中途写入导致的文件损坏。
第四,别忘了shell里默认没有echo这样的内置命令支持。经常有人掉进“我在shell脚本里写的echo $var不生效”的坑,是因为/system/bin/sh和bash的行为差异。Android的mksh环境下,变量引用不加引号很容易被分词,习惯性把所有变量都加上双引号是最保险的写法。
自动辅助脚本做多了之后会发现,AutoJs也好,sqlite3也好,都只是工具集里的零件。真正的生产力来自把这些零件组合起来的思路——什么时候用API,什么时候跨进程,什么时候该切到shell处理,什么时候用事务批量,这个判断力才是核心。希望这套“AutoJs + shell + sqlite3”的组合方案能在你的脚本工具箱里占个位置,少走点我走过的弯路。
本文还有配套的精品资源,点击获取