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;
        }
    }
}
⚠️ 注意:不要每条数据都flush。磁盘IO很慢,频繁写入会掉帧。我建议100-200ms刷一次,数据不会丢太多,性能也扛得住。

文件存储位置

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文件的完整数据流:

传感器数据写入CSV文件流程图 加速度传感器 每秒50-200次回调 onSensorChanged 提取ax, ay, az CsvLogger writeSensorData() CSV文件 acc_20240101.csv 每100ms flush一次 数据从传感器 → 回调 → 写入缓冲区 → 定时刷入磁盘

避坑指南

我曾经在项目里犯过一个低级错误:在传感器回调里直接new FileWriter,每次回调都打开关闭文件。结果手机烫得能煎鸡蛋,App直接ANR。

后来学乖了,用单例的BufferedWriter,只在开始和结束时操作文件。中间只往缓冲区写,定时刷盘。

还有一次,我忘了在onPause里关闭文件,结果数据全丢了。嗯,文件流一定要在生命周期结束时关闭,这是血泪教训。

数据验证小技巧

写完CSV后,怎么确认数据是对的?我一般做三件事:

  1. 看文件大小:采集10秒,文件应该有几KB到几十KB。太小说明没写进去
  2. 用Excel打开:检查表头对不对,数据有没有错位
  3. 画个折线图:把加速度数据画出来,看看波形是否合理。静止时z轴应该在9.8左右,x和y接近0

如果发现数据全是0或者重复值,八成是传感器没注册成功,或者回调里取值写错了。

核心要点:

  • CSV格式简单通用,适合传感器数据存储
  • 用BufferedWriter批量写入,避免频繁IO
  • 每100ms刷一次盘,平衡性能和实时性
  • 文件名加时间戳,方便管理多次采集的数据
  • 记得在生命周期结束时关闭文件流

数据存下来了,下一步就是分析。你可以把CSV拷到电脑上,用Python的pandas库做FFT分析,或者用Excel画个趋势图。这些数据就是你调试算法的依据,存好了事半功倍。


公众号:蓝海资料掘金营,微信deep3321