Learning Objectives
- 寫出定時讀取感測器、加上時間戳記的記錄程式,並說明取樣間隔怎麼選
- 用 csv 模組把資料寫成 CSV 檔,知道附加模式與 newline="" 的用途
- 用 sqlite3 建立資料表、以參數化查詢寫入,再用 SQL 算出平均與最大值
- 比較 CSV 與 SQLite 的適用情境
只看現在的數字還不夠
前面幾課的程式讀到溫度就印出來,程式一停,數字就不見了。可是很多問題要看一段時間的變化才回答得了:
- 教室下午三點是不是特別熱?冷氣開了多久才降下來?
- 光線在一天裡怎麼變?什麼時候該開燈?
這一課把感測器的讀值定時記錄下來:每一筆都有時間、有數值,存成檔案或資料庫,之後可以畫圖、統計、和別人分享。
記錄程式的三個決定
| 決定 | 要想的事 |
|---|---|
| 取樣間隔 | 溫度變化慢,每分鐘一次通常就夠;按鈕、震動變化快,要密很多。太密會產生大量資料,太疏會漏掉變化 |
| 時間戳記 | 每一筆都要記下時間。建議記 UTC(世界協調時間),顯示時再換成當地時間,跨時區、日光節約時間都不會錯亂 |
| 存在哪裡 | 簡單的表格用 CSV 檔;資料多、要查詢與彙總用 SQLite 資料庫 |
本課的感測器沿用前幾課:I2C 溫度感測器 TMP102(位址 0x48,第 09 課)與接在 MCP3008 上的光敏電阻(第 08 課)。
CSV:最通用的表格檔
CSV(Comma-Separated Values)是純文字檔,一列一筆資料,欄位用逗號分開;第一列通常放欄位名稱。 試算表、資料分析工具、幾乎所有程式語言都讀得懂。
time_utc,temp_c,light
2026-01-01T00:00:00Z,26.5,0.62
2026-01-01T00:00:01Z,26.81,0.55
Python 內建的 csv 模組幫你處理逗號與引號(例如欄位內容本身有逗號時要加引號),不用自己組字串。兩個細節:
- 開檔時加
newline="":讓csv模組自己決定換行,在 Windows 上才不會多出空白列(Python 官方文件的建議寫法)。 - 時間寫成
2026-01-01T00:00:00Z這種 ISO 8601 格式,最後的Z表示 UTC;照字母排序就是照時間排序。
time.gmtime(time.time()) 把「從 1970 年起算的秒數」換成 UTC 的年月日時分秒,time.strftime 再排成字串。
模擬器的時鐘從 2026-01-01 00:00:00 UTC 開始,所以範例裡的時間都是這一天。
逐步看:每秒取樣一次,寫成 CSV
上方的電路畫面有溫度感測器與光敏電阻,數值會隨時間慢慢變。注意第 19 行讓虛擬時間前進 1 秒;程式最後把寫好的檔案讀出來印。
長時間記錄:每一筆都寫進檔案
上一個範例寫完 3 筆才關檔。真正的記錄程式會一直跑好幾天,這時候要考慮突然斷電: 寫入的資料可能還在記憶體的緩衝區,沒有真的寫到 SD 卡上,斷電就沒了。
比較穩健的寫法是每一筆都用附加模式 "a" 打開、寫一列、關閉:
while True:
row = [時間, 溫度]
with open("log.csv", "a", newline="", encoding="utf-8") as f:
csv.writer(f).writerow(row)
time.sleep(60)
| 模式 | 檔案已經存在時 |
|---|---|
"w" |
清空,從頭寫(重新開機後跑一次,舊資料就全部不見) |
"a" |
接在最後面繼續寫(舊資料保留) |
每一筆開關一次檔案,對每分鐘幾筆的記錄來說負擔很小,換來的是斷電時最多只少最後一筆。 下面的模擬器就用這種寫法。
小測驗:CSV 與取樣
下面的程式執行後會印出什麼?
import csv, io
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(["t", "temp"])
w.writerow(["08:00", 26.5])
w.writerow(["08:01", 27])
print(len(buf.getvalue().splitlines()))
每 30 秒記錄一次溫度,連續記錄一整天,大約會有幾筆?
為什麼建議時間戳記存成 UTC,而不是直接存當地時間?
SQLite:一個檔案就是一個資料庫
資料一多,CSV 就不方便了:想找「溫度超過 27 度的時間」或「每小時的平均」,要把整個檔案讀進來自己算。
SQLite 是一個不需要伺服器的資料庫:整個資料庫就是一個檔案(例如 env.db),Python 內建 sqlite3 模組可以直接用。
| 步驟 | 程式 |
|---|---|
| 打開(不存在就建立) | db = sqlite3.connect("env.db") |
| 建立資料表 | db.execute("CREATE TABLE IF NOT EXISTS readings (ts INTEGER, temp_c REAL)") |
| 寫入一筆 | db.execute("INSERT INTO readings VALUES (?, ?)", (時間, 溫度)) |
| 確定寫入 | db.commit() |
| 查詢 | db.execute("SELECT AVG(temp_c) FROM readings").fetchone() |
參數化查詢:值一律用 ? 佔位、另外傳一個元組,不要把值用字串拼進 SQL。
如果值來自網頁表單或其他裝置(下一課的網頁就會),拼字串會讓別人有機會改寫你的 SQL(SQL 注入)。
下面的範例為了在瀏覽器裡執行,把資料庫開在記憶體(":memory:"),程式結束就消失;在樹莓派上改成檔名就會存成檔案。
時間存成整數秒(int(time.time()),從 1970 年起算的 UTC 秒數),比較大小與排序都很方便。
逐步看:寫進 SQLite,再用 SQL 彙總
第 14~15 行是一個敘述(括號沒關之前可以換行)。看第 19 行一個 SQL 就同時算出筆數、平均與最大值;第 21 行用 ? 帶入查詢條件。
CSV 還是 SQLite?
| CSV | SQLite | |
|---|---|---|
| 檔案內容 | 純文字,用記事本就看得懂 | 二進位檔,要用程式或工具開 |
| 給別人用 | 直接用試算表打開 | 要會 SQL,或匯出成 CSV |
| 查詢與彙總 | 自己讀檔、自己算 | 一個 SQL 敘述就算好 |
| 資料量大時 | 越來越慢 | 有索引,查詢仍然快 |
| 多個程式同時讀寫 | 容易互相干擾 | 資料庫處理鎖定,比較安全 |
常見的做法是兩者並用:樹莓派上用 SQLite 記錄,需要分享時再匯出成 CSV。 第 18 課的教室環境監測站會用到這兩種格式。
小測驗:SQLite
下面的程式會印出什麼?
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE r (temp REAL)")
for t in [26.0, 28.0, 30.0]:
db.execute("INSERT INTO r VALUES (?)", (t,))
print(db.execute("SELECT COUNT(*), AVG(temp) FROM r WHERE temp > ?", (27,)).fetchone())
下面哪一種寫法是安全的 SQLite 寫入方式?
寫入幾筆資料後忘了呼叫 db.commit() 就直接關閉連線,會怎樣?
重點整理
- 記錄程式要決定三件事:取樣間隔、時間戳記(建議 UTC)、存在哪裡。
- CSV 用
csv.writer寫,開檔加newline="";長時間記錄用附加模式"a",每一筆開關一次檔案,斷電時損失最少。 - SQLite 一個檔案就是一個資料庫:
connect、CREATE TABLE IF NOT EXISTS、INSERT加?參數、commit、SELECT彙總。 - 值一律用參數傳入,不要拼進 SQL 字串。
- CSV 方便分享與檢視,SQLite 方便查詢與彙總,常常兩者並用。
下一課用 Flask 做一個網頁,讓同一個網路上的手機或電腦可以控制 LED、看感測器的數值。