Python 檔案操作(File I/O)
主題:seek / tell
簡介
在日常開發中,讀寫檔案是最常見的需求之一。大多數人只會使用 open()、read()、write() 等基本方法,卻忽略了檔案指標(file pointer)這個概念。檔案指標決定了下一次讀寫操作會從檔案的哪一個位元組開始,而 seek() 與 tell() 正是 控制與查詢檔案指標 的兩個核心函式。
掌握 seek / tell 後,我們就能:
- 隨機存取大型檔案(不必一次讀完整個檔案)
- 讀取或修改檔案中的特定區塊(例如 CSV 的某一列、二進位檔的標頭)
- 實作簡易的「暫存」或「回溯」機制,提升程式的彈性與效能
因此,本篇文章會從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用,幫助你在 Python 中自如地使用 seek 與 tell。
核心概念
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.TextIOWrapper 的 seek |
| 在大型檔案上頻繁搬移資料 | 效能極差,甚至耗盡磁碟空間 | 使用暫存檔、mmap 或資料庫(如 SQLite)代替直接搬移 |
忘記 flush() 或 close() |
寫入的資料可能仍在緩衝區,導致檔案內容不完整 | 使用 with open(...) as f: 自動關閉,或手動呼叫 f.flush() |
| 跨平台差異(Windows vs POSIX) | Windows 不支援在文字模式下 seek 超過 2GB |
在跨平台程式中,盡量使用二進位模式或 os 模組提供的 os.lseek |
最佳實踐
- 預先取得檔案大小:
f.seek(0, 2); size = f.tell(),可避免超出範圍的seek。 - 使用
with句柄:保證檔案在例外發生時仍會正確關閉。 - 盡量在二進位模式操作,尤其是需要精確定位的情況。
- 對於大量隨機讀寫,考慮
mmap、io.BufferedRandom或資料庫。 - 加入錯誤檢查:
try/except OSError,捕捉非法的seek或tell行為。
實際應用場景
| 場景 | 為什麼需要 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 程式在處理大型或結構化檔案時更加靈活、效能更佳。祝你在實務開發中玩得開心、寫得順手!