2026年8月31日 星期一

Python一下:保存地方記憶——安全讀寫UTF-8文字檔

《Python一下:從風土資料到智慧生活》第 17 篇

保存地方記憶——安全讀寫UTF-8文字檔

從原始逐字稿、講述者確認稿到巡查日誌,讓每一份文字都有來源、版本與安全邊界。

CH10 文字檔with open口述歷史
學習目標
完成本篇後,你能使用 with open() 及不同模式讀寫文字檔,逐行處理大型資料,正確處理UTF-8與換行,安全追加巡查日誌,並以新檔案保存修訂版本而不覆蓋原始資料。

一、文字檔不只是「開啟與儲存」

水井村USR可能保存長者訪談、台語逐字稿、導覽文字、學生反思與設備巡查日誌。程式寫得出檔案不代表資料管理正確:原始稿是否保留?誰確認過?能否公開?都是教材的一部分。

四項原則:原始檔唯讀保存;整理稿另存新檔;公開前取得講述者或權利人確認;備份與權限控管不可只依賴程式。範例皆使用教學資料,不應寫入真實姓名、電話、精確位置或設備憑證。

二、open()的基本結構

from pathlib import Path

path = Path("地方故事.txt")

with path.open("w", encoding="utf-8") as file:
    file.write("水井三寶:白馬、烏龜、姻緣花\n")

with path.open("r", encoding="utf-8") as file:
    content = file.read()

print(content)

with 區塊結束後會自動關閉檔案,即使讀寫時發生例外,也比手動呼叫 close() 安全。

三、選對檔案模式

模式用途風險或特性
r讀取檔案不存在會出錯
w寫入會清空並覆蓋既有檔案
a追加內容寫在檔案尾端
x排他建立檔案已存在就拒絕寫入
r+讀寫游標位置較複雜,初學時慎用
USR資料建議:建立新版本優先使用 x;巡查日誌可用 a;原始逐字稿只用 r。除非已備份且明確確認,不要對重要資料使用 w

四、拒絕覆蓋原始逐字稿

from pathlib import Path


def create_new_text(path, content):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("x", encoding="utf-8", newline="") as file:
        file.write(content)
    return path


target = Path("逐字稿") / "水井三寶_整理稿_v1.txt"
try:
    created = create_new_text(target, "教學用整理稿\n")
except FileExistsError:
    print("檔案已存在,請建立新版本名稱。")
else:
    print("已建立:", created)

五、一次讀完與逐行讀取

from pathlib import Path

path = Path("地方故事.txt")

with path.open("r", encoding="utf-8") as file:
    for line_number, line in enumerate(file, start=1):
        cleaned = line.rstrip("\n")
        print(line_number, cleaned)

read() 適合小檔案;直接迭代檔案物件能逐行處理大型日誌。rstrip("\n") 只移除行尾換行,比不加參數的 strip() 更能保留有意義的前後空白。

六、readline()與readlines()

from pathlib import Path

path = Path("地方故事.txt")

with path.open("r", encoding="utf-8") as file:
    first_line = file.readline()
    remaining_lines = file.readlines()

print("第一行:", first_line.rstrip("\n"))
print("其餘行數:", len(remaining_lines))

readline() 每次讀一行;readlines() 會把剩餘內容放入串列,大檔案可能占用較多記憶體。

七、安全追加智慧養殖巡查日誌

from datetime import datetime
from pathlib import Path


def append_patrol_log(path, site, message):
    timestamp = datetime.now().isoformat(timespec="seconds")
    safe_site = " ".join(str(site).split())
    safe_message = " ".join(str(message).split())
    line = f"{timestamp}\t{safe_site}\t{safe_message}\n"

    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8", newline="") as file:
        file.write(line)


append_patrol_log(
    "巡查資料/教學巡查.log",
    "示範池A",
    "完成例行檢查",
)

先移除輸入中的換行,可避免一筆紀錄被偽裝成多行。真正系統還要限制可寫入欄位,避免Token、密碼及個資進入日誌。

八、使用writelines()時別忘了換行

from pathlib import Path

items = ["白馬", "烏龜", "姻緣花"]
lines = [f"{number}. {item}\n" for number, item in enumerate(items, 1)]

path = Path("水井三寶清單.txt")
with path.open("w", encoding="utf-8", newline="") as file:
    file.writelines(lines)

writelines() 不會自動加入換行,因此要自行在每個字串結尾放入 \n

九、處理不同作業系統的換行

Windows常見CRLF,Linux常見LF。文字模式通常會自動轉換;需要輸出一致格式時,可使用 newline="" 並明確寫入 \n。讀取時可用 splitlines() 處理常見換行。

text = "第一行\r\n第二行\n第三行\r第四行"

for number, line in enumerate(text.splitlines(), start=1):
    print(number, line)

十、遇到編碼錯誤時不要默默刪字

from pathlib import Path


def read_utf8(path):
    path = Path(path)
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError:
        print("找不到檔案:", path)
    except UnicodeDecodeError as error:
        print("檔案不是有效UTF-8,請確認來源編碼:", error)
    return None

不建議用 errors="ignore",因為它會直接丟掉無法解碼的字元。地方名稱、臺羅聲調或訪談內容可能因此無聲消失。

十一、從原始稿產生確認稿

以下流程只讀原始稿,將清理結果寫入新的確認稿;如果目標已存在就停止。

import unicodedata
from pathlib import Path


def prepare_review_copy(source, target):
    source = Path(source)
    target = Path(target)

    text = source.read_text(encoding="utf-8")
    normalized = unicodedata.normalize("NFC", text)
    lines = [line.rstrip() for line in normalized.splitlines()]
    review_text = "\n".join(lines) + "\n"

    target.parent.mkdir(parents=True, exist_ok=True)
    with target.open("x", encoding="utf-8", newline="") as file:
        file.write(review_text)
    return target


# 實際使用時請換成資料副本路徑
# prepare_review_copy("逐字稿/原始檔/訪談.txt",
#                     "逐字稿/確認稿/訪談_v1.txt")
不能自動完成的工作:程式可以統一Unicode與換行,但不能決定講述者真正想表達的台語用字,也不能自行把確認稿標成公開版。須保留修改紀錄並取得當事人確認。

十二、簡易版本資訊檔

from pathlib import Path

metadata = [
    "文件:水井三寶訪談整理稿",
    "版本:v1",
    "狀態:待講述者確認",
    "公開:否",
]

path = Path("逐字稿") / "水井三寶_v1_metadata.txt"
with path.open("x", encoding="utf-8", newline="") as file:
    file.write("\n".join(metadata) + "\n")

真實專案還可記錄建立日期、來源檔案、整理者、確認者、授權範圍及撤回方式,但不要把不必要的個資寫入公開中繼資料。

十三、常見錯誤檢查表

現象原因修正
原始內容消失誤用w模式重要新檔使用x,原始檔只讀
檔案沒有完整寫入未正確關閉檔案使用with管理資源
writelines全部黏在一起沒有自行加入換行每個字串結尾加\n
中文或臺羅缺字編碼錯誤或忽略解碼失敗確認來源編碼,不用errors="ignore"
日誌一筆變多行使用者輸入含換行寫入前清理並限制欄位

十四、USR實作挑戰

挑戰A|四階段逐字稿
建立原始檔、整理稿、講述者確認稿與公開版流程,為每份檔案加上狀態資訊,確認程式永不覆蓋原始檔。
挑戰B|巡查日誌
追加十筆匿名教學紀錄,再逐行統計正常、待確認與離線筆數;不把無效讀值當成正常。
挑戰C|跨平台換行
在Thonny、Colab與不同作業系統開啟同一UTF-8檔,記錄換行及編碼差異。

十五、與生成式AI協作

請擔任Python文字檔與USR資料治理助教。
請設計地方訪談文字檔處理流程:
1. 原始逐字稿只能讀取,不得覆蓋;
2. 整理稿使用新版本檔名與x模式;
3. 明確指定UTF-8;
4. 不可使用errors="ignore"刪字;
5. 保留原始稿、修改紀錄與確認狀態;
6. 公開版必須由講述者或權利人確認。
請先列風險,再提供小型、可測試的函式。
本篇小結
安全讀寫文字檔的核心不只是模式與編碼,而是知道哪些資料可以覆寫、哪些必須保留,以及誰有權決定公開。with、UTF-8與排他建立模式,是技術上的第一道防線。

沒有留言:

張貼留言