Python 檔案讀寫:open()、r、w、a 模式與 CSV 操作

Python 檔案讀寫讓程式能與外部世界交換資料——讀取歷史 K 線、儲存回測結果、匯出交易紀錄,都仰賴 open() 函數與正確的開檔模式。本文完整解析 open() 的四種模式(rwarb/wb)、with 語法的安全開檔慣例、逐行讀取與整體讀取的差異,以及用 Python 內建 csv 模組讀寫 CSV 格式的量化資料完整實戰。

Cinematic dark alchemy forge with glowing ancient stone tablets representing files being written and read by mystical arcane tools, amber firelight illuminating data runes flowing between a ledger book and the forge workbench, deep dramatic shadows, data streams as light particles, cinematic lighting, 16:9

什麼是檔案讀寫?

Python 的檔案讀寫(File I/O)透過內建的 open() 函數操作,指定檔案路徑開檔模式後,取得一個檔案物件,再透過 read()write()readline() 等方法與檔案內容互動。用完之後必須呼叫 close() 關閉檔案,釋放系統資源。

最佳實踐是使用 with 語法(情境管理器)自動管理開關檔案,就像煉金坊的自動封蓋熔爐:取用原料(讀寫資料)完畢後,爐蓋自動密封,不需要手動記得關閉,避免因忘記 close() 而造成資源洩漏或檔案損壞。

核心語法:open()、模式與 with

# === open() 四種常用模式 ===
# 'r'  → 讀取(Read):檔案必須存在,否則 FileNotFoundError
# 'w'  → 寫入(Write):覆蓋整個檔案;檔案不存在時自動建立
# 'a'  → 附加(Append):在檔案末尾追加;檔案不存在時自動建立
# 'x'  → 獨占建立:檔案已存在時拋出 FileExistsError
# 'rb' / 'wb' → 二進位讀寫(用於圖片、PDF 等非文字檔)

# === 不推薦的傳統寫法(容易忘記 close)===
f = open('data.txt', 'r', encoding='utf-8')
content = f.read()
f.close()    # 必須手動關閉,若中途例外就不會執行到這裡!

# === ✅ 推薦:with 語法(自動關閉,安全)===
with open('data.txt', 'r', encoding='utf-8') as f:
    content = f.read()
# with 區塊結束後,f 自動關閉,無論是否發生例外
print(content)
開檔時務必指定 encoding='utf-8'(或 'utf-8-sig' 處理 BOM 標頭),否則在不同作業系統上可能因預設編碼不同而出現亂碼,尤其是包含中文字元的檔案。Windows 預設編碼為 cp950,Mac/Linux 為 utf-8,明確指定才能跨平台相容。
# === 寫入檔案:'w' 模式(覆蓋)===
lines = [
    "日期,開盤,高點,低點,收盤\n",
    "2024-01-02,182.5,185.9,181.0,185.2\n",
    "2024-01-03,185.5,188.2,184.7,187.8\n",
    "2024-01-04,187.2,188.5,182.1,183.5\n",
]

with open('ohlc.txt', 'w', encoding='utf-8') as f:
    f.writelines(lines)           # 一次寫入多行(List)
print("寫入完成")

# write() 寫入單一字串
with open('log.txt', 'w', encoding='utf-8') as f:
    f.write("策略啟動\n")
    f.write("載入資料完成\n")

# === 附加檔案:'a' 模式(不覆蓋,追加到末尾)===
with open('log.txt', 'a', encoding='utf-8') as f:
    f.write("2024-01-02 執行買入:AAPL x 100\n")
    f.write("2024-01-03 執行賣出:AAPL x 100\n")
'w' 模式每次都會清空並覆蓋整個檔案,若只需要在既有內容後面追加(如交易日誌),必須改用 'a' 模式,否則之前的記錄會全部消失。
# === 讀取方式比較 ===

# read():一次讀取整個檔案成字串(適合小檔案)
with open('ohlc.txt', 'r', encoding='utf-8') as f:
    all_text = f.read()
print(all_text)

# readlines():一次讀取所有行,回傳 List(每行是一個元素,含 \n)
with open('ohlc.txt', 'r', encoding='utf-8') as f:
    all_lines = f.readlines()
print(all_lines[0])    # '日期,開盤,高點,低點,收盤\n'

# ✅ 逐行迭代(最省記憶體,適合大檔案)
with open('ohlc.txt', 'r', encoding='utf-8') as f:
    for line in f:                        # 直接迭代檔案物件
        print(line.strip())               # strip() 去除行尾 \n

規則與注意事項

模式 行為 檔案不存在時
'r' 唯讀,不可寫入 FileNotFoundError
'w' 寫入,清空舊內容 自動建立新檔案
'a' 追加,保留舊內容 自動建立新檔案
'r+' 讀寫,不清空 FileNotFoundError
'x' 建立新檔案並寫入 自動建立;已存在則 FileExistsError

常見錯誤與防呆

錯誤一:忘記指定 encoding,中文亂碼

# ❌ 錯誤寫法(Windows 預設 cp950,Mac/Linux 預設 utf-8,跨平台亂碼)
with open('data.txt', 'r') as f:
    content = f.read()   # 可能出現 UnicodeDecodeError 或亂碼

# ✅ 正確寫法:永遠明確指定編碼
with open('data.txt', 'r', encoding='utf-8') as f:
    content = f.read()

# 若 CSV 有 BOM 標頭(Excel 存出的 UTF-8 CSV)
with open('data.csv', 'r', encoding='utf-8-sig') as f:
    content = f.read()

錯誤二:用 'w' 模式誤刪既有資料

# ❌ 危險寫法(每次執行都清空 log 檔案!)
with open('trade_log.txt', 'w', encoding='utf-8') as f:
    f.write("2024-01-05 買入 AAPL\n")   # 之前的記錄全被刪除

# ✅ 正確做法:交易日誌用 'a' 模式追加
with open('trade_log.txt', 'a', encoding='utf-8') as f:
    f.write("2024-01-05 買入 AAPL\n")   # 保留所有歷史記錄

錯誤三:讀取不存在的檔案未加例外處理

# ❌ 錯誤寫法(檔案不存在時程式崩潰)
with open('missing.csv', 'r', encoding='utf-8') as f:
    data = f.read()   # FileNotFoundError,程式中止

# ✅ 正確做法:用 try-except 搭配檔案讀寫
import os

filepath = 'missing.csv'
if os.path.exists(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        data = f.read()
else:
    print(f"檔案不存在:{filepath}")
    data = None

# 或直接用 try-except
try:
    with open(filepath, 'r', encoding='utf-8') as f:
        data = f.read()
except FileNotFoundError:
    print(f"找不到檔案:{filepath}")
    data = None

進階用法

csv 模組:讀寫結構化資料

Python 內建 csv 模組專門處理逗號分隔值格式,自動處理引號跳脫、分隔符號等邊界情況,比手動用 split(',') 分割更安全可靠,是量化資料處理的必備工具。

import csv

# === 寫入 CSV ===
ohlc_data = [
    ['Date',       'Open',  'High',  'Low',   'Close', 'Volume'],
    ['2024-01-02', 182.5,   185.9,   181.0,   185.2,   45000000],
    ['2024-01-03', 185.5,   188.2,   184.7,   187.8,   52000000],
    ['2024-01-04', 187.2,   188.5,   182.1,   183.5,   38000000],
    ['2024-01-05', 183.8,   186.3,   183.0,   185.9,   41000000],
]

with open('aapl_ohlc.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerows(ohlc_data)    # 一次寫入多行
print("CSV 寫入完成")

# === 讀取 CSV ===
with open('aapl_ohlc.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    header = next(reader)          # 讀取標題行
    print(f"欄位:{header}")

    for row in reader:
        date, o, h, l, c, vol = row
        print(f"{date}  收盤:{float(c):.2f}  成交量:{int(vol):,}")

csv.DictReader / DictWriter:欄位名稱存取

DictReader 讓每一行資料以字典形式讀取,用欄位名稱而非索引取值,程式碼更易讀,也不怕欄位順序改變;DictWriter 則讓寫入時以字典指定欄位值。

import csv

# === DictReader:以欄位名稱讀取 ===
closes = []
with open('aapl_ohlc.csv', 'r', encoding='utf-8') as f:
    reader = csv.DictReader(f)       # 自動把第一行當標題
    for row in reader:
        # row 是 OrderedDict,用欄位名稱取值
        closes.append(float(row['Close']))

print(f"收盤價序列:{closes}")
print(f"最高收盤:{max(closes)}")
print(f"平均收盤:{sum(closes)/len(closes):.2f}")

# === DictWriter:以欄位名稱寫入 ===
results = [
    {'Date': '2024-01-02', 'Signal': 'HOLD', 'PnL': 0},
    {'Date': '2024-01-03', 'Signal': 'BUY',  'PnL': 0},
    {'Date': '2024-01-04', 'Signal': 'HOLD', 'PnL': 2.6},
    {'Date': '2024-01-05', 'Signal': 'SELL', 'PnL': 2.4},
]

fieldnames = ['Date', 'Signal', 'PnL']
with open('backtest_result.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()             # 寫入標題行
    writer.writerows(results)        # 寫入所有資料行
print("回測結果已儲存")

量化實戰:完整回測結果讀寫流程

將回測的每日損益記錄寫入 CSV,之後再讀回計算績效指標,是量化開發中最基本也最重要的資料持久化流程。

import csv
import os

def save_trades(trades, filepath='trades.csv'):
    """儲存交易紀錄到 CSV"""
    fieldnames = ['date', 'ticker', 'action', 'price', 'shares', 'pnl']
    file_exists = os.path.exists(filepath)

    with open(filepath, 'a', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        if not file_exists:
            writer.writeheader()    # 只有建立新檔案時才寫標題
        writer.writerows(trades)

def load_trades(filepath='trades.csv'):
    """從 CSV 讀取交易紀錄並計算績效"""
    if not os.path.exists(filepath):
        print("找不到交易紀錄檔案")
        return []

    trades = []
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                row['price']  = float(row['price'])
                row['shares'] = int(row['shares'])
                row['pnl']    = float(row['pnl'])
                trades.append(row)
    except Exception as e:
        print(f"讀取失敗:{e}")
    return trades

# 模擬使用
sample_trades = [
    {'date': '2024-01-03', 'ticker': 'AAPL', 'action': 'BUY',
     'price': 185.5, 'shares': 100, 'pnl': 0},
    {'date': '2024-01-05', 'ticker': 'AAPL', 'action': 'SELL',
     'price': 188.2, 'shares': 100, 'pnl': 270.0},
]

save_trades(sample_trades)
loaded = load_trades()

total_pnl = sum(t['pnl'] for t in loaded)
wins  = [t for t in loaded if t['pnl'] > 0]
print(f"總交易筆數:{len(loaded)}")
print(f"獲利交易:{len(wins)}")
print(f"總損益:{total_pnl:.2f}")

煉金坊小叮嚀

恭喜你走到了 Python 語法系列的最後一篇!檔案讀寫是程式與外部世界溝通的橋樑,在量化開發中幾乎每天都用到。有三個習慣從一開始就要養成:第一,永遠用 with 開檔,不要手動 open + close;第二,永遠明確指定 encoding='utf-8',避免跨平台亂碼;第三,寫入交易日誌或追加記錄時確認用的是 'a' 而非 'w' 模式——我曾因一個 'w' 的筆誤,把幾個月的回測日誌清空,教訓深刻。對於量化資料建議優先使用 csv.DictReader / DictWriter,用欄位名稱取值比索引更健壯,之後加欄位不會讓程式碼崩潰。當資料量大時,進一步考慮使用 pandas 的 read_csv(),但掌握原生 csv 模組才能真正理解底層運作原理。

張貼留言

較新的 較舊