本文 AI 產出,尚未審核

Python 檔案操作(File I/O)

主題:seek / tell


簡介

在日常開發中,讀寫檔案是最常見的需求之一。大多數人只會使用 open()read()write() 等基本方法,卻忽略了檔案指標(file pointer)這個概念。檔案指標決定了下一次讀寫操作會從檔案的哪一個位元組開始,而 seek()tell() 正是 控制與查詢檔案指標 的兩個核心函式。

掌握 seek / tell 後,我們就能:

  • 隨機存取大型檔案(不必一次讀完整個檔案)
  • 讀取或修改檔案中的特定區塊(例如 CSV 的某一列、二進位檔的標頭)
  • 實作簡易的「暫存」或「回溯」機制,提升程式的彈性與效能

因此,本篇文章會從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用,幫助你在 Python 中自如地使用 seektell


核心概念

1. 檔案指標的本質

當使用 open('example.txt', 'r') 開啟一個檔案時,Python 會在記憶體中維護一個 檔案指標(file pointer),它是一個整數,代表「目前在檔案的第幾個位元組」:

位元組編號 0 1 2 3
內容 H e l l o
  • 初始時指標 指向檔案開頭(位置 0)。
  • 每次呼叫 read(n),指標會往前移動 n 個位元組。
  • 每次呼叫 write(),指標也會往前移動寫入的位元組數。

seek(offset, whence) 用來改變這個指標的位置,tell() 則是回傳目前的指標值。

2. seek(offset, whence=0)

參數 說明
offset 移動的位元組數(正數或負數)
whence 參考點,預設 0(檔案開頭)
1:目前位置
2:檔案結尾

常見用法

f.seek(0)          # 移到檔案開頭
f.seek(10, 0)      # 從開頭往前 10 位元組
f.seek(-5, 2)      # 從檔案結尾往回 5 位元組(只適用於可隨機存取的模式)

⚠️ 注意:在文字模式('r''w')下,seek 的行為可能會因編碼(UTF-8、UTF-16)而有所不同;若要精確控制位元組,建議使用 二進位模式'rb''wb')。

3. tell()

tell() 直接回傳目前指標所在的 位元組索引。這在以下情境非常有用:

  • 需要記錄「讀到哪裡」以便之後回到同一位置。
  • 實作檔案分割(splitting)或分段讀寫時,需要知道每段的起始位置。
pos = f.tell()     # 取得目前位置
print(f'目前指標在第 {pos} 個位元組')

程式碼範例

以下示範 5 個實用範例,從基礎到稍微進階的應用,全部使用 二進位模式 以避免編碼問題。

範例 1:基本的 seek / tell

# 讀取檔案前 10 個位元組,然後回到開頭再讀一次
with open('sample.txt', 'rb') as f:
    data = f.read(10)          # 指標移到第 10 位元組
    print('前 10 位元組:', data)

    pos = f.tell()             # 取得目前位置(應該是 10)
    print('目前指標位置:', pos)

    f.seek(0)                  # 回到檔案開頭
    again = f.read(10)         # 再次讀取相同的內容
    print('再次讀取:', again)

重點tell() 回傳的值正好等於已讀取的位元組數。


範例 2:從檔案結尾往回讀取最後 20 個位元組(適用於日誌檔)

def tail_bytes(filepath, n=20):
    """回傳檔案最後 n 個位元組(類似 Unix `tail -c`)"""
    with open(filepath, 'rb') as f:
        f.seek(-n, 2)          # 從結尾往回 n 位元組
        return f.read(n)

last_bytes = tail_bytes('log.txt', 20)
print('檔案最後 20 位元組:', last_bytes)

技巧whence=2 表示「以檔案結尾為基準」,負的 offset 代表往回移動。


範例 3:隨機存取 CSV 檔的特定列(每列固定長度)

假設每列固定 30 個位元組(包含換行),我們要直接抓第 5 列:

LINE_LEN = 30          # 每列佔用的位元組數

def read_line(filepath, line_no):
    """讀取第 line_no 列(從 1 開始計算)"""
    with open(filepath, 'rb') as f:
        offset = (line_no - 1) * LINE_LEN
        f.seek(offset, 0)          # 計算偏移量並移動指標
        return f.read(LINE_LEN).decode('utf-8').rstrip('\n')

row5 = read_line('fixed_width.csv', 5)
print('第 5 列內容:', row5)

說明:這種「固定寬度」的檔案非常適合使用 seek,不必一次讀完整個檔案。


範例 4:在二進位檔中插入資料(需要先搬移後面的內容)

Python 原生的檔案寫入只能覆寫,若要 插入,必須自行搬移後面的位元組:

def insert_bytes(filepath, insert_data, pos):
    """在檔案 pos 位置插入 insert_data(bytes),其餘內容往後移"""
    with open(filepath, 'rb+') as f:
        f.seek(0, 2)                # 先移到檔案結尾取得總長度
        file_size = f.tell()

        # 從結尾開始往前搬移,確保不會覆蓋尚未搬移的資料
        for i in range(file_size, pos, -1):
            f.seek(i - 1)
            byte = f.read(1)
            f.seek(i)
            f.write(byte)

        # 把插入資料寫入目標位置
        f.seek(pos)
        f.write(insert_data)

# 範例:在第 100 位元組插入 b'HELLO'
insert_bytes('binary.dat', b'HELLO', 100)

提醒:此方法在大型檔案上效能較差,實務上會使用暫存檔或 mmap(稍後說明)。


範例 5:使用 mmap 搭配 seek / tell 進行高速隨機存取

mmap 讓檔案映射到記憶體,能直接使用類似 bytearray 的操作,同時保有 seek / tell

import mmap

def read_via_mmap(filepath, start, length):
    """利用 mmap 從 start 位置讀取 length 個位元組"""
    with open(filepath, 'rb') as f:
        # 建立只讀的 mmap 物件
        mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        mm.seek(start)               # 移動指標
        data = mm.read(length)       # 讀取資料
        mm.close()
        return data

chunk = read_via_mmap('large.bin', 1024 * 1024, 64)   # 讀取第 1 MB 後的 64 位元組
print('讀取結果:', chunk)

優點:不需要自行搬移資料,且對大型檔案的隨機讀寫效能遠高於傳統 seek + read


常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
在文字模式下使用負向 seek 會拋出 OSError: can't do nonzero cur-relative seeks(特別是 Windows) 改用二進位模式 ('rb') 或先 f.seek(0, 2) 取得檔案長度再計算正向偏移
忽略編碼差異 以 UTF‑8 為例,單一字元可能佔 1~4 位元組,seek(5) 可能落在字元中間,導致解碼錯誤 若需精確位元組控制,使用二進位模式;若必須在文字模式下,僅在已知每行固定長度或使用 io.TextIOWrapperseek
在大型檔案上頻繁搬移資料 效能極差,甚至耗盡磁碟空間 使用暫存檔、mmap 或資料庫(如 SQLite)代替直接搬移
忘記 flush()close() 寫入的資料可能仍在緩衝區,導致檔案內容不完整 使用 with open(...) as f: 自動關閉,或手動呼叫 f.flush()
跨平台差異(Windows vs POSIX) Windows 不支援在文字模式下 seek 超過 2GB 在跨平台程式中,盡量使用二進位模式或 os 模組提供的 os.lseek

最佳實踐

  1. 預先取得檔案大小f.seek(0, 2); size = f.tell(),可避免超出範圍的 seek
  2. 使用 with 句柄:保證檔案在例外發生時仍會正確關閉。
  3. 盡量在二進位模式操作,尤其是需要精確定位的情況。
  4. 對於大量隨機讀寫,考慮 mmapio.BufferedRandom 或資料庫。
  5. 加入錯誤檢查try/except OSError,捕捉非法的 seektell 行為。

實際應用場景

場景 為什麼需要 seek / tell 範例概念
日誌檔輪替 只需要讀取最後幾行,避免把整個巨型日誌載入記憶體 f.seek(-1024, 2) 讀取最後 1KB
多媒體檔案切割 MP3、MP4 等格式有固定的幀(frame)結構,需要根據幀大小定位 f.seek(frame_offset, 0)
資料庫檔案快照 需要從檔案的特定位置開始寫入增量備份 f.seek(snapshot_start); f.write(delta)
網路協議實作(例如 HTTP 分段傳輸) 需要先寫入標頭,再回頭填入內容長度 f.seek(0); f.write(header),之後 f.seek(content_start); f.write(body)
大型科學資料(如遙測影像) 只取出圖像的特定區塊,避免載入全圖 計算每行位元組長度,f.seek(row_offset + col_offset) 讀取區塊

總結

  • seek(offset, whence)tell()檔案指標 的核心控制工具,讓我們能在檔案中任意跳躍、回溯或查詢位置。
  • 二進位模式 下使用最為直接且不受編碼影響;文字模式則需注意換行與編碼的差異。
  • 常見陷阱包括負向 seek、跨平台行為差異以及在大型檔案上頻繁搬移資料。遵循「先取得檔案大小、使用 with 管理資源、必要時採用 mmap」等最佳實踐,可大幅降低錯誤與效能問題。
  • 透過上述範例,我們已展示了從簡單的指標查詢、倒讀最後幾位元組、固定寬度檔案的隨機讀取,到插入資料與高速隨機存取的完整流程。這些技巧在日誌分析、媒體處理、資料備份與科學計算等領域都有實際價值。

掌握 seek / tell,就等於擁有了 檔案隨機存取的鑰匙,讓你的 Python 程式在處理大型或結構化檔案時更加靈活、效能更佳。祝你在實務開發中玩得開心、寫得順手!