
简介这份资源是一份AutoJs项目模板源码主题为通过shell命令操作sqlite数据库面向具备一定AutoJs基础、希望扩展脚本数据存储能力的开发者与学习者。它解决的是在AutoJs环境中直接调用shell执行sqlite相关操作、实现本地数据读写与管理的需求适合作为脚本开发中的数据库操作参考范例。压缩包为7z格式仅含1个js文件体积约775B文件虽小但结构完整安装好AutoJs后可直接运行查看逻辑。目前已有123人学习关注说明该模板在AutoJs数据库操作方向具有一定参考价值。读者可从中获取shell与sqlite结合的调用思路、命令组织方式及脚本模板结构便于迁移到自己的自动化项目中同时需注意资源仅供学习参考不支持商业用途且需自行确认AutoJs版本兼容性。1. 从一次脚本数据丢失说起这个 AutoJs 源码到底解决什么问题写过 AutoJs 脚本的人大概都经历过这种崩溃脚本跑了一晚上采集了几千条数据结果第二天打开一看数据全没了。原因往往很简单——用files.write()或者storages存数据脚本一重启、一崩溃文件被覆盖或者写坏了连后悔药都没得吃。这个「AutoJs源码-用shell操作sqlite数据库」要解决的就是脚本里的数据持久化问题。它不走 AutoJs 自带的存储 API而是直接调用 Android 系统里的sqlite3命令行工具通过 shell 命令来建库、建表、增删改查。适合谁适合那些脚本需要长期保存结构化数据、需要按条件查询、或者数据量大到用 JSON 文件已经扛不住的开发者。说白了当你发现storages不够用的时候就该看看这份源码了。2. 技术选型拆解为什么是 shell sqlite3 而不是 AutoJs 自带存储2.1 AutoJs 自带存储的天花板在哪AutoJs 提供了storages和files两套存储方案。storages底层是 SharedPreferences适合存配置项键值对结构数据量一大就力不从心。files就是纯文本读写存 JSON 数组是常见做法但每次读写都要全量序列化和反序列化几千条数据就开始卡上万条直接让脚本变慢动作。更致命的是并发问题——脚本里多个线程同时写同一个文件数据损坏几乎是必然的。我见过太多脚本因为存储方案选错最后数据变成一堆乱码。2.2 shell 调用 sqlite3 的可行性Android 系统从很早就内置了sqlite3命令行工具位置通常在/system/bin/sqlite3。AutoJs 的shell()函数可以执行系统命令这就打通了一条路用 shell 调 sqlite3把 SQL 语句传进去结果通过标准输出拿回来。这样做的好处是绕开了 AutoJs 存储 API 的所有限制——数据库文件独立存在脚本崩溃不影响数据完整性支持标准 SQL复杂查询不在话下数据量到几万条依然流畅。代价是每次操作都要起一个 shell 进程有轻微性能开销但对于脚本场景完全够用。2.3 核心封装思路这份源码的核心思路是把 sqlite3 的命令行调用封装成几个常用函数让调用者感觉像在用普通的数据库 API。关键要解决三个问题——命令拼接时的引号转义、查询结果的解析、以及数据库文件的路径管理。下面这段是典型的封装结构// 数据库文件路径放在脚本同目录下 var DB_PATH files.join(files.cwd(), app_data.db); // 执行 SQL 的底层函数返回原始输出 function execSQL(sql) { // 用双引号包裹 SQL内部单引号需转义 var cmd sqlite3 DB_PATH \ sql.replace(//g, \\) \; var result shell(cmd, true); if (result.code ! 0) { console.error(SQL执行失败: result.error); return null; } return result.result; } // 建表如果不存在则创建 function initTable() { var sql CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP); execSQL(sql); }这里有几个细节值得说。shell(cmd, true)的第二个参数表示是否等待命令执行完成必须传true否则拿不到结果。SQL 语句用双引号包裹是因为 shell 里单引号内的内容不会被转义而 SQL 里的字符串常量通常用单引号这样刚好错开。result.code是退出码非零表示出错result.error里会有错误信息。建表时用IF NOT EXISTS是常见做法避免重复执行报错。2.4 查询结果的解析sqlite3 命令行默认输出是竖线分隔的表格形式解析起来需要一点技巧。更稳妥的做法是在 SQL 里用.mode命令指定输出格式// 查询并解析为 JSON 数组 function query(sql) { // 先设置输出模式为 list字段分隔符为 | var fullSql .mode list\n.separator |\n sql; var raw execSQL(fullSql); if (!raw) return []; var lines raw.trim().split(\n); var results []; for (var i 0; i lines.length; i) { if (lines[i].trim() ) continue; var fields lines[i].split(|); results.push({ id: fields[0], title: fields[1], content: fields[2], created_at: fields[3] }); } return results; }.mode list让每行一条记录.separator |指定字段分隔符。这样解析逻辑就很简单了——按行分割再按竖线分割。注意execSQL里对 SQL 做了双引号转义但这里传入的是多行命令实际使用时需要确认 shell 对换行符的处理。如果遇到问题可以把多条命令用分号连接写成一行。3. 跑起来从零搭建可运行的数据库操作环境3.1 环境确认与 sqlite3 可用性检测在写任何代码之前先确认设备上 sqlite3 能不能用。不同 Android 版本、不同 ROMsqlite3 的位置和权限可能不一样。新建一个脚本跑这段检测代码// 检测 sqlite3 是否可用 function checkSqlite() { var paths [/system/bin/sqlite3, /system/xbin/sqlite3, sqlite3]; for (var i 0; i paths.length; i) { var result shell(paths[i] -version, true); if (result.code 0) { console.log(找到 sqlite3: paths[i]); console.log(版本: result.result); return paths[i]; } } console.error(未找到 sqlite3请确认设备已 root 或系统自带); return null; } var SQLITE_PATH checkSqlite();如果三个路径都找不到说明设备环境不支持这份源码就跑不起来。常见情况是模拟器或者某些定制 ROM 移除了 sqlite3。真机上大部分 Android 7.0 以上系统都自带不需要 root。检测到路径后后续所有命令都用这个完整路径避免依赖 PATH 环境变量。3.2 数据库初始化与表结构设计确认 sqlite3 可用后第一步是建库建表。数据库文件建议放在脚本目录下方便管理和备份// 初始化数据库和表 function initDatabase() { // 建表语句 var createTable CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT, tag TEXT DEFAULT default, created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) ); // 执行建表 var result shell(SQLITE_PATH DB_PATH \ createTable \, true); if (result.code 0) { console.log(数据库初始化完成); } else { console.error(建表失败: result.error); } // 创建索引加速按标签查询 var createIndex CREATE INDEX IF NOT EXISTS idx_tag ON notes(tag); shell(SQLITE_PATH DB_PATH \ createIndex \, true); }表结构设计上id用自增主键created_at和updated_at用datetime(now, localtime)自动填充本地时间。tag字段加索引因为按标签筛选是高频操作。这里有个坑SQLite 的datetime函数默认返回 UTC 时间必须加localtime参数才是本地时间否则你会发现存进去的时间比实际少了 8 小时。3.3 增删改查的完整实现有了表之后把四个基本操作封装成函数。插入操作要注意字符串里的单引号转义// 插入一条记录 function insertNote(title, body, tag) { // 转义单引号防止 SQL 注入和语法错误 var safeTitle title.replace(//g, ); var safeBody body.replace(//g, ); var safeTag (tag || default).replace(//g, ); var sql INSERT INTO notes (title, body, tag) VALUES ( safeTitle , safeBody , safeTag ); var result shell(SQLITE_PATH DB_PATH \ sql \, true); if (result.code 0) { // 获取刚插入的 ID var idResult shell(SQLITE_PATH DB_PATH \SELECT last_insert_rowid()\, true); return parseInt(idResult.result.trim()); } return -1; } // 按标签查询 function getNotesByTag(tag) { var safeTag tag.replace(//g, ); var sql SELECT id, title, body, tag, created_at FROM notes WHERE tag safeTag ORDER BY created_at DESC; var raw shell(SQLITE_PATH DB_PATH \.mode list\ \.separator |\ \ sql \, true); if (result.code ! 0 || !raw.result) return []; var lines raw.result.trim().split(\n); var notes []; for (var i 0; i lines.length; i) { if (!lines[i].trim()) continue; var f lines[i].split(|); notes.push({ id: parseInt(f[0]), title: f[1], body: f[2], tag: f[3], created_at: f[4] }); } return notes; }插入时把单引号替换成两个单引号这是 SQL 标准转义方式。last_insert_rowid()返回刚插入行的 ID方便后续关联操作。查询时用.mode list和.separator |控制输出格式解析逻辑就变得很直接。注意这里把多个点命令和 SQL 语句放在同一个 shell 调用里sqlite3 会按顺序执行。3.4 更新与删除操作更新和删除逻辑类似关键是 WHERE 条件要写对否则会误伤全表// 更新记录 function updateNote(id, title, body) { var safeTitle title.replace(//g, ); var safeBody body.replace(//g, ); var sql UPDATE notes SET title safeTitle , body safeBody , updated_atdatetime(now,localtime) WHERE id parseInt(id); var result shell(SQLITE_PATH DB_PATH \ sql \, true); return result.code 0; } // 删除记录 function deleteNote(id) { var sql DELETE FROM notes WHERE id parseInt(id); var result shell(SQLITE_PATH DB_PATH \ sql \, true); return result.code 0; }parseInt(id)是防御性写法确保传入的是数字避免拼接出奇怪的 SQL。更新时自动刷新updated_at字段。删除操作只按主键删如果要按条件批量删建议先查询确认再执行避免误删。4. 避坑指南shell 操作 sqlite 的五个血泪教训4.1 引号嵌套导致 SQL 执行失败现象明明 SQL 语句没问题shell 返回退出码 1错误信息是unrecognized token或者near ...。原因shell 的单引号、双引号、SQL 自身的引号三层嵌套转义没处理好。解决统一用双引号包裹 SQLSQL 内部字符串用单引号插入数据时把单引号替换成两个单引号。如果 SQL 里必须用双引号比如列名就改成用\转义。4.2 数据库文件路径含空格或中文现象建库成功但后续操作报unable to open database file。原因数据库路径里有空格或中文字符shell 解析时被截断。解决路径用单引号包裹并且尽量把数据库文件放在纯英文、无空格的目录下。AutoJs 的files.cwd()返回的路径通常没问题但如果你手动指定了外部存储路径要特别注意。4.3 并发写入导致 database is locked现象脚本里多个线程同时写数据库偶尔报database is locked。原因SQLite 默认的锁机制在并发写入时会阻塞shell 调用是同步的但多个 shell 进程同时操作同一个库文件就会冲突。解决在脚本层面加一个简单的锁用threads.lock()或者自己维护一个标志位确保同一时间只有一个写操作。读操作可以并发但写操作必须串行。4.4 查询结果为空时的解析异常现象查询没有匹配记录时raw.result是空字符串直接split(\n)会得到[]解析后产生一条空记录。原因没有对空结果做判断。解决在解析前先判断raw.result.trim() 是则直接返回空数组。这个坑很隐蔽因为不报错只是数据里多了一条空记录排查起来费时间。4.5 忘记关闭 shell 进程导致资源泄漏现象脚本运行一段时间后变卡或者系统提示应用无响应。原因每次shell()调用都会起一个进程虽然 AutoJs 会自动回收但高频调用时回收不及时。解决批量操作时尽量合并 SQL用事务一次性提交多条语句减少 shell 调用次数。比如插入 100 条数据不要循环调 100 次 shell而是拼成一条INSERT INTO ... VALUES (...), (...), ...批量插入。5. 二次开发把这份源码改造成你自己的数据层5.1 封装成可复用的模块这份源码最直接的改造方向是把它封装成一个独立的模块文件比如db.js然后在其他脚本里require进来。封装时把数据库路径、表名、字段映射都做成可配置的参数// db.js - 可配置的数据库模块 module.exports function(options) { var dbPath options.path || files.join(files.cwd(), default.db); var tableName options.table || records; var sqlitePath options.sqlite || /system/bin/sqlite3; // 内部函数使用闭包访问配置 function exec(sql) { var cmd sqlitePath dbPath \ sql.replace(//g, \\) \; return shell(cmd, true); } return { exec: exec, query: function(sql) { /* 解析逻辑 */ }, insert: function(data) { /* 插入逻辑 */ }, update: function(id, data) { /* 更新逻辑 */ }, remove: function(id) { /* 删除逻辑 */ } }; };这样改造后不同脚本可以共用同一套数据库操作代码只是传入不同的配置。表名和字段映射做成配置项换项目时不用改核心逻辑。5.2 增加事务支持批量操作时事务能大幅提升性能。SQLite 默认每条语句自动提交100 条插入就是 100 次磁盘写入。用BEGIN和COMMIT包裹后只写一次磁盘// 批量插入使用事务 function batchInsert(records) { var values []; for (var i 0; i records.length; i) { var r records[i]; values.push(( r.title.replace(//g, ) , r.body.replace(//g, ) )); } var sql BEGIN; INSERT INTO notes (title, body) VALUES values.join(,) ; COMMIT;; var result shell(SQLITE_PATH DB_PATH \ sql \, true); return result.code 0; }注意事务里的 SQL 如果有一条失败整个事务会回滚这是我们需要的行为。但错误信息可能只显示第一条失败的原因排查时要逐条检查数据格式。5.3 数据导出与备份数据库文件本身就是一个文件备份很简单——直接复制.db文件即可。但运行中复制可能拿到不完整的数据稳妥做法是先用 sqlite3 的.dump命令导出 SQL 文本// 导出数据库为 SQL 文本 function exportDatabase() { var result shell(SQLITE_PATH DB_PATH .dump, true); if (result.code 0) { var backupPath files.join(files.cwd(), backup_ Date.now() .sql); files.write(backupPath, result.result); console.log(备份完成: backupPath); return backupPath; } return null; }.dump输出的是完整的 SQL 语句包含建表和所有数据。恢复时用sqlite3 new.db backup.sql即可。这个技巧我每次做数据迁移都会用比直接复制文件可靠得多。5.4 一个容易忽略的细节数据库版本管理二次开发时表结构可能会变。比如一开始只有title和body后来要加status字段。直接ALTER TABLE加字段是常见做法但老版本脚本可能不认识新字段。建议在数据库里存一个版本号脚本启动时检查并执行对应的迁移脚本// 简单的版本管理 function migrate() { var verResult shell(SQLITE_PATH DB_PATH \PRAGMA user_version\, true); var currentVer parseInt(verResult.result.trim()) || 0; if (currentVer 1) { exec(ALTER TABLE notes ADD COLUMN status INTEGER DEFAULT 0); exec(PRAGMA user_version 1); } // 后续版本继续追加 }PRAGMA user_version是 SQLite 内置的版本标记不占用表空间读写都很快。每次改表结构就递增版本号脚本启动时自动迁移。这个习惯让我在多次迭代中从没丢过用户数据。从那以后我每次写涉及数据持久化的脚本都会先跑一遍checkSqlite()确认环境再走一遍migrate()确认表结构最后才执行业务逻辑。这套流程帮我避开了至少三次数据丢失事故。希望帮到你。本文还有配套的精品资源点击获取