跳到內容

檔案讀寫

程式跑完,變數裡的資料就沒了。想把結果留下來、下次再讀回來,就要寫進檔案。這一章看怎麼讀寫檔案。

用 open() 打開檔案,第一個參數是檔名,第二個是模式:"w" 是寫入(write)、"r" 是讀取(read)。搭配 with,區塊結束時檔案會自動關好。寫入用 write(),讀取用 read():

with open("note.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
with open("note.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)

"w" 模式會把檔案從頭寫過:檔案不存在就新建,已經存在就把舊內容整個蓋掉,要小心。\n 是換行符號,讓每筆資料各自成一行。

檔案讀進來常常要逐行處理。直接用 for 走過打開的檔案,每一圈就是一行。行尾會帶著換行符號,通常用 strip() 把它去掉:

with open("fruits.txt", "w", encoding="utf-8") as f:
f.write("蘋果\n")
f.write("香蕉\n")
f.write("橘子\n")
with open("fruits.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())

如果只想讀一行,用 readline():每呼叫一次就讀下一行,讀到的字串同樣帶著行尾的換行符號。for 走過檔案,其實就是幫你一次次呼叫 readline(),直到檔案讀完為止:

with open("fruits.txt", "w", encoding="utf-8") as f:
f.write("蘋果\n香蕉\n橘子\n")
with open("fruits.txt", "r", encoding="utf-8") as f:
first = f.readline()
second = f.readline()
print(first.strip())
print(second.strip())

如果想一次把所有行讀成一個串列,用 readlines()。每個元素是一行,行尾一樣帶著換行符號:

with open("data.txt", "w", encoding="utf-8") as f:
f.write("第一行\n第二行\n第三行\n")
with open("data.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(lines)
print(len(lines))

另一個常見做法是先用 read() 讀進整份內容,再用字串的 splitlines() 一次切成一行一行,而且會順便去掉換行符號:

with open("fruits.txt", "w", encoding="utf-8") as f:
f.write("蘋果\n香蕉\n橘子\n")
with open("fruits.txt", "r", encoding="utf-8") as f:
text = f.read()
for line in text.splitlines():
print(line)

如果想在檔案後面接著寫、而不是蓋掉,就用 "a"(append,附加)模式:

with open("log.txt", "w", encoding="utf-8") as f:
f.write("第一筆\n")
with open("log.txt", "a", encoding="utf-8") as f:
f.write("第二筆\n")
with open("log.txt", "r", encoding="utf-8") as f:
print(f.read())

先用 "w" 寫了第一筆,再用 "a" 補上第二筆,最後讀回來,兩筆都在。記錄日誌、持續累積資料時,附加模式很好用。

打開的檔案裡有一個看不見的讀寫位置,像閱讀時的手指:讀到哪、寫到哪,它就跟到哪。剛打開時它在開頭,每讀一段或寫一段就往後移,讀完整個檔案之後它會停在結尾。tell() 會告訴你它現在在哪裡,seek() 則把它搬到你指定的地方。執行下面的例子時,看右邊物件面板裡的檔案卡片:它印出檔案目前的內容,中間那條直線就是讀寫位置,按下一步時看它怎麼移動:

with open("note.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
with open("note.txt", "r", encoding="utf-8") as f:
print(f.tell())
first = f.readline()
print(f.tell())
rest = f.read()
print(f.tell())
again = f.read()
print(again == "")
f.seek(0)
print(f.tell())
print(f.read())

一開始 tell() 是 0,表示在開頭。readline() 讀走第一行後變成 10,read() 把剩下的全讀完後變成 20,這時位置已經在結尾,再 read() 一次只會拿到空字串,因為後面沒有東西了。seek(0) 把位置搬回開頭,tell() 又變回 0,就能從頭再讀一次。

tell() 回傳的數字是從開頭算起的位元組數,不是字數:encoding="utf-8" 下一個中文字佔 3 個位元組,換行符號佔 1 個,所以「第一行」加一個換行剛好是 10。你不需要自己算這個數字,記住「0 是開頭」就夠用了;想之後回到某個位置,先用 tell() 把數字記下來,再交給 seek()。

"r"、"w"、"a" 各自只能做一件事:"r" 打開的檔案不能寫,"w" 和 "a" 打開的檔案不能讀。如果想在同一次打開裡又讀又寫,在模式後面加上 + 就好:"r+"、"w+"、"a+"。

with open("note.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
with open("note.txt", "r+", encoding="utf-8") as f:
before = f.read()
f.write("第二行\n")
f.seek(0)
after = f.read()
print(before)
print(after)

這個例子用 "r+" 打開已經有內容的檔案,先把原本的內容讀出來,再接著寫一行,最後讀回全部內容。讀完、寫完之後讀寫位置停在檔案結尾,直接 read() 只會得到空字串,所以讀回之前要先 seek(0) 回到開頭。

+ 只是讓原本只能讀的也能寫、只能寫的也能讀,其他行為都跟原本一樣:"r+" 要求檔案已經存在,而且寫入從開頭開始,沒先讀到結尾就寫會蓋掉原本位置上的字;"w+" 一打開就先清空;"a+" 不管位置在哪,寫入永遠接在最後面。

六種模式一次比較,忘記的時候回來查:

模式讀取寫入打開時清空檔案不存在時
"r"可不可不會錯誤
"r+"可可不會錯誤
"w"不可可會新建
"w+"可可會新建
"a"不可可不會新建
"a+"可可不會新建

看出規律了嗎?字母決定會不會清空、檔案不在時怎麼辦,+ 只決定能不能同時讀寫。平常讀檔用 "r"、重寫用 "w"、累積用 "a" 就夠了,+ 留給需要邊讀邊寫的時候。

上面六種都是文字模式:read() 拿到的是字串,write() 也要給字串,Python 會照 encoding 幫你在文字和位元組之間來回轉換。但圖片、音檔、影片、PDF、壓縮檔這些檔案本來就不是文字,用文字模式打開不是出錯就是內容壞掉。這時候在模式後面加一個 b(binary,二進位):"rb"、"wb"、"ab",讀到的就是檔案裡的原始位元組。

text = "哈囉"
with open("note.txt", "w", encoding="utf-8") as file:
file.write(text)
with open("note.txt", "rb") as file:
data = file.read()
print(data)
print(len(text), len(data))
with open("copy.txt", "wb") as file:
file.write(data)
with open("copy.txt", "r", encoding="utf-8") as file:
print(file.read())

同一個檔案,先用 "w" 以文字寫進去,再用 "rb" 讀回來,拿到的是 b'\xe5\x93\x88\xe5\x9b\x89'。開頭那個 b 表示它是 bytes(位元組序列),不是字串。len(text) 是 2,len(data) 是 6,因為一個中文字在 UTF-8 底下佔 3 個位元組。把這串位元組用 "wb" 原封不動寫進另一個檔案,再用文字模式讀回來,還是「哈囉」:二進位模式搬的是一模一樣的內容,只是不幫你解釋它的意思。

加了 b 就不能再給 encoding,硬給會直接出錯,因為二進位模式不做編碼轉換,指定編碼沒有意義。b 也可以和 + 併用,"rb+"、"wb+" 的規則和對應的文字模式一樣,只是讀寫的東西換成位元組。

平常處理文字檔用不到 b。後面網路爬蟲那個專題要把抓下來的圖片存成 .png,用的就是 "wb"。

檔案用完一定要關掉,否則資料可能還沒真正寫進去。with open(...) as f: 的好處,就是不管區塊裡發生什麼事,離開時都會自動幫你把檔案關好,不用自己記得呼叫 close()。

另外,例子裡都寫了 encoding="utf-8"。處理中文時這個很重要:不指定的話,有些系統(特別是 Windows)會用別的編碼,讀寫中文就容易變成亂碼。養成習慣加上它,中文就不會出問題。

v0.9.1 (c924b5b)