2022年9月3日 星期六

Python 學習筆記 : 用 csv 模組讀寫 csv 檔

CSV (Comma Separated Values, 逗號分隔值) 檔是副檔名為 .csv, 預設以逗號隔開的純文字檔案, 它是匯出 Excel 試算表或 MySQL 資料表這種二維結構化表格式資料時最常用的格式, 也是試算表或資料庫軟體都有支援的通用格式. 

CSV 檔第一列通常是資料表的欄位名稱, 第二列以後則是各列資料 (紀錄, record), 不論欄名或紀錄, 預設各欄都是以逗號隔開的, 例如下面這個台積電近五日成交紀錄的 CSV 檔 :





可見在 GitHub 上 csv 檔會以表格方式呈現, 按 "Raw" 鈕會顯示檔案原始內容, 按滑鼠右鍵點選 "另存新檔" 即可下載該檔案 : 




在 Window 點擊 csv 檔預設會以 Excel 開啟 :




也可以用純文字編輯器如記事本開啟 : 




下載內容如下 :

date,open,high,low,close,capacity
2021-10-30,595,596,590,590,"27,684,673"
2021-10-29,598,598,593,595,"16,927,154"
2021-10-28,598,599,594,599,"16,260,278"
2021-10-27,595,600,591,599,"27,047,187"
2021-10-26,597,597,589,593,"17,724,968"

可見 CSV 檔只是簡化版的試算表 (只有一個工作表), 其值沒有型別資訊, 也沒有字型與色彩等格式資訊, 全部內容都是字串, 因此數值欄位之資料在進行計算之前必須用 int(), float(), 或 double() 轉換成數值後再處理. 

雖然可以利用一般檔案存取與 split() 等內建函式來處理 CSV 檔, 但那樣做很繁瑣 (例如當儲存格內容有逗號時需使用轉義字元等), 事實上 Python 標準函式庫裡有一個內建模組 csv 可讓 CSV 檔的處理更簡單, 不須安裝只要 import 即可使用 :

import csv 

例如 :

>>> import csv   
>>> dir(csv)      
['Dialect', 'DictReader', 'DictWriter', 'Error', 'OrderedDict', 'QUOTE_ALL', 'QUOTE_MINIMAL', 'QUOTE_NONE', 'QUOTE_NONNUMERIC', 'Sniffer', 'StringIO', '_Dialect', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__version__', 'excel', 'excel_tab', 'field_size_limit', 'get_dialect', 'list_dialects', 're', 'reader', 'register_dialect', 'unix_dialect', 'unregister_dialect', 'writer']

其常用函式如下 : 


 csv 模組常用函式 說明
 reader(f [, delimiter]) 建立一個 reader 物件以便從檔案參考 f 讀取 CSV 檔
 writer(f [, delimiter]) 建立一個 writer 物件以便將資料寫入 CSV 檔
 DictReader() 建立一個 DictReader 物件以便從檔案參考 f 讀取 CSV 檔
 DictWriter() 建立一個 DictWriter 物件以便將資料寫入 CSV 檔


其中 reader() 與 writer() 用來讀寫 CSV 檔 (串列型態), 而 DictReader() 與 DictWriter() 則支持以字典的格式來讀寫 CSV 檔.

csv.reader() 會傳回一個 reader 物件, 其常用參數如下 :

csv.reader(f [, delimiter=','] [, quotechar=''])   

必要參數 f 為所開啟的 CSV 檔案參考, 參數 delimiter 為欄位分隔字元 (預設為逗號), 參數 quotechar 為指定引號字元 (用於轉義欄位, 預設為空字元). 

csv.writer() 會傳回一個 writer 物件, 其常用參數如下 :

csv.writer(f [, delimiter=","] [, quotechar=""] [, quoting=csv.QUOTE_MINIMAL])   

必要參數 f 為所開啟的 CSV 檔案參考, 參數 delimiter 為欄位分隔字元 (預設為逗號), 參數 quotechar 為指定引號字元 (用於轉義欄位), 參數 quoting 為指定引號套用範圍, 可用 csv,QUOTE_MINIMAL (僅套用於與分隔字元衝突的欄位, 預設) 或 csv.QUOTE_ALL (套用於全部欄位). 此函式會傳回一個 writer 物件, 寫入資料是利用此物件的三個方法 : 
  • writerow(一維串列) : 將一維串列輸出到 CSV 檔案的最後面 (寫入單列) 
  • writerows(二維串列) : 將二維串列輸出到 CSV 檔案的最後面 (寫入多列)
csv.DictReader() 會傳回一個 DictReader 物件, 它是一個迭代器物件, 它是將 CSV 檔中的每一列以 fieldnames 所指定之欄位為 key 轉成字典, 只要迭代此物件即可取得每列轉成之字典, 可用 [key] 存取欄位資料, 常用參數如下 :

csv.DictReader(f [, fieldnames=None] [, restkey=None] [, restval=None])   

必要參數 f 為所開啟的 CSV 檔案指標, 參數 fieldnames 為欄位名稱序列 (可用 tuple 或 list 表示), 若沒有傳入 fieldnames 則會把 CSV 檔的第一列當作欄位名稱. 參數 restkey 用來指定剩餘欄位的key, 當一列中的實際欄位比 fieldnames 所指定的還多時, 多出來的欄位資料全部會被放在串列中並儲存於 restkey 所指定之鍵. 參數 restval 用來指定遺漏值 (missing values), 當一列中的實際欄位比 fieldname 所指定的還少時, 這些遺漏的欄位值就會以 restval 來填補. 

csv.DictWriter() 會傳回一個 DictWriter 物件, 其常用參數如下 :

csv.DictWriter(f [, fieldnames=None] [, restkey=None] [, restval=None])   

必要參數 f 為所開啟的 CSV 檔案指標, 參數 fieldnames 為欄位名稱序列 (可用 tuple 或 list 表示), 用來指定傳給 writerow() 方法. 

DictWriter 物件的 writeheader() 方法可用來將 fieldnames 寫入標題列, 而 writerow() 方法則可將字典寫入內容列, 參考 : 



1. 使用 csv.reader() 讀取 CSV 檔 : 

與讀取一般 TXT 文字檔一樣使用內建函式 open() 開啟 CSV 檔, 它會傳回一個檔案指標, 將此參考傳入 csv.reader() 函式即可, 它會傳回一個 reader 物件, 此物件為一個迭代器 (iterator), 其內容就是檔案內容裡的各列資料, 只要用 for 迴圈走訪 reader 物件即可逐獵取得檔案內容, 也可以傳入 內建函式 list() 轉成串列, 例如 : 

>>> import csv
>>> f=open("2330.TW.csv", "r")             # 開啟 CSV 檔, 傳回檔案指標
>>> reader=csv.reader(f)                           # 將檔案指標傳入 csv.reader() 
>>> type(reader)                                           # csv.reader() 傳回一個 csv.reader 物件
<class '_csv.reader'>
>>> hasattr(reader, "__next__")                 # 檢查 reader 物件是否為迭代器 (是)
True   
>>> dir(reader)                                             # reader 物件的成員
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'dialect', 'line_num']   


[['date', 'open', 'high', 'low', 'close', 'capacity'], ['2021-10-30', '595', '596', '590', '590', '27,684,673'], ['2021-10-29', '598', '598', '593', '595', '16,927,154'], ['2021-10-28', '598', '599', '594', '599', '16,260,278'], ['2021-10-27', '595', '600', '591', '599', '27,047,187'], ['2021-10-26', '597', '597', '589', '593', '17,724,968']]

使用 hasattr() 函式檢查 reader 物件有 __next__() 方法, 直接呼叫 dir() 檢查 reader 物件的成員中也有 __next__() 方法, 可見它是一個迭代器. 注意, reader 物件成員中有一個 line_num 屬性是用來記錄列編號的 (注意是 1 起始). 

既然 csv.reader 物件是一個迭代器, 則可用 next() 函式, for 迴圈或其簡潔版的串列生成式來走訪其元素 :

>>> for row in reader:                            # 走訪 reader 迭代器
    print(reader.line_num, " : ", row)     # 輸出列編號與列內容 (串列)
    
1 : ['date', 'open', 'high', 'low', 'close', 'capacity']
2 : ['2021-10-30', '595', '596', '590', '590', '27,684,673']
3 : ['2021-10-29', '598', '598', '593', '595', '16,927,154']
4 : ['2021-10-28', '598', '599', '594', '599', '16,260,278']
5 : ['2021-10-27', '595', '600', '591', '599', '27,047,187']
6 : ['2021-10-26', '597', '597', '589', '593', '17,724,968']    
>>> fp.close()                             # 關閉檔案 (必須在走訪完畢才能關閉)

可見 csv.reader() 讀取 CSV 檔案後會將每一列內容放進串列中儲存, 所以若要取得第一欄的內容可用 row[0] 取得, 其餘類推. 也可以將 reader 物件傳給 list() 函式轉成二維串列, 這樣就可以用

注意, 走訪迭代器完畢後其內容會耗盡, 再次走訪將沒有資料, 想要再次走訪必須從頭 (重新開啟檔案) 才行, 例如下面是從頭開始但改為呼叫 next() 來走訪迭代器 : 

>>> f=open("2330.TW.csv", "r")                        # 開啟 CSV 檔, 傳回檔案指標
>>> reader=csv.reader(f)                                      # 將檔案指標傳入 csv.reader() 
>>> next(reader)                                                      # 傳回下一個元素 (列)
['date', 'open', 'high', 'low', 'close', 'capacity']
>>> next(reader)                                                      # 傳回下一個元素 (列)
['2021-10-30', '595', '596', '590', '590', '27,684,673']
>>> next(reader)                                                      # 傳回下一個元素 (列)
['2021-10-29', '598', '598', '593', '595', '16,927,154']
>>> next(reader)                                                      # 傳回下一個元素 (列)
['2021-10-28', '598', '599', '594', '599', '16,260,278']
>>> next(reader)                                                      # 傳回下一個元素 (列)
['2021-10-27', '595', '600', '591', '599', '27,047,187']
>>> next(reader)                                                      # 傳回下一個元素 (列)
['2021-10-26', '597', '597', '589', '593', '17,724,968']
>>> next(reader)                                                      # 迭代器內容耗盡拋出例外
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
StopIteration   
>>> fp.close()                             # 關閉檔案 (必須在走訪完畢才能關閉)
 
以上是直接使用 open() 開啟檔案, 必須在走訪完畢後呼叫 close() 釋放資源. 實務上通常會用 with open, 這樣就不需要手動關檔, with 語句會自動關檔. 其次, 呼叫 open() 時最好要用 encodidng 參數指定編碼格式, 特別是 CSV 檔內含有非 ASCII 字元時必須指定 (中文通常使用 "utf8"), 否則會出現錯誤. 

例如將上面的 CSV 檔標題改為中文, 然後以 utf8 編碼存成 2330.TW2.csv 檔 :

日期,開盤價,最高價,最低價,收盤價,成交量
2021-10-30,595,596,590,590,"27,684,673"
2021-10-29,598,598,593,595,"16,927,154"
2021-10-28,598,599,594,599,"16,260,278"
2021-10-27,595,600,591,599,"27,047,187"
2021-10-26,597,597,589,593,"17,724,968" 

若用 with open 語句開啟此檔案時沒有用 encoding 參數指定編碼格式會出現解碼錯誤 : 

>>> import csv
>>> with open('2330.TW2.csv', 'r') as f:       # 沒有指定 encoding
    reader=csv.reader(f)   
    for row in reader:    
        print(reader.line_num, " : ", row)    
        
Traceback (most recent call last):
  File "<pyshell>", line 3, in <module>   
UnicodeDecodeError: 'cp950' codec can't decode byte 0xe6 in position 0: illegal multibyte sequence
 
由於 2330.TW2.csv 是以 UTF-8 格式編碼, 因此用 open() 開啟檔案時若未指定 encoding 參數為 'utf8', 則預設會以 ASCII 編碼開啟, 結果因為編碼不符而報錯, 若加上 encoding='utf8' 即可正常讀取 : 

>>> with open('2330.TW2.csv', 'r', encoding='utf8') as f:    # 指定 encoding 為 utf8
    reader=csv.reader(f)    
    for row in reader:     
        print(reader.line_num, " : ", row)    
        
1  :  ['日期', '開盤價', '最高價', '最低價', '收盤價', '成交量']
2  :  ['2021-10-30', '595', '596', '590', '590', '27,684,673']
3  :  ['2021-10-29', '598', '598', '593', '595', '16,927,154']
4  :  ['2021-10-28', '598', '599', '594', '599', '16,260,278']
5  :  ['2021-10-27', '595', '600', '591', '599', '27,047,187']
6  :  ['2021-10-26', '597', '597', '589', '593', '17,724,968']

可見開啟檔案時用 encoding 參數指定正確的編碼是非常重要的. 

完整的 CSV 檔讀取程式如下 : 

import csv 
with open('2330.TW2.csv', 'r', encoding='utf8') as f:      
    reader=csv.reader(f)   
    for row in reader:    
        print(reader.line_num, " : ", row) 


2. 使用 csv.writer() 寫入 CSV 檔 : 

欲將資料寫入 CSV 檔須以 "w" 模式開啟檔案, 然後將取得之檔案指標傳入 csv.writer() 函式, 它會傳回一個 writer 物件, 呼叫此物件的 writerow() 方法即可將以串列表示的列資料寫入 CSV 檔中. 注意, 在 Windows 系統必須於呼叫 open() 函式時傳入 newline='' 參數, 否則寫入每列之間會多一個空白列. 例如 :

>>> import csv   
>>> with open('2330.TW3.csv', 'w') as f:       # 沒有指定 newline 參數
  

(續)

參考 : 


沒有留言 :