# <font face='consolas' size=5 color=#000080><center>**Excel檔案讀寫練習程式**</center></font> 難度:★★進階 台北市文山社大2017暑期Python入門課,老師教了openpyxl這個好用的library,功能是讀寫Excel檔案。限於時間,授課內容點到即止,不過對簡單的操作已夠用。課後我再研究好一陣子,以下是心得和測試成果。 :::success 我的習慣是文章和程式源碼post上後,會不定期檢查修改,務求文字精練準確,內容完備。本文亦不例外,內容將不斷更新。 ::: 第一步,欲使用openpyxl,得先安裝這個library。方法和安裝其他Python的libraries一樣,用pip就是: >pip install openpyxl 此外,在程式中也要import此library的相關模組。 :::info 提示:openpyxl讀寫的Excel檔,是Microsoft Office 2007以後副檔名為xlsx格式的檔案,並非早期的xls。 ::: --- ## 讀取檔案內容(含格式) 讀取Excel檔案,社大課程只教讀取檔案的內容(資料),一般網站的教學也是這樣。但單單讀取資料並不能滿足我的求知欲。特地花些時間研究測試,找出在讀取內容之外,也<font color=red>可以讀取大部分**格式(style)**</font>的方法。 :::info * 這裡的「格式」是指工作表名稱標籤的顏色以及儲存格的各項格式,如數值、對齊方式、字型、外框、保護等。 * 說「大部分」而非「全部」,是由於部分格式例如原儲存格的長寬等未能讀取。努力中... ::: 以下是欲讀取的Excel檔案擷圖以及<font color=red>**讀取檔案內容/格式**</font>的程式源碼。這個範例Excel檔有四個工作表,為節省篇幅,僅貼第一個工作表的擷圖。程式則讀取所有工作表。 ![](https://i.imgur.com/D7prmtJ.png =500x) ```python= # -*- coding: utf-8 -*- """ Excel(Xlsx)檔案讀取練習。 """ import sys from openpyxl import load_workbook from copy import copy """ 名 稱:storeXlsxContent() 功 能:將Excel檔案(副檔名xlsx)的資料/格式讀出並儲存於list中。 傳入參數: str inFile: 欲讀取的檔案檔名(可含路徑)字串。 bool dataOnly: 是否儲存cell的值而非公式。True表存值,False表存公式。預設存公式。 bool isStoreStyle: 是否儲存各工作表及儲存格格式。True表儲存,False不存。預設不存。 傳 回 值: list wbContent。這個list內容為從檔案讀取到的資料及(如有)其格式,wsContent規格為: . 每個元素都是tuple,代表一張worksheet「工作表」。 . 每張工作表tuple的元素固定為3個dict's,分別為: . dict 0: 六個keys。第一為工作表名稱('wsName'),第二個以後分別是'wsSheetFormat', 'wsSheetProperties', 'wsSheetState', 'wsViews'和'wsSortState', 記錄該工作表整體的屬性、格式、狀態等等。 . dict 1: 每個cell(儲存格)的資料。keys是cell的名稱,如 'A1', 'C3'等,value則是 該cell的資料,即內容。 . dict 2: 每個cell的格式。keys是cell的名稱,如 'A1', 'C3'等,value則是該cell 的格式。格式冗長複雜,我一時間難以消化搞懂。反正還原時copy過去就是。 """ def storeXlsxContent(inFile, dataOnly=False, isStoreStyle=False): # . dataOnly是控制「僅儲存各cells的值」或「存其公式」(如有)的flag。 # . True表示cells如有公式,僅存由公式計算出來的值而不存公式本身。False則表示須存公式。 # . openpyxl好像只在load_workbook()函式提供這個選項,所以存值或存公式的是屬於整個 # workbook層次,無法做到不同cells有不同的選擇。如有此需要,請開兩個wb's,一存值 # 一存公式,視需要靈活運用。例如: # wbValue = load_workbook(inFile, data_only=True) # wbFomula = load_workbook(inFile, data_only=False) wb = load_workbook(inFile, data_only=dataOnly) wbContent = [] # wbContent是將要傳回的list。 # 取得一個list,其內容為工作表名稱字串。 wsNames = wb.get_sheet_names() for wsName in wsNames: # wsNames是個list。 # 利用工作表名稱(wsName)來載入實際的工作表(ws)。 ws = wb.get_sheet_by_name(wsName) if isStoreStyle: sheetFormat = ws.sheet_format sheetProperties = ws.sheet_properties sheetState = ws.sheet_state views = ws.views sortState = ws.sort_state else: sheetFormat = None sheetProperties = None sheetState = None views = None sortState = None # . wbContent這個list的規格是:每一個元素都是tuple,代表一張工作表。 # 每張工作表tuple的元素固定為3個dict's。 # . 按照以上規格,在list中先append一個tuple,再在tuple內放3個dict's。目前 # 僅在第一個dict(dict 0)存入資料(工作表的整體資訊),其餘兩個暫時空置。 wbContent.append(({'wsName':wsName, 'wsSheetFormat':sheetFormat, 'wsSheetProperties':sheetProperties, 'wsSheetState':sheetState, 'wsViews':views, 'wsSortState':sortState}, {}, {})) wsID = len(wbContent) - 1 # 工作表編號(從0起)。 # 剖析這個工作表。 for row in ws.rows: # 表內逐列處理。 for cell in row: # 列內逐cell處理。 # 檢查cell的內容,有資料(非None)才存入wbContent的dict 1中。 if cell.value != None: wbContent[wsID][1].update({(cell.column + str(cell.row)): cell.value}) # isStoreStyle為True(即「要讀取並儲存格式」)時,再檢查該儲存格實際上 # 有無設定「格式」(style)。有設定格式,就存入wbContent的dict 2中。 if isStoreStyle and cell.has_style: wbContent[wsID][2].update({(cell.column + str(cell.row)): getStyle(cell)}) return wbContent """ 名 稱:getStyle() 功 能:取得cell儲存格的格式。 傳入參數: cell: 儲存格,型態為 <class 'openpyxl.cell.cell.Cell'>。 bool font=True: 是否擷取 font。 bool border=True: 是否擷取 border。 bool fill=True: 是否擷取 fill。 bool number_format=True: 是否擷取 number_format=True。 bool protection=True: 是否擷取 protection。 bool alignment=True 是否擷取 alignment。 傳 回 值:dict style。keys為'font', 'border'...等儲存格格式。 """ def getStyle(cell, font=True, border=True, fill=True, number_format=True, protection=True, alignment=True): style = {} if font: style['font'] = cell.font if border: style['border'] = cell.border if fill: style['fill'] = cell.fill if number_format: style['number_format'] = cell.number_format if protection: style['protection'] = cell.protection if alignment: style['alignment'] = cell.alignment return style # 請先確認在下列路徑有此xlsx檔案,或自行修改路徑和檔名。 content1 = storeXlsxContent('D:\\Python\\粵語長片.xlsx', isStoreStyle=True) ``` --- ## 寫入檔案(含格式) 成功讀出內容及格式後,該測試寫入到另一個Excel檔案了。以下是寫入檔案的程式源碼。這個程式完全沒有修改原Excel檔的內容和格式,是「保持原貎」重建。當然這只是練習。如果真要原封不動還原,乾脆用拷貝檔案就好,程式源碼大概只需一行,既快又保證完全一樣。實務上很可能會做些改變,也許更改工作表名稱,或者修改、新增、刪除某些儲存格的內容,又或者只寫入部分內容。修改內容的程式碼我放在後面再介紹。 另外,我建議如果要修改content list,最好改「資料」就好,list中的「格式」很複雜,改錯了怕會當機。 :::info 以下的code snippet實際上是接續上面的程式碼。 ::: ```python=+ """ Excel(Xlsx)檔案寫入練習。 """ """ 名 稱:WriteContent2Xlsx() 功 能:將之前用storeXlsxContent()函式讀取某xlsx檔而產生,或自行製造的來源list     (內有資料/格式)寫入到指定的xlsx檔。 傳入參數: str outFile: 欲寫入的檔名(可含路徑)字串。 list wbContent: 存放Excel資料/格式的list,就是寫入的資料來源。 bool isRestoreStyle: . 存檔時是否還原工作表及各儲存格的格式。True要還原,False不用還原。 . 如果選擇還原,wbContent當初必須有儲存格式。如當初未存格式,當然無法還原,不過程式 不會當機,目前版本也不產生警告訊息。 傳 回 值:無。 """ def WriteContent2Xlsx(outFile, wbContent, isRestoreStyle=False): from openpyxl import Workbook from copy import copy wb = Workbook() wb.remove_sheet(wb.active) # 先刪除預設的那個sheet(因為要用自己的)。 # 第一層:restore工作表的整體資訊。 for wsID in range(len(wbContent)): # 產生工作表。名稱不用預設,而使用在list上記錄的工作表名。 ws = wb.create_sheet(title=wbContent[wsID][0]['wsName']) if isRestoreStyle: # list內tuple的dict 0有'wsViews'這個key,而且其value不是None, # 才要restore回去。實際上用copy()來restore。以下幾個if's同理。 if 'wsViews' in wbContent[wsID][0] \ and wbContent[wsID][0]['wsViews'] != None: ws.views = copy(wbContent[wsID][0]['wsViews']) if 'wsSheetProperties' in wbContent[wsID][0] \ and wbContent[wsID][0]['wsSheetProperties'] != None: ws.sheet_properties = copy(wbContent[wsID][0]['wsSheetProperties']) if 'wsSheetFormat' in wbContent[wsID][0] \ and wbContent[wsID][0]['wsSheetFormat'] != None: ws.sheet_format = copy(wbContent[wsID][0]['wsSheetFormat']) if 'wsSheetState' in wbContent[wsID][0] \ and wbContent[wsID][0]['wsSheetState'] != None: ws.sheet_state = copy(wbContent[wsID][0]['wsSheetState']) if 'wsSortState' in wbContent[wsID][0] \ and wbContent[wsID][0]['wsSortState'] != None: ws.sort_state = copy(wbContent[wsID][0]['wsSortState']) # 第二層:restore工作表中的cells。 # restore data。先檢查這個工作表tuple是否最少有2個 dict's,確定OK後才 # restore資料。檢查的原因是怕自行製造的wbContent可能沒有dict 1 和 dict 2。 if len(wbContent[wsID]) >= 2: for key, value in wbContent[wsID][1].items(): ws[key] = value # restore style。同樣道理,先檢查一下是否有dict 2。 if isRestoreStyle and len(wbContent[wsID]) >= 3: for key, style in wbContent[wsID][2].items(): restoreCellStyle(ws[key], style) wb.save(outFile) # 存檔。 """ 名 稱:restoreCellStyle() 功 能:還原儲存格的格式。 傳入參數: cell: 儲存格,型態為 <class 'openpyxl.cell.cell.Cell'>。 dict style: 儲存格格式。 傳 回 值:無。 """ def restoreCellStyle(cell, style): if 'font' in style and style['font'] != None: cell.font = copy(style['font']) if 'border' in style and style['border'] != None: cell.border = copy(style['border']) if 'fill' in style and style['fill'] != None: cell.fill = copy(style['fill']) if 'number_format' in style and style['number_format'] != None: cell.number_format = copy(style['number_format']) if 'protection' in style and style['protection'] != None: cell.protection = copy(style['protection']) if 'alignment' in style and style['alignment'] != None: cell.alignment = copy(style['alignment']) # 這裡的content1就是前面storeXlsxContent()傳回,存放資料/格式的那個list。 WriteContent2Xlsx(r'D:\Python\New粵語長片.xlsx', content1, isRestoreStyle=True) ``` 寫入的Excel檔的第一個工作表擷圖如下,內容及格式都和原檔相同: ![](https://i.imgur.com/OcOuE7A.png =500x) 其實要使用WriteContent2Xlsx()函式寫入檔案,不一定要先從某個Excel檔讀取內容。只要自行製造一個符合規格的wsContent list就行。以下是示範的code snippet。這段code只寫入資料,格式過於複雜,暫不考慮。 ```python= content = [({'wsName':'First Sheet'},{'A1':23.4,'B1':56.9,'C1':'=SUM(A1:B1)'}), ({'wsName':'第二工作表'},{'A1':'台北市','B2':'文山','C3':'社大'}, {})] WriteContent2Xlsx('D:\\Python\\MyTest.xlsx', content, isRestoreStyle=False) ``` 以上code snippet造了一個叫做content的list,放入兩個tuple's,代表兩個工作表。 第一個工作表tuple放兩個dict's,第一個dict定義工作表名稱,第二個記錄欲寫入的儲存格名稱及內容。因不寫入格式,可以不必放置第三個dict: >({'wsName': 'First Sheet'}, {'A1': 23.4, 'B1': 56.9, 'C1': '=SUM(A1:B1)'}) 第二個工作表tuple示範用三個dict's。因不寫入格式,第三個dict其實是家徒二壁({}),空空如也,和第一個工作表省略第三個dict的作法效果相同: >({'wsName': '第二工作表'}, {'A1': '台北市', 'B2': '文山', 'C3': '社大'}, {}) 製造出來的MyTest.xlsx長這樣: ![](https://i.imgur.com/VFeDU9z.png =400x) ![](https://i.imgur.com/uDUj2BM.png =300x) --- ## 修改儲存格內容及格式 以上介紹了讀取檔案內容/格式及將之寫入其他檔案。但如果只是想修改一下檔案的部分內容/格式,又當如何處理呢? openpyxl並沒有提供直接修改檔案的功能。要修改,採用的也是上面的讀檔及寫檔程序,只不過讀和寫同一個檔案吧了。 以下是code snippet: ```python= # -*- coding: utf-8 -*- from openpyxl import load_workbook file = 'd:\\Python\\MyTest.xlsx' wb = load_workbook(file) c = wb['第二工作表']['B2'] c.value = '士林' wb.save(file) ``` 修改前的「第二工作表」內容是: ![](https://i.imgur.com/pJk7vcW.png =300x) 修改後: ![](https://i.imgur.com/aRqsA6v.png =300x) 以下code snippet修改儲存格的格式。可以修改現有檔案,也可以在寫入資料到新檔時一道設定格式: ```python= # -*- coding: utf-8 -*- from openpyxl import load_workbook from openpyxl.styles import Font from openpyxl.styles import Alignment file = 'd:\\Python\\MyTest.xlsx' wb = load_workbook(file) # 字型、顏色等。 fontCity = Font(name='華康龍門石碑', size=20, bold=True, italic=False, vertAlign=None, underline='double', strike=False, color='006400') # 對齊方式。 alignmentCity = Alignment(horizontal='center', vertical='center') fontDist = Font(name='文鼎特明', size=16, bold=False, italic=True, vertAlign='baseline', underline='none', strike=False, color='8A2BE2') alignmentDist = Alignment(horizontal='left', vertical='top') city = wb['第二工作表']['A1'] city.font = fontCity city.alignment = alignmentCity dist = wb['第二工作表']['B2'] dist.font = fontDist dist.alignment = alignmentDist wb.save(file) ``` 改變儲存格字型、顏色、對齊方式之後工作表長相如下: ![](https://i.imgur.com/KIY1o82.png =400x) 暫時到此為止。以後有新料再續。 ###### tags: `Excel` `style` `格式`