4、数据存储:将清洗后的数据存入本地SQLite数据库,设计表结构
数据清洗完了,下一步就是存起来。
你可能觉得,存数据嘛,不就是 insert 一下?其实没那么简单。表结构设计得好不好,直接影响你后面查数据、做报表、甚至上云同步的效率。我刚开始做物联网项目时,就吃过这个亏——表结构拍脑袋就定了,结果后面改得想哭。
4.1 为什么选 SQLite?
说实话,本地存储的选择不少。CSV、JSON、Excel 都能存。但为什么我坚持用 SQLite?
- 零配置:不需要装数据库服务,一个文件搞定
- 标准 SQL:你会的 SQL 语法,它基本都支持
- Python 内置支持:import sqlite3 就能用,不用装第三方库
- 轻量级:整个库才几百 KB,嵌入式设备也能跑
嗯,这里要注意一点:SQLite 不适合高并发写入场景。但咱们这个课程是单机采集、单机存储,完全够用。我在一个树莓派项目上跑过 SQLite,连续采集了三个月,稳得很。
4.2 表结构设计思路
设计表结构,说白了就是回答三个问题:
- 要存什么?
- 怎么查最快?
- 以后怎么扩展?
咱们的空气质量数据,核心字段包括:
- 采集时间(精确到秒)
- PM2.5 浓度
- PM10 浓度
- 温度
- 湿度
- 设备编号(如果有多个设备)
你想想看,如果以后要按天、按小时做统计,时间字段就特别重要。所以我习惯把时间设为主键的一部分,或者至少建个索引。
核心原则:宁可多花一分钟设计表结构,不要花一小时改数据。
4.3 建表 SQL 语句
直接上代码。这是我个人比较喜欢的一种设计:
CREATE TABLE IF NOT EXISTS air_quality (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL DEFAULT 'device_001',
record_time TEXT NOT NULL,
pm25 REAL,
pm10 REAL,
temperature REAL,
humidity REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(device_id, record_time)
);
解释一下几个关键点:
- id:自增主键,方便按行操作
- device_id:预留多设备场景,默认单设备
- record_time:采集时间,用 TEXT 类型存 ISO 格式字符串
- UNIQUE 约束:同一个设备同一时间只能有一条记录,防止重复插入
小技巧:record_time 用 TEXT 而不是 DATETIME 类型,是因为 SQLite 没有真正的日期时间类型。用 ISO 格式字符串(如 "2024-01-15 08:30:00")排序、比较都方便。
4.4 Python 实现:连接与建表
代码写起来其实很简单。我习惯把数据库操作封装成一个类,方便复用:
import sqlite3
import os
class AirQualityDB:
def __init__(self, db_path="air_quality.db"):
self.db_path = db_path
self.conn = None
self.cursor = None
def connect(self):
"""连接数据库,如果文件不存在会自动创建"""
self.conn = sqlite3.connect(self.db_path)
self.cursor = self.conn.cursor()
print(f"✅ 数据库连接成功:{self.db_path}")
def create_table(self):
"""创建空气质量数据表"""
sql = """
CREATE TABLE IF NOT EXISTS air_quality (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL DEFAULT 'device_001',
record_time TEXT NOT NULL,
pm25 REAL,
pm10 REAL,
temperature REAL,
humidity REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(device_id, record_time)
);
"""
self.cursor.execute(sql)
self.conn.commit()
print("✅ 数据表创建成功")
def close(self):
"""关闭数据库连接"""
if self.conn:
self.conn.close()
print("🔌 数据库连接已关闭")
# 使用示例
if __name__ == "__main__":
db = AirQualityDB("air_quality.db")
db.connect()
db.create_table()
db.close()
运行这段代码,你会在当前目录下看到 air_quality.db 文件。用 DB Browser for SQLite 打开看看,表结构一目了然。
注意:我曾经犯过一个低级错误——每次运行程序都重新建表,结果之前存的数据全没了。所以一定要用 IF NOT EXISTS,确保表已存在时不会覆盖。
4.5 插入数据:从清洗到入库
清洗完的数据怎么插进去?我一般这样写:
def insert_record(self, device_id, record_time, pm25, pm10, temp, humidity):
"""插入一条空气质量记录"""
sql = """
INSERT OR IGNORE INTO air_quality
(device_id, record_time, pm25, pm10, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?)
"""
try:
self.cursor.execute(sql, (device_id, record_time, pm25, pm10, temp, humidity))
self.conn.commit()
return True
except Exception as e:
print(f"❌ 插入失败:{e}")
return False
# 批量插入更高效
def insert_batch(self, records):
"""批量插入,records 是列表,每个元素是元组"""
sql = """
INSERT OR IGNORE INTO air_quality
(device_id, record_time, pm25, pm10, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?)
"""
try:
self.cursor.executemany(sql, records)
self.conn.commit()
print(f"✅ 批量插入 {len(records)} 条记录")
return True
except Exception as e:
print(f"❌ 批量插入失败:{e}")
return False
注意 INSERT OR IGNORE 这个写法。如果遇到重复的 (device_id, record_time),它不会报错,而是直接跳过。这在重复采集时特别有用。
4.6 验证数据是否存进去了
存完数据,总得看一眼吧?写个简单的查询:
def query_recent(self, limit=10):
"""查询最近 N 条记录"""
sql = "SELECT * FROM air_quality ORDER BY record_time DESC LIMIT ?"
self.cursor.execute(sql, (limit,))
rows = self.cursor.fetchall()
for row in rows:
print(row)
return rows
运行一下,你应该能看到类似这样的输出:
(1, 'device_001', '2024-01-15 08:30:00', 35.2, 68.5, 22.1, 45.3, '2024-01-15 08:30:05')
(2, 'device_001', '2024-01-15 08:31:00', 36.8, 70.1, 22.3, 44.9, '2024-01-15 08:31:05')
...
4.7 避坑指南
做数据存储这块,有几个坑我替你们踩过了:
| 常见问题 | 后果 | 解决方案 |
|---|---|---|
| 忘记 commit | 数据没写进去,程序退出就丢了 | 每次 insert 后记得 conn.commit() |
| 主键设计不合理 | 重复数据一堆,查起来头疼 | 用 UNIQUE 约束防重复 |
| 时间存成字符串但格式不统一 | 排序、过滤全乱套 | 统一用 ISO 格式:YYYY-MM-DD HH:MM:SS |
| 没有关闭连接 | 数据库文件被锁,其他程序打不开 | 用 with 语句或显式 close() |
我曾经在一个项目里,因为忘记 commit,跑了整整一天的数据全没了。从那以后,我每次写完 insert 都会下意识加一句 commit,都快成肌肉记忆了。
4.8 小结
这一章我们做了三件事:
- 设计了合理的表结构,预留了扩展空间
- 用 Python 的 sqlite3 模块连接并建表
- 实现了单条和批量插入,以及简单的查询验证
数据存进去了,下一步就是怎么把这些数据可视化出来,让人一眼看懂空气质量的变化趋势。下一章我们聊聊数据可视化的事。
课后练习:试着在表里加一个 city 字段,然后插入几条不同城市的数据,看看能不能按城市分组查询。