一、問題的來源以及網上的錯誤方法
最近遇到了一個問題,給定了一個Excel模板,修改表格裏面的內容,但是不能修改Excel表格的格式。用pywin32太慢,用xlrd只能讀,用xlwt只能寫。
很快,我查到了網上“修改Excel內容但保留格式”的方法,大概是需要用到另一個輔助的庫xlutils,併爲formatting_info參數配置爲True,代碼大概是這樣的:
import xlrd
import xlwt
from xlutils.copy import copy
rb = xlrd.open_workbook('open.xls', formatting_info=True)
wb = copy(rb)
sheet = wb.get_sheet(0)
for r in range(8):
for c in range(5):
sheet.write(r, c, 'R%sC%s'%(r+1, c+1))
wb.save('save.xls')
大部分格式的確是保住了,但是“修改了”的部分的格式慘不忍睹,一看這就是默認格式嘛。
於是繼續查找,發現xlwt.Worksheet(工作表)對象的write函數的用法參數說明爲:
write(r, c, label='', style=)
網上找到的教程也有說明,只要配置xlwt庫的XFStyle類,併爲write函數的第4個參數賦值,就可以爲新寫入的單元格設置單元格格式了。代碼類似於這樣,這段代碼實現了對RC位置的單元格設置了20號宋體、上下水平居中、四邊邊框:
style = xlwt.XFStyle()
style.font.height = 400
style.font.name = '宋體'
style.alignment.horz = xlwt.Alignment.HORZ_CENTER
style.alignment.vert = xlwt.Alignment.VERT_CENTER
style.borders.left = xlwt.Borders.THIN
style.borders.right = xlwt.Borders.THIN
style.borders.top = xlwt.Borders.THIN
style.borders.bottom = xlwt.Borders.THIN
sheet.write(r, c, 'R%sC%s'%(r+1, c+1), style)
然而衆所周知,xlwt庫只能寫不能讀,相當於我在設置單元格的樣式的,原表格的樣式對我是“盲”的。雖然我可以“手動”地記下來所有單元格的格式,然後“抄”過去,但是這太麻煩太繁瑣了。
於是繼續網上查找資料,全部都是答非所問的結果,要麼告訴我“怎麼複製原表格而保留樣式”,要麼告訴我“如何修改單元格同時配置格式”,但沒有一個是回答“如何修改單元格而保留格式”的。
二、根據屬性名猜可以用的函數
沒辦法,代碼小白的我,只好繼續猜,猜猜看那個函數能做到這樣的配置。
首先我嘗試爲工作表對象sheet的sheet.write函數的第4個參數配置“默認”項,企圖讓它“write”單元格的時候“不修改”格式,比如False啊、0啊、None啊、-1啊,都試過了,全部報錯。
那麼嘗試找xlrd讀取的單元格的cell對象,看看裏面有沒有存儲格式信息。但是cell對象中的屬性不多,很快就爬乾淨了,沒有存儲格式這樣如此“細緻”的內容。
線索斷了,我只好窮舉用xlrd讀取的工作簿rb的所有屬性函數,突然發現了一個看起來很像記錄格式的東西,rb.xf_list,其中是一個大列表,裏面記錄的全是“xlrd.formatting.XF”類型的對象,而字體格式的對象名字不是叫做“XFStyle”嗎,這可是非常一致了。
這可是一個很大的突破,我猜想Excel中的單元格格式,不是分別記錄到各個單元格的對象的屬性中,而是將工作簿中出現的樣式彙總,再通過另一個方法把各個單元格設定的格式的“索引編號”讀出來,於是就做到了記錄各個單元格的格式信息。
得到了線索就有了進展,我找到xlrd讀取的工作表sheet對象有一個sheet.cell_xf_index(rowx, colx)函數,返回結果是一個數值,而且針對相同格式的單元格佈局,這個結果在相同格式單元格中計算返回的索引編號很一致!那麼結果肯定是這個了!
於是我急急忙忙把rb.xf_list[sheet.cell_xf_index(rowx, colx)]賦值到了sheet.write函數的style參數上,期待奇蹟的發生。
三、瞎貓碰不到死耗子,那就硬着頭皮爬源碼
果不其然,事情的進展不會這麼順利,果然出現了報錯。
我突然想起來了write函數的說明,style的默認參數設置是“style=”,而我賦值的是“xlrd.formatting.XF”對象,這都不是一個庫的東西,怎麼能直接用呢。
但是我堅定一個信念,既然通過xlutils.copy庫的copy函數可以把格式“複製”過去,那麼肯定在某個時候發生了讀取和寫入,這個格式讀寫的“管道”肯定是通的,關鍵是我要找到它。
於是我撐住頭皮,開始爬xlutils庫的代碼,xlutils.copy庫的內容很乾淨,代碼只有這些:
from xlutils.filter import process,XLRDReader,XLWTWriter
def copy(wb):
w = XLWTWriter()
process(
XLRDReader(wb,'unknown.xls'),
w
)
return w.output[0][1]
於是轉頭去爬這裏引用了的xlutils.filter庫。既然知道了rb.xf_list存儲了xlrd格式的“單元格樣式”,那麼就找找看它什麼時候轉化爲了xlwt格式的“單元格樣式”。
按照關鍵詞搜索,果然找了一系列的判斷和轉換,代碼段大概是這樣的:
for rdxf in rdbook.xf_list:
wtxf = xlwt.Style.XFStyle()
... 各種判斷和轉換
self.style_list.append(wtxf)
那麼很顯然,我要找的就是self.style_list了。
那麼按理說,我只要獲取到“self.style_list”的“self”,也就是它的父對象“BaseWriter”的實例,就能獲取到這個屬性了(也只有這個方法)。但是我驚訝地發現BaseWriter裏面有一個close方法,裏面赫然寫着“del self.style_list”,這可了得,我雖然不知道它是在什麼時候調用了,但是一旦調用了,那不就功虧一簣了,這個列表刪掉了那不就全完了。
再看看xlutils.copy庫裏的內容,找到對應的定義:
class XLWTWriter(BaseWriter):
def __init__(self):
self.output = []
def close(self):
if self.wtbook is not None:
self.output.append((self.wtname,self.wtbook))
del self.wtbook
最終copy函數中的定義中返回的是“w.output[0][1]”,那實際上就是“self.wtbook”這個東西了。
接着找相關定義,可以看到這樣一段:
...
self.wtbook = xlwt.Workbook(style_compression=2)
self.wtbook.dates_1904 = rdbook.datemode
self.wtname = wtbook_name
self.style_list = []
...鄭州婦科醫院 http://m.zyfuke.com/
這可麻煩了呀,wtbook和style_list是BaseWriter類下的兩個平級的屬性,並沒有相互的聯繫,並且wtbook是一個xlwt.Workbook類型的對象,自然不可能提供獲取其父對象的方法(確認了屬性也確實沒有),這可咋辦,進度又陷入了停滯。
於是我又靈機一動,“copy”函數雖然是已經封裝好的,但是我也可以把它拆開,就比方說這樣:
from xlutils.filter import process, XLRDReader, XLWTWriter
rb = xlrd.open_workbook('open.xls', formatting_info=True)
w = XLWTWriter()
process(XLRDReader(rb, 'unknown.xls'), w)
wb = w.output[0][1]
w是一個XLWTWriter類的對象,而XLWTWriter繼承於BaseWriter,BaseWriter有style_list屬性。那麼我嘗試訪問其style_list屬性,也就是“w.style_list”,並按照之前猜想的方法,將sheet.cell_xf_index函數獲取到的每個單元格的對應數字,認爲是“w.style_list”列表中的查詢單元格樣式的序列號,寫入程序:
style_list = w.style_list
sheet2 = wb.get_sheet(0)
style = style_list[sheet.cell_xf_index(r, c)]
sheet2.write(r, c, sheet.cell_xf_index(r, c), style)
再次打開生成的保存文件,發現格式完美地保留了下來,而內容卻如我設定地修改了,至此,程序調試任務完成!
四、終於可以運行的完整代碼
完整樣例代碼是:
import xlrd
from xlutils.filter import process, XLRDReader, XLWTWriter
rb = xlrd.open_workbook('open.xls', formatting_info=True)
# 參考xlutils.copy庫內的用法 參考xlutils.filter內的參數定義style_list
w = XLWTWriter()
process(XLRDReader(rb, 'unknown.xls'), w)
wb = w.output[0][1]
style_list = w.style_list
for n, sheet in enumerate(rb.sheets()):
sheet2 = wb.get_sheet(n)
for r in range(sheet.nrows):
for c, cell in enumerate(sheet.row_values(r)):
style = style_list[sheet.cell_xf_index(r, c)]
sheet2.write(r, c, sheet.cell_xf_index(r, c), style)
wb.save('save.xls')