2022年9月6日 星期二

Python 學習筆記 : 檔案存取 (一) 讀寫文字檔

檔案是具有名稱的有序資料集合, 資料按照特定順序儲存於外部持久性媒體成為檔案. 檔案系統是以樹狀目錄結構來存放檔案, 可透過目錄路徑與檔案名稱以絕對路徑或相對路徑存取檔案. Python 是以物件與串流來管理檔案與目錄, 關於檔案與目錄的操作參考 : 


本篇則是 Python 檔案存取 (即讀寫檔案) 的測試筆記, 參考書籍如下 :

Python 零基礎入門班 (碁峰 2018, 文淵閣工作室) 
# Python 程式設計入門與運算思維 (新陸 2017, 曹祥雲)

本篇其實是為了將前兩本借自母校圖書館的書還回去而整理的, 雖然這兩本是基礎書, 但仍有甚多可觀之處, 溫故知新兼寫筆記. 

檔案分成文字檔與二進位檔兩種, 兩種檔案的差別在於底層的存取模式, 二進位檔是直接將資料以原始 Byte 形式寫入媒體中 (在 Python 就是利用 Bytes 類別), 沒有經過任何加工; 而文字檔則不同, 其內容是以 ASCII 或 Unicode 編碼的字元, 因此它們是以人可閱讀的形式寫到檔案中, 即使是數值也是轉成字元編碼, 並且會對跳行進行翻譯. 文字檔其實就是以跳行符號串接起來的字串.

二進位檔不像文字檔有通用的編碼 (即 Unicode), 其結構沒有普遍認可的格式 (自訂), 但在檔案處理效能上佔有優勢, 因為二進位檔因為不需要花費時間做數字到文字與文字到數字的轉換, 故其處理速度比文字檔要快上幾倍. 二進位檔的優點就是遂度快與占用空間少. 


1. 常用的檔案目錄函式 : 

Python 標準函式庫中的 os 模組提供許多操作檔案與目錄的函式, 其中較常用的檔案與目錄操作函式摘要如下表 : 


 常用的 os 模組函式 說明
 os.getcwd() 傳回目前的工作目錄
 os.chdir(d) 切換工作目錄至目錄 d
 os.mkdir(d) 建立目錄 d, 須搭配 os.path.exists() 檢查目錄是否存在以免產生執行錯誤
 os.rmdir(d) 刪除目錄 d, 須搭配 os.path.exists() 檢查目錄是否存在以免產生執行錯誤
 os.remove(f) 刪除檔案 f, 須搭配 os.path.exists() 檢查檔案是否存在以免產生執行錯誤
 os.rename(old, new) 將舊目錄或檔案名稱 old 改成新目錄或檔案名稱 new
 os.path.exists(x) 檢查指定之目錄或檔案 x 是否存在
 os.path.isfile(x) 檢查指定之路徑 x 是否為檔案 (或檔案是否存在)
 os.path.isdir(x) 檢查指定之路徑 x 是否為目錄 (或目錄是否存在)
 os.path.exists(x) 檢查指定之目錄或檔案 x 是否存在


注意, mkdir() 在目錄已存在, chdir() 與 rmdir() 在目錄不存在時會出現錯誤, 必須先用 os.path.exists() 檢查, 或將檔案目錄操作放在 try exception 區塊中, 例如 : 

>>> import os   
>>> os.getcwd()   
'D:\\test\\python'   
>>> os.path.exists('test.py')    
True
>>> os.path.isfile('test.py')    
True
>>> if os.path.exists('tmp'):           # 目錄存在才切換目錄
    os.chdir('tmp')    
>>> if not os.path.exists('tmp'):     # 目錄不存在才新增目錄
    os.mkdir('tmp')    
>>> if os.path.exists('tmp2'):          # 目錄存在才刪除目錄
    os.rmdir('tmp2')    
>>> try:                                             # 試試會不會出現例外
    os.chdir('tmp2')   
except Exception:   
    print('tmp2 not exists')   
>>> try:   
    os.rmdir('tmp2')       
except Exception as e:                     # e.args 會傳回例外原因 (tuple)
    print(e.args)    

I/O 輸出入作業 (例如網路與檔案目錄存取等) 因為存在不確定性, 故通常要放在 try except 區塊中以捕捉可能的例外. 


2. 開啟檔案 : 

檔案存取的程序有三個步驟 : 
  • 開啟檔案 : 用 open() 或 with open() as
  • 讀取/寫入內容 : f.read(), f.readline(), f.readlines()
  • 關閉檔案 : f.close()
不論讀寫都要先用內建函數 open() 開啟檔案, 處理完須關閉檔案釋放緩衝區資源. 如果使用 with open() 語法開啟則會自動關閉檔案, 不須呼叫 f.close().

Python 內建函式 open() 用來開啟舊檔或建立新檔, 它會傳回該檔案的參考, 利用此檔案物件之讀寫方法即可讀取或寫入內容, 語法有兩種 : 

f=open(filename [, mode='r'] [, encoding=''])       

此語法處理完須呼叫 f.close() 關閉檔案.

with open(filename [, mode='r'] [, encoding=''])  as f: 

此語法是在內縮區塊處理讀寫, 結束後不須呼叫 f.close(), 它會自動關閉檔案.

 open() 的必要參數 filename 為包含路徑之檔案名稱 (字串), 例如 'test.txt', '..\test.txt' 或 'C:\Users\tony\test.txt' 等. 備選參數 mode 是開啟模式 (字串), 可用模式如下 : 


 mode 說明
 r 以唯讀方式開啟檔案
 w  以寫入模式開啟檔案, 檔案若已存在覆蓋其內容, 若不存在就建立檔案
 x 以寫入模式建立一個檔案, 檔案若已存在拋出 FileExistsError 錯誤
 a 以寫入模式開啟檔案, 寫入之內容會加在檔案末尾
 b 開啟二進位檔案
 t 開啟文字檔案 (預設)
 + 以修改模式開啟檔案 (可讀取可寫入)


備選參數 encoding 用來指定檔案內容的編碼, 若未指定則使用預設編碼, 這視作業系統而定,  可以用標準函式庫 locale 模組的 getpreferredencoding() 函式查詢系統預設編碼, 例如在 Win10 繁中是 'cp950' (Unicode 的 Code Page 950, 此為從 BIG5 碼修改而來) :

>>> import locale       
>>> locale.getpreferredencoding()      
'cp950'  

在樹莓派的 Raspbian Linux 則是 'UTF-8' :

Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale   
>>> locale.getpreferredencoding()    
'UTF-8'    

讀取檔案時必須指定以前寫入時所使用的編碼格式, 否則會出現解碼錯誤. 

open() 會傳回一個檔案物件的參考 :
  • 開啟文字檔 : 傳回 TextIOWrapper 物件
  • 開啟二進位檔 : 傳回 BufferedReader 物件 (讀) 或 BufferedWritter 物件 (寫)
例如 : 

>>> f=open('test.txt', 'r')             # 以唯讀模式開啟文字檔
>>> type(f)   
<class '_io.TextIOWrapper'>         # 傳回 TextIOWrapper 物件
>>> f=open('test.txt', 'w')            # 以寫入模式開啟文字檔
>>> type(f)   
<class '_io.TextIOWrapper'>         # 傳回 TextIOWrapper 物件
>>> f=open('test.dat', 'rb')          # 以唯讀模式開啟二進位檔
>>> type(f)    
<class '_io.BufferedReader'>         # 傳回 BufferedReader 物件
>>> f=open('test.dat', 'wb')         # 以寫入模式開啟二進位檔
>>> type(f)    
<class '_io.BufferedWriter'>           # 傳回 BufferedWritter 物件
>>> f.close()   

可見不論讀或寫, 開啟文字檔都是傳回 TextIOWrapper 物件; 但開啟二進位檔則有分, 讀是回 BufferedReader 物件, 寫則傳回 BufferedWriter 物件. 


3. 檢視檔案物件的內容 : 

先來檢視檔案物件的內容, 以下使用一個自訂模組 members, 其 list_members() 函式會列出模組或套件中的公開成員 (即屬性與方法), 參考 :

Python 學習筆記 : 檢視物件成員與取得變數名稱字串的方法

首先匯入 members 模組, 然後呼叫其 list_members() 函式檢視檔案物件內容, 先檢視以唯讀模式開啟文字檔所傳回的 TXIOWrapper 物件 :

>>> import members   
>>> f=open('test.txt', 'r')             # 開啟文字檔
>>> members.list_members(f)   
buffer <class '_io.BufferedReader'>   
close <class 'builtin_function_or_method'>
closed <class 'bool'>
detach <class 'builtin_function_or_method'>
encoding <class 'str'>
errors <class 'str'>
fileno <class 'builtin_function_or_method'>
flush <class 'builtin_function_or_method'>
isatty <class 'builtin_function_or_method'>
line_buffering <class 'bool'>
mode <class 'str'>
name <class 'str'>
newlines <class 'NoneType'>
read <class 'builtin_function_or_method'>
readable <class 'builtin_function_or_method'>
readline <class 'builtin_function_or_method'>
readlines <class 'builtin_function_or_method'>
reconfigure <class 'builtin_function_or_method'>
seek <class 'builtin_function_or_method'>
seekable <class 'builtin_function_or_method'>
tell <class 'builtin_function_or_method'>
truncate <class 'builtin_function_or_method'>
writable <class 'builtin_function_or_method'>
write <class 'builtin_function_or_method'>
write_through <class 'bool'>
writelines <class 'builtin_function_or_method'>
>>> f.close()    

可見裡面包覆了 BufferedReader 物件. 

其次檢視以寫入模式開啟文字檔所傳回的 TXIOWrapper 物件 :

>>> f=open('test.txt', 'w')    
>>> members.list_members(f)     
buffer <class '_io.BufferedWriter'>    
close <class 'builtin_function_or_method'>
closed <class 'bool'>
detach <class 'builtin_function_or_method'>
encoding <class 'str'>
errors <class 'str'>
fileno <class 'builtin_function_or_method'>
flush <class 'builtin_function_or_method'>
isatty <class 'builtin_function_or_method'>
line_buffering <class 'bool'>
mode <class 'str'>
name <class 'str'>
newlines <class 'NoneType'>
read <class 'builtin_function_or_method'>
readable <class 'builtin_function_or_method'>
readline <class 'builtin_function_or_method'>
readlines <class 'builtin_function_or_method'>
reconfigure <class 'builtin_function_or_method'>
seek <class 'builtin_function_or_method'>
seekable <class 'builtin_function_or_method'>
tell <class 'builtin_function_or_method'>
truncate <class 'builtin_function_or_method'>
writable <class 'builtin_function_or_method'>
write <class 'builtin_function_or_method'>
write_through <class 'bool'>
writelines <class 'builtin_function_or_method'>
>>> f.close()    

可見裡面包覆了 BufferedWriter 物件.

接下來檢視以唯讀模式開啟二進位檔所傳回的 BufferedReader 物件 :

>>> f=open('test.dat', 'rb')     
>>> members.list_members(f)     
close <class 'builtin_function_or_method'>
closed <class 'bool'>
detach <class 'builtin_function_or_method'>
fileno <class 'builtin_function_or_method'>
flush <class 'builtin_function_or_method'>
isatty <class 'builtin_function_or_method'>
mode <class 'str'>
name <class 'str'>
peek <class 'builtin_function_or_method'>
raw <class '_io.FileIO'>
read <class 'builtin_function_or_method'>
read1 <class 'builtin_function_or_method'>
readable <class 'builtin_function_or_method'>
readinto <class 'builtin_function_or_method'>
readinto1 <class 'builtin_function_or_method'>
readline <class 'builtin_function_or_method'>
readlines <class 'builtin_function_or_method'>
seek <class 'builtin_function_or_method'>
seekable <class 'builtin_function_or_method'>
tell <class 'builtin_function_or_method'>
truncate <class 'builtin_function_or_method'>
writable <class 'builtin_function_or_method'>
write <class 'builtin_function_or_method'>
writelines <class 'builtin_function_or_method'>
>>> f.close()    

最後檢視以寫入模式開啟二進位檔所傳回的 BufferedWriter 物件 :

>>> f=open('test.dat', 'wb')   
>>> members.list_members(f)    
close <class 'builtin_function_or_method'>
closed <class 'bool'>
detach <class 'builtin_function_or_method'>
fileno <class 'builtin_function_or_method'>
flush <class 'builtin_function_or_method'>
isatty <class 'builtin_function_or_method'>
mode <class 'str'>
name <class 'str'>
raw <class '_io.FileIO'>
read <class 'builtin_function_or_method'>
read1 <class 'builtin_function_or_method'>
readable <class 'builtin_function_or_method'>
readinto <class 'builtin_function_or_method'>
readinto1 <class 'builtin_function_or_method'>
readline <class 'builtin_function_or_method'>
readlines <class 'builtin_function_or_method'>
seek <class 'builtin_function_or_method'>
seekable <class 'builtin_function_or_method'>
tell <class 'builtin_function_or_method'>
truncate <class 'builtin_function_or_method'>
writable <class 'builtin_function_or_method'>
write <class 'builtin_function_or_method'>
writelines <class 'builtin_function_or_method'>
>>> f.close()    

可見與 BufferedReader 內容是一樣的. 


3. 檔案物件的常用方法 : 

從上面的檢視中可知, 檔案物件有許多成員, 對於文字檔來說, 屬性中較常用的是紀錄編碼格式的 encoding, 而存取檔案則透過其方法, 存取文字檔時最常用的方法摘要如下表 : 
 

 檔案物件的常用方法 說明
 read([size]) 讀取指定長度 size 個字元, 預設讀取全部字元
 readline([size]) 從目前指標所在之列讀取指定長度 size 個字元, 預設讀取整列字元
 readlines() 讀取檔案中所有列, 傳回各列組成的串列
 write(str) 將字串 str 寫入檔案末尾, 傳回寫入的字元數
 close() 關閉檔案物件
 flush() 強制將緩衝區內資料立即寫入檔案並清除緩衝區
 readable() 測試檔案是否可讀取 (True/False)
 writable() 測試檔案是否可寫入 (True/False)
 seek(0) 將指標移到檔案最前面
 tell() 傳回檔案指標目前位置
 next() 將指標移到下一列


例如寫入文字檔 : 

>>> f=open('hello.txt', 'w')    
>>> f.write('Hello World!\n你是在說哈囉嗎?')    
21   
>>> f.encoding    
'cp950'   
>>> f.close()   

可見呼叫 write() 會傳回寫入的字元數, 在 Windows 系統預設編碼是 cp950 (BIG5). 這會在工作目錄下建立一個文字檔 hello.txt (注意, 寫完一定要關檔才會真正寫入) :




接著以唯讀模式開啟該檔案, 然後用 read() 讀取全部檔案內容 : 

>>> f=open('hello.txt', 'r')    
>>> f.read()     
'Hello World!\n你是在說哈囉嗎?'   
>>> f.close()   

read() 方法會一次讀完全部內容, 檔案內容指標嘿來到檔尾, 可呼叫 tell() 查詢目前指標位置, 這時再次呼叫 f.read() 將傳回空字串, 若要重新讀取只要呼叫 seek(0) 將指標指向檔首即可 : 

>>> f=open('hello.txt', 'r')      
>>> f.read()   
'Hello World!\n你是在說哈囉嗎?'   
>>> f.read()           # 已到檔尾, 再次讀取將傳回空字串 
''
>>> f.tell()             # 查詢目前檔案內容指標位置
29
>>> f.seek(0)         # 將檔案內容指標移到檔首
0
>>> f.tell()            # 查詢目前檔案內容指標位置 (已在檔首)
0
>>> f.read()          # 再次讀取到檔案內容
'Hello World!\n你是在說哈囉嗎?'
>>> f.close()  

呼叫 f.readline() 則是每次只讀回一列 : 

>>> f=open('hello.txt', 'r')      
>>> f.readline()        # 傳回第一列
'Hello World!\n'
>>> f.readline()         # 傳回第二列
'你是在說哈囉嗎?'
>>> f.readline()          # 已到檔尾傳回空字串
''
>>> f.close()  

也可以用 readlines() 讀取檔案的全部內容, 但它會將檔案拆分成一列一列放在串列中傳回 (包含列尾的跳行字元 '\n'), 以下為了避免呼叫 f.close() 的麻煩改用 with open as 語法 :

>>> with open('hello.txt', 'r') as f:     
    f.readlines()    
    
['Hello World!\n', '你是在說哈囉嗎?']

也可以用 for 迴圈走訪檔案物件來列印每列資料, 例如 :

>>> with open('hello.txt', 'r') as f:      
    for line in f:                      # 走訪檔案物件內容
        print(line, end='')      
        
Hello World!
你是在說哈囉嗎?

注意, 如果讀取時指定之 encoding 編碼與檔案寫入時不同會出現解碼錯誤, 例如存檔時使用 UTF-8 格式, 但讀取時沒有指定 encoding, 在 Windows 作業系統下預設會以 cp950 格式去解碼而產生錯誤, 例如 :

>>> f=open('hello.txt', 'w', encoding='utf-8')         # 以 utf-8 格式存檔
>>> f.write('Hello World!\n你是在說哈囉嗎?')      
21
>>> f.close()       
>>> f=open('hello.txt', 'r')         # 未指定 encoding : 預設以 cp950 開啟
>>> f.read()       
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
UnicodeDecodeError: 'cp950' codec can't decode byte 0xa0 in position 16: illegal multibyte sequence

>>> f.close()     

沒有留言 :