22、保存数据:将传感器数据写入CSV文件用于分析
数据采集完了,光在屏幕上看看数字可不行。你得把数据存下来,才能做后续分析。我个人习惯用CSV格式——简单、通用,Excel能打开,Python也能读。
说白了,CSV就是逗号分隔的文本文件。每一行是一条记录,每一列是一个字段。对于加速度传感器来说,时间戳、x轴、y轴、z轴,正好四列。
为什么选CSV?
你想想看,我们做传感器实验,数据量不会太大。几万条记录,CSV完全扛得住。而且它不需要额外库,纯文本操作,Android原生支持。
我在项目中遇到过用SQLite存传感器数据的场景,后来发现分析时还得导出成CSV,多此一举。除非你要做实时查询,否则CSV就是最省事的方案。
CSV文件的基本结构
一个标准的CSV文件长这样:
timestamp,ax,ay,az
1623456789000,0.12,-0.34,9.81
1623456789010,0.15,-0.33,9.80
1623456789020,0.11,-0.35,9.82
第一行是表头,后面是数据。每行末尾不要有多余的逗号,不然Excel打开会多一列空数据。
Android中写入CSV的代码实现
嗯,这里要注意:文件操作不能在主线程做。传感器回调频率很高,每秒几十上百次,直接写文件会卡UI。
我建议用BufferedWriter配合StringBuilder,批量写入。来看代码:
public class CsvLogger {
private BufferedWriter writer;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault());
public boolean openFile(String filePath) {
try {
File file = new File(filePath);
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true), "UTF-8"));
// 写入表头
writer.write("timestamp,datetime,ax,ay,az");
writer.newLine();
writer.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public void writeSensorData(long timestamp, float ax, float ay, float az) {
if (writer == null) return;
try {
StringBuilder sb = new StringBuilder();
sb.append(timestamp).append(",");
sb.append(sdf.format(new Date(timestamp))).append(",");
sb.append(String.format(Locale.US, "%.4f", ax)).append(",");
sb.append(String.format(Locale.US, "%.4f", ay)).append(",");
sb.append(String.format(Locale.US, "%.4f", az));
writer.write(sb.toString());
writer.newLine();
} catch (IOException e) {
e.printStackTrace();
}
}
public void flush() {
try {
if (writer != null) writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public void close() {
try {
if (writer != null) {
writer.flush();
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这段代码有几个关键点:
- UTF-8编码:避免中文乱码,虽然我们没写中文,但时间格式里可能有中文环境问题
- 追加模式:
FileOutputStream(file, true),这样不会覆盖已有数据 - 格式化浮点数:保留4位小数,够用了。太多小数位文件会很大
- 时间戳双保险:既存毫秒值,又存可读时间,方便不同场景查看
在传感器回调中使用
你需要在onSensorChanged里调用写入方法。但注意频率控制:
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
float ax = event.values[0];
float ay = event.values[1];
float az = event.values[2];
long timestamp = System.currentTimeMillis();
// 写入CSV
csvLogger.writeSensorData(timestamp, ax, ay, az);
// 每100ms刷一次盘,避免频繁IO
if (timestamp - lastFlushTime > 100) {
csvLogger.flush();
lastFlushTime = timestamp;
}
}
}
文件存储位置
Android有几种存法:
| 存储位置 | 路径示例 | 特点 |
|---|---|---|
| 应用内部存储 | context.getFilesDir() + "/sensor_data.csv" |
不需要权限,但其他应用看不到 |
| 外部存储(私有) | context.getExternalFilesDir(null) + "/sensor_data.csv" |
Android 10以上不需要权限,方便用文件管理器查看 |
| 外部存储(公共) | Environment.getExternalStorageDirectory() + "/sensor_data.csv" |
需要WRITE_EXTERNAL_STORAGE权限,不推荐 |
我个人推荐第二种。Android 10开始分区存储,用getExternalFilesDir最省心,既不需要申请权限,又能用电脑连USB拷出来。
数据采集的完整流程
把上面这些串起来,整个流程是这样的:
// 1. 初始化
CsvLogger logger = new CsvLogger();
String filePath = context.getExternalFilesDir("sensor") + "/acc_" + System.currentTimeMillis() + ".csv";
logger.openFile(filePath);
// 2. 注册传感器
SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sm.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
// 3. 在onSensorChanged中写入数据(见上面代码)
// 4. 停止采集时关闭文件
sm.unregisterListener(this);
logger.close();
acc_20240101_143022.csv。这样多次采集不会覆盖,而且一看文件名就知道是什么时候的数据。
用SVG看数据流向
下面这张图展示了从传感器到CSV文件的完整数据流:
避坑指南
我曾经在项目里犯过一个低级错误:在传感器回调里直接new FileWriter,每次回调都打开关闭文件。结果手机烫得能煎鸡蛋,App直接ANR。
后来学乖了,用单例的BufferedWriter,只在开始和结束时操作文件。中间只往缓冲区写,定时刷盘。
还有一次,我忘了在onPause里关闭文件,结果数据全丢了。嗯,文件流一定要在生命周期结束时关闭,这是血泪教训。
数据验证小技巧
写完CSV后,怎么确认数据是对的?我一般做三件事:
- 看文件大小:采集10秒,文件应该有几KB到几十KB。太小说明没写进去
- 用Excel打开:检查表头对不对,数据有没有错位
- 画个折线图:把加速度数据画出来,看看波形是否合理。静止时z轴应该在9.8左右,x和y接近0
如果发现数据全是0或者重复值,八成是传感器没注册成功,或者回调里取值写错了。
核心要点:
- CSV格式简单通用,适合传感器数据存储
- 用BufferedWriter批量写入,避免频繁IO
- 每100ms刷一次盘,平衡性能和实时性
- 文件名加时间戳,方便管理多次采集的数据
- 记得在生命周期结束时关闭文件流
数据存下来了,下一步就是分析。你可以把CSV拷到电脑上,用Python的pandas库做FFT分析,或者用Excel画个趋势图。这些数据就是你调试算法的依据,存好了事半功倍。