最近抽空研究了一下 基於DocumentFormat.OpenXml操作Excel,也把自己的理解記錄下來,便於日后可以查閱。
各種系統中,導出Excel是一種很常見的功能,在C#/.Net 環境下, 市面上存在很多相關的類庫,開源免費的有基於JAVA的POI移植過來的 NPOI、 EPPlus、OpenXML 等等;也有收費的 如 Aspose.Cells、Spire.XLS 等。
收費的商業組件在性能,功能上會一般來說都會比較好,例如Aspose.Cells,在性能上是比較卓越的,價格最便宜也要1000美金左右。 但是大部分公司,特別是初創公司,考慮到成本問題,用得比較多的,可能還是會使用開源免費的 NPOI 和 EPPlus,而OpenXML(DocumentFormat.OpenXml) 是微軟官方推出的一個操作Excel, Word, PPT文件的組件,而且操作的是更為底層的部分,能夠做到靈活和精確的控制,但是本身操作起來會比較復雜,有些順序的限制,會感覺沒有 NPOI 和 EPPlus 方便,而且生成出來的excel搞不好還會提示錯誤(有可能WPS打開不會,office打開會),而且性能方面雖然操作底層,但是也要看使用者本身,不一定性能可以很高。所以本身是比較少人會考慮使用它。
這個組件為啥叫 OpenXML SDK,因為從07版本之后,office系列的文檔,包括Excel, Word, PPT這些文檔,都是使用XML方式存儲的。 而 Office 的 Open XML 文件格式規范是一個開放的國際性標准 ,是 ECMA 376 標准, 可以查閱這個網址:https://www.ecma-international.org/publications/standards/Ecma-376.htm 。 所以如果要學習這個SDK,則需要了解Excel里面 XML的組成元素,這個可以查閱 ECMA 376 標准的定義,但是會比較難啃。 但是了解Excel的內部組成,也對於你后期操作Excel的准確度,以及性能優化也是有幫助的。另外,由於是操作XML, 所以 OpenXML SDK只能適用與07版本后的office, 像2003版本的Excel 文件(.xls)是不支持的。
在一個文檔的內部,都是由多份XML的東西來組成的。 我們可以自己新建一個Excel文檔(.xlsx),然后修改其后綴名 xlsx 為 rar/zip, 可以直接解壓。
如下圖所示:
從以上的圖中解壓出來后的文件,可以看出,一個 Excel (.xlsx)文件里面包含很多個XML文件,分別代表一個Excel不同模塊的數據組成。
從名字上,我們大概初步可以猜測出幾點:
(1)workbook.xml 一個工作簿(Excel文件)的總覽
(2)styles.xml 樣式相關
(3)worksheets文件夾,包含多份xml, 每一份代表一個工作簿(Excel文件)里面其中的一個工作表(WorkSheet)
(4)theme 文件夾,Excel主題相關
(5)printerSettings文件夾, 打印設置相關
那么再來看看,初步通過代碼應該怎么實現。
微軟官方提供的文檔說明,可以查閱:https://docs.microsoft.com/zh-cn/office/open-xml/structure-of-a-spreadsheetml-document
OpenXML SDK 對應的Nuget包叫DocumentFormat.OpenXml,目前最新版本是 2.11.3,地址是: https://www.nuget.org/packages/DocumentFormat.OpenXml/
OpenXML SDK 的相關類型說明,可以查閱微軟官方提供的API文檔:https://docs.microsoft.com/zh-cn/dotnet/api/documentformat.openxml.spreadsheet.workbook?view=openxml-2.8.1
在C#.Net 項目中,通過VS開發工具的Nuget管理工具安裝,也可以直接在終端通過nuget包管理命令安裝: Install-Package DocumentFormat.OpenXml -Version 2.11.3
根據微軟官方的文檔,先來通過C#代碼,簡單生成一個excel文件,並且保存到磁盤。 通過Nuget安裝好SDK。
根據微軟官方的介紹,一份最小(空白)工作簿必須包含以下內容,要:
1、有一個工作表(WorkSheet)
2、工作表的Id
3、指向工作表定義位置的關系 Id
可能聽起來有點難理解,通過這個來看下面的代碼示例(可以運行的)
1 using System; 2 using System.IO; 3 using DocumentFormat.OpenXml; 4 using DocumentFormat.OpenXml.Packaging; 5 using DocumentFormat.OpenXml.Spreadsheet; 6 7 namespace PracticePart1 8 { 9 public class Program 10 { 11 public static void Main(string[] args) 12 { 13 //當前運行時路徑 14 var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory()); 15 var fileName = $@"PracticePart1-{DateTime.Now:yyyyMMddHHmmss}.xlsx"; 16 17 //文件路徑,保存在運行時路徑下 18 var filepath = Path.Combine(directoryInfo.ToString(), fileName); 19 Console.WriteLine($"FilePath: {filepath}"); 20 21 //創建SpreadsheetDocument對象,xlsx類型,通過路徑 22 var spreadsheetDocument = SpreadsheetDocument.Create(filepath, SpreadsheetDocumentType.Workbook); 23 24 //通過Stream對象 25 //MemoryStream ms = new MemoryStream(); 26 //SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook); 27 28 //調用AddWorkbookPart, 創建WorkbookPart對象, 創建Workbook對象(相當於XML根元素)關聯到WorkbookPart 29 var workbookPart = spreadsheetDocument.AddWorkbookPart(); 30 workbookPart.Workbook = new Workbook(); 31 32 //通過上面的WorkbookPart,創建WorksheetPart對象,創建Worksheet對象(相當於XML根元素)關聯到 WorksheetPart 33 var worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); 34 worksheetPart.Worksheet = new Worksheet(new SheetData()); 35 36 // 創建Sheets 到 Workbook 37 var sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets()); 38 39 // 創建添加Sheet對象, Id關聯 Worksheet, 從而命名工作表的名稱 40 var sheet = new Sheet() 41 { 42 Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart), 43 SheetId = 1, 44 Name = "myFirstSheet" 45 }; 46 47 //追加到 Sheets 48 sheets.Append(sheet); 49 50 //保存到磁盤 51 workbookPart.Workbook.Save(); 52 53 // Close the document. 54 spreadsheetDocument.Close(); 55 } 56 } 57 }
以上是可以直接運行,然后生成Excel文件。上述涉及到的幾個類型,依據我個人的理解,下面簡單說明下。
SpreadsheetDocument 表格文檔類, 從字面意思,它就表示一個Excel相關的文檔包。 通過它的Create方法, 通過枚舉來指定文檔類型(*.xlsx, 可以指定其它如*.xltx, *.xlsm等),同時指定文件路徑(重載方法中,是可以支持傳入一個Stream),創建一個document對象。然后創建表示 工作簿 的WorkbookPart, 創建表示 工作表的 WorksheetPart, 這個時候符合上訴的 第一個要求【有一個工作表(WorkSheet)】。
接着通過工作簿對象Workbook創建一個Sheets,表示工作簿中的所有表。 這里要先明白Sheet 和 Worksheet 的這2個的不同:
(1)Worksheet : 表示的是工作簿的一份工作表的內容定義,就是我們打開Excel文件,下方顯示sheet1, sheet2,sheet3....的每一份工作表。
(2)Sheet : 代表的是工作簿的一個表,它可以是工作表(Worksheet), 也可以圖表(ChartSheet),也可以是其它類型的表。它本身不涉及具體表怎么定義,但是會通過Id關聯具體表的定義; 所以 Sheets 是表示工作簿中關聯的所有表的集合(圖表,工作表,宏表等)。 比如一個Sheets,它其中包含2個工作表,1個圖表,而具體的定義放在其它的地方,一般一個表的定義有一個xml。
Sheet 對象, 有一個 Id 和 SheetId, 這2個字段。 按照上訴的 第二個要求 【工作表的Id】,就是指 SheetId, 自己賦值。 第三個要求【指向工作表定義位置的關系 Id 】, 就是指這個Id字段, 這個Id 指向工作表定義位置(就是指Sheet 關聯到具體哪個 WorksheetPart ), 通過 WorkbookPart.GetIdOfPart 的方法來獲取這個Id 。
以上就是DocumentFormat.OpenXml 創建一個最小化空白文檔的方式。