如何在JSON文件中修改数组?Rust下交易记录追加优化问询
问题:如何在Rust中向JSON文件的数组追加记录而不全量覆盖?
背景与现有实现
我正在通过编写简易记账程序学习Rust,选择本地JSON文件存储数据,其结构如下:
{ "wallet": "wallet1", "balance": 2000.0, "transactions": [ { "tr_type": "Withdraw", "date": 20230522, "volume": 300.0 }, { "tr_type": "Deposit", "date": 20230422, "volume": 150.0 }, { "tr_type": "Withdraw", "date": 20230222, "volume": 300.0 }, { "tr_type": "Deposit", "date": 20230707, "volume": 200.0 } ] }
程序需要读取文件数据进行计算展示,并接收用户新增交易记录写入JSON文件。当前实现代码如下:
use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::prelude::*; #[derive(Serialize, Deserialize, Debug)] struct Transaction { tr_type: TRtype, date: u64, volume: f32, } #[derive(Serialize, Deserialize, Debug)] struct Account { wallet: String, balance: f32, transactions: Vec<Transaction>, } #[derive(Serialize, Deserialize, Debug)] enum TRtype { Deposit, Withdraw, } fn main() -> std::io::Result<()> { let mut myfile = OpenOptions::new() .read(true) .write(true) .create(true) .open("wallets.json")?; let mut raw_json = String::new(); myfile.read_to_string(&mut raw_json)?; let mut deserialized: Account = serde_json::from_str(&raw_json)?; for trx in &deserialized.transactions { println!( "transaction: {:?}, volume = {}, date = {}", trx.tr_type, trx.volume, trx.date ); } let new_transaction: Transaction = Transaction { tr_type: TRtype::Deposit, date: 20230707, volume: 1500.0, }; deserialized.transactions.push(new_transaction); let serialized = serde_json::to_string(&deserialized).unwrap(); myfile.rewind().unwrap(); myfile.write_all(serialized.as_bytes())?; Ok(()) }
当前实现每次新增交易都会全量覆盖整个JSON文件,不够优雅。请问是否存在方法,可直接访问文件中的transactions数组并追加记录,而不改动其他内容?
解决方案
方法1:手动修改文件末尾(轻量但依赖JSON格式)
要直接追加数组元素,核心是修改JSON文件的末尾部分:原文件以]}结尾,我们需要将其替换为, 新交易对象]}。这种方法不需要全量解析和序列化,但依赖JSON文件的格式化规则(比如末尾无多余空格、数组闭合后直接跟对象闭合)。
实现步骤:
- 打开文件并获取文件大小
- 从文件末尾向前读取,找到
]}的位置 - 截断文件到该位置(去掉原有的
]}) - 根据数组是否为空,写入逗号(如果已有元素)和新序列化的交易对象
- 写入
]}闭合JSON
对应的Rust代码示例:
use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; #[derive(Serialize, Deserialize, Debug)] struct Transaction { tr_type: TRtype, date: u64, volume: f32, } #[derive(Serialize, Deserialize, Debug)] enum TRtype { Deposit, Withdraw, } fn append_transaction(new_trx: &Transaction) -> std::io::Result<()> { let mut file = OpenOptions::new() .read(true) .write(true) .create(true) .open("wallets.json")?; let file_size = file.metadata()?.len(); if file_size == 0 { // 文件为空,初始化完整的Account结构 let account = serde_json::json!({ "wallet": "wallet1", "balance": 2000.0, "transactions": [new_trx] }); let serialized = serde_json::to_string_pretty(&account)?; file.write_all(serialized.as_bytes())?; return Ok(()); } // 从末尾向前查找"]}"的位置 let mut buffer = [0; 2]; let mut pos = file_size; let mut found = false; while pos >= 2 { pos -= 2; file.seek(SeekFrom::Start(pos))?; file.read_exact(&mut buffer)?; if buffer == b"]}" { found = true; break; } // 回退1字节,处理可能的空格或换行(这里简化处理) pos += 1; } if !found { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid JSON format: cannot find transactions array end", )); } // 截断文件到"]}"的起始位置 file.set_len(pos)?; file.seek(SeekFrom::Start(pos))?; // 检查数组是否为空 file.seek(SeekFrom::Start(pos - 1))?; let mut char_buf = [0; 1]; file.read_exact(&mut char_buf)?; if char_buf != b"[" { // 已有元素,先写逗号 file.write_all(b", ")?; } // 写入新交易 let serialized_trx = serde_json::to_string(new_trx)?; file.write_all(serialized_trx.as_bytes())?; // 闭合数组和对象 file.write_all(b"]}")?; Ok(()) } fn main() -> std::io::Result<()> { let new_transaction = Transaction { tr_type: TRtype::Deposit, date: 20230708, volume: 1500.0, }; append_transaction(&new_transaction)?; println!("Transaction appended successfully"); Ok(()) }
注意事项:
- 该代码假设JSON格式规范,若文件是美化后的格式(比如数组闭合前有换行/空格),需要额外处理格式字符。
- 存在并发修改风险,若有其他进程同时操作文件,可能导致文件损坏,建议配合文件锁使用。
- 如果需要同步更新
balance字段,这种方法无法关联处理,此时全量覆盖反而更可靠。
方法2:使用流式JSON处理库(更健壮)
如果需要更健壮的解决方案,可以使用支持流式修改JSON的库,比如通过serde_json的低级流式API定位到transactions数组后直接追加元素,或者使用json-patch类库实现增量修改。
不过这种方式实现复杂度较高,对于小型记账程序而言,全量覆盖是更简单可靠的选择——除非JSON文件体积达到GB级,否则全量序列化和写入的性能影响可以忽略。
内容的提问来源于stack exchange,提问作者mehdi
相关产品推荐
相关产品推荐

