在第二篇文章中我已經向大家詳細介紹了Excel 2007以后版本的文件的OpenXML結構。所謂的OpenXML結構從本質上來講其實就是一個ZIP壓縮包,所有基於OpenXML數據結構的文件都一樣。這樣文件的結構是開放的,你不需要借助於任何第三方的組件就可以讀取文件中的數據,或者修改文件的內容。而且所有資源文件諸如圖片和多媒體文件都是被編譯成一個固定的格式存放在壓縮包中,而不是傳統形式上的流。另外,以壓縮包的形式存儲數據也使得文件所占用的空間更小。
這些都是它的優點。其實,微軟在.NET中已經提供了相應的類庫來幫助我們操作OpenXML文件,就像我在第二篇文章中向大家介紹的一樣,微軟把OpenXML壓縮包稱之為一個Package,在Package中的文件稱作Parts,不同的Parts有自己的Content Type。Content Type可以描述任何內容,如application XML,user XML,images,sounds,videos,或者其它二進制內容。各個Part之間通過一個被稱之為relationship的part連接起來,這個part其實也是一種特殊的XML文件,后綴為".rels",在Package中可以找到它。下圖說明了一個Package中各個Part之間的關系,
雖然.NET中提供的Package類可以幫助我們非常方便地解析Excel包,但是也有許多的局限性,有的時候你不得不使用一些方法來自己讀取包中的內容,或者嘗試去修改包中的數據。
1. 將Package作為一個標准的zip文件進行解壓。
2. 找到Package中你想要讀取的parts。有許多的方法可以幫助你找到這些parts,你可以解析relationship,也可以直接通過path來定位到某一個文件,不過不太確定path在將來是否會有變化。或者還可以通過part的content type來找到它。
3. 讀取part中的內容。如果parts是以XML的形式存放的,通過標准的XML類庫就可以非常方便地讀取到數據。如果是其它形式,如圖片、聲音或視頻文件則也可以通過相應的方法來獲取到內容。
另一方面,你也可以創建一個基於OpenXML的文件(如采用非COM組件的形式創建一個Excel文件),
1. 創建或復制所有必須的parts。通過標准XML類庫來創建這些基於XML數據格式的parts,或者從其它package中復制這些parts,或者采用任何其它你所熟悉的方法。
2. 創建relationships部分。也就是創建一個后綴為".rels"的特殊XML文件。
3. 將整個package壓縮成一個zip包,然后將文件的后綴修改成需要的類型(如docx,xlsx,或者pptx等)。只要package中的結構符合要求,修改之后的文件可以直接被打開。當然,該過程可以在完全沒有安裝office的機器上完成(例如服務器),然后分發給需要的地方使用。
所有這一切包括packages,parts,content types,以及relationships都被稱之為OpenXML文檔,微軟將這個叫做Open Packaging Conventions。
有關Excel OpenXML的內部結構以及一些比較重要的節點含義在前一篇文章中已經做了一些介紹,除了直接使用.NET類庫中的Package類之外,我們當然也可以使用其它的第三方類庫或者自己編寫代碼讀取包中的內容,這里有一個.NET的開源類庫專門用來操作zip壓縮文件。
http://www.icsharpcode.net/OpenSource/SharpZipLib/
提供一個程序集下載吧,源代碼大家去上面這個網址下載。ICSharpCode.ShareZipLib.zip
借用該類庫中的ExtractZip()和CreateZip()方法可以幫助我們讀取或修改(創建)Excel文件。當然這個類庫中的某些代碼是使用較早的.NET版本中的語法和對象來編寫的,諸如ArratList、Hashtable等,它可能不適合在Silverlight工程中使用(支持Silverlight的.NET Framework與普通的.NET Framework有許多區別並且變化比較頻繁),接下來的文章中我會向大家介紹如何在Silverlight工程中使用它。但現在並不妨礙我們在其它類型的工程中使用。
來看一看實際的例子。這里有一個類提供了一些方法用來讀取和修改Excel文件中的數據:
1 using System.Collections.Generic; 2 using System.Data; 3 using System.Globalization; 4 using System.IO; 5 using System.Text; 6 using System.Xml; 7 using ICSharpCode.SharpZipLib.Zip; 8 9 namespace XlsxReadWrite 10 { 11 internal static class XlsxRW 12 { 13 public static void DeleteDirectoryContents(string directory) 14 { 15 var info = new DirectoryInfo(directory); 16 17 foreach (var file in info.GetFiles()) 18 { 19 file.Delete(); 20 } 21 22 foreach (var dir in info.GetDirectories()) 23 { 24 dir.Delete(true); 25 } 26 } 27 28 public static void UnzipFile(string zipFileName, string targetDirectory) 29 { 30 new FastZip().ExtractZip(zipFileName, targetDirectory, null); 31 } 32 33 public static void ZipDirectory(string sourceDirectory, string zipFileName) 34 { 35 new FastZip().CreateZip(zipFileName, sourceDirectory, true, null); 36 } 37 38 public static IList<string> ReadStringTable(Stream input) 39 { 40 var stringTable = new List<string>(); 41 42 using (var reader = XmlReader.Create(input)) 43 { 44 for (reader.MoveToContent(); reader.Read(); ) 45 { 46 if (reader.NodeType == XmlNodeType.Element && reader.Name == "t") 47 { 48 stringTable.Add(reader.ReadElementString()); 49 } 50 } 51 } 52 53 return stringTable; 54 } 55 56 public static void ReadWorksheet(Stream input, IList<string> stringTable, DataTable data) 57 { 58 using (var reader = XmlReader.Create(input)) 59 { 60 DataRow row = null; 61 int columnIndex = 0; 62 string type; 63 int value; 64 65 for (reader.MoveToContent(); reader.Read(); ) 66 if (reader.NodeType == XmlNodeType.Element) 67 switch (reader.Name) 68 { 69 case "row": 70 row = data.NewRow(); 71 data.Rows.Add(row); 72 73 columnIndex = 0; 74 75 break; 76 77 case "c": 78 type = reader.GetAttribute("t"); 79 reader.Read(); 80 value = int.Parse(reader.ReadElementString(), CultureInfo.InvariantCulture); 81 82 if (type == "s") 83 row[columnIndex] = stringTable[value]; 84 else 85 row[columnIndex] = value; 86 87 columnIndex++; 88 89 break; 90 } 91 } 92 } 93 94 public static IList<string> CreateStringTables(DataTable data, out IDictionary<string, int> lookupTable) 95 { 96 var stringTable = new List<string>(); 97 lookupTable = new Dictionary<string, int>(); 98 99 foreach (DataRow row in data.Rows) 100 foreach (DataColumn column in data.Columns) 101 if (column.DataType == typeof(string)) 102 { 103 var value = (string)row[column]; 104 105 if (!lookupTable.ContainsKey(value)) 106 { 107 lookupTable.Add(value, stringTable.Count); 108 stringTable.Add(value); 109 } 110 } 111 112 return stringTable; 113 } 114 115 public static void WriteStringTable(Stream output, IList<string> stringTable) 116 { 117 using (var writer = XmlWriter.Create(output)) 118 { 119 writer.WriteStartDocument(true); 120 121 writer.WriteStartElement("sst", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); 122 writer.WriteAttributeString("count", stringTable.Count.ToString(CultureInfo.InvariantCulture)); 123 writer.WriteAttributeString("uniqueCount", stringTable.Count.ToString(CultureInfo.InvariantCulture)); 124 125 foreach (var str in stringTable) 126 { 127 writer.WriteStartElement("si"); 128 writer.WriteElementString("t", str); 129 writer.WriteEndElement(); 130 } 131 132 writer.WriteEndElement(); 133 } 134 } 135 136 public static string RowColumnToPosition(int row, int column) 137 { 138 return ColumnIndexToName(column) + RowIndexToName(row); 139 } 140 141 public static string ColumnIndexToName(int columnIndex) 142 { 143 var second = (char)(((int)'A') + columnIndex % 26); 144 145 columnIndex /= 26; 146 147 if (columnIndex == 0) 148 return second.ToString(); 149 else 150 return ((char)(((int)'A') - 1 + columnIndex)).ToString() + second.ToString(); 151 } 152 153 public static string RowIndexToName(int rowIndex) 154 { 155 return (rowIndex + 1).ToString(CultureInfo.InvariantCulture); 156 } 157 158 public static void WriteWorksheet(Stream output, DataTable data, IDictionary<string, int> lookupTable) 159 { 160 using (XmlTextWriter writer = new XmlTextWriter(output, Encoding.UTF8)) 161 { 162 writer.WriteStartDocument(true); 163 164 writer.WriteStartElement("worksheet"); 165 writer.WriteAttributeString("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); 166 writer.WriteAttributeString("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); 167 168 writer.WriteStartElement("dimension"); 169 var lastCell = RowColumnToPosition(data.Rows.Count - 1, data.Columns.Count - 1); 170 writer.WriteAttributeString("ref", "A1:" + lastCell); 171 writer.WriteEndElement(); 172 173 writer.WriteStartElement("sheetViews"); 174 writer.WriteStartElement("sheetView"); 175 writer.WriteAttributeString("tabSelected", "1"); 176 writer.WriteAttributeString("workbookViewId", "0"); 177 writer.WriteEndElement(); 178 writer.WriteEndElement(); 179 180 writer.WriteStartElement("sheetFormatPr"); 181 writer.WriteAttributeString("defaultRowHeight", "15"); 182 writer.WriteEndElement(); 183 184 writer.WriteStartElement("sheetData"); 185 WriteWorksheetData(writer, data, lookupTable); 186 writer.WriteEndElement(); 187 188 writer.WriteStartElement("pageMargins"); 189 writer.WriteAttributeString("left", "0.7"); 190 writer.WriteAttributeString("right", "0.7"); 191 writer.WriteAttributeString("top", "0.75"); 192 writer.WriteAttributeString("bottom", "0.75"); 193 writer.WriteAttributeString("header", "0.3"); 194 writer.WriteAttributeString("footer", "0.3"); 195 writer.WriteEndElement(); 196 197 writer.WriteEndElement(); 198 } 199 } 200 201 public static void WriteWorksheetData(XmlTextWriter writer, DataTable data, IDictionary<string, int> lookupTable) 202 { 203 var rowsCount = data.Rows.Count; 204 var columnsCount = data.Columns.Count; 205 string relPos; 206 207 for (int row = 0; row < rowsCount; row++) 208 { 209 writer.WriteStartElement("row"); 210 relPos = RowIndexToName(row); 211 writer.WriteAttributeString("r", relPos); 212 writer.WriteAttributeString("spans", "1:" + columnsCount.ToString(CultureInfo.InvariantCulture)); 213 214 for (int column = 0; column < columnsCount; column++) 215 { 216 object value = data.Rows[row][column]; 217 218 writer.WriteStartElement("c"); 219 relPos = RowColumnToPosition(row, column); 220 writer.WriteAttributeString("r", relPos); 221 222 var str = value as string; 223 if (str != null) 224 { 225 writer.WriteAttributeString("t", "s"); 226 value = lookupTable[str]; 227 } 228 229 writer.WriteElementString("v", value.ToString()); 230 231 writer.WriteEndElement(); 232 } 233 234 writer.WriteEndElement(); 235 } 236 } 237 } 238 }
1. DeleteDirectoryContents()方法用來清空臨時目錄的內容,該臨時目錄被用來存放解壓Excel之后的文件。
2. UnzipFile()方法直接使用了開源類庫中的ExtractZip()方法,將Excel文件解壓到臨時目錄。
3. ZipDirectory()方法使用開源類庫中的CreateZip()方法將臨時目錄中的內容重新打包成zip壓縮文件。
4. ReadStringTable()方法用來讀取xl/sharedStrings.xml文件中節點t的內容。其實應該是直接讀取si節點的內容,應為並不是所有的si節點都有t子節點。
考慮一個情況:在Excel的單元格中,選中一部分內容,如內容中的某一個數字或某一個單詞,然后對它單獨設置樣式。通過這種方法你可以將Excel單元格中的某一部分內容設置為粗體、上標,還可以在單元格內進行換行等。如下圖:
在Excel單元格內設置的樣式被存放到sharedStrings.xml文件中后會變成如下這種形式:
<si> <r> <t xml:space="preserve"> Short-term investments (including securities loaned of </t> </r> <r> <rPr> <b/> <sz val="8"/> <color rgb="FF404040"/> <rFont val="Verdana"/> <family val="2"/> </rPr> <t>$9,999</t> </r> <r> <rPr> <sz val="8"/> <color rgb="FF404040"/> <rFont val="Verdana"/> <family val="2"/> </rPr> <t xml:space="preserve"> and $8,888)</t> </r> </si>
這個時候節點si中的內容就不是單純的子節點t了。因此,如何解析XML文件需要根據實際情況去考慮,這主要取決於你在Excel文件中存儲的內容。
5. ReadWorksheet()方法會按照指定的工作表XML文件(如"xl/worksheets/sheet1.xml")在sharedStrings.xml文件中查找數據,並將結果存放到一個DataTable中。
6. CreateStringTables()和WriteStringsTable()方法用來創建一個sharedStrings.xml文件。
7. WriteWorksheet()方法用來創建一個工作表XML文件。
來看看如何調用:
1 private void ReadInput(object sender, RoutedEventArgs e) 2 { 3 // Get the input file name from the text box. 4 var fileName = this.inputTextBox.Text; 5 6 // Delete contents of the temporary directory. 7 XlsxRW.DeleteDirectoryContents(tempDir); 8 9 // Unzip input XLSX file to the temporary directory. 10 XlsxRW.UnzipFile(fileName, tempDir); 11 12 IList<string> stringTable; 13 // Open XML file with table of all unique strings used in the workbook.. 14 using (var stream = new FileStream(Path.Combine(tempDir, @"xl\sharedStrings.xml"), 15 FileMode.Open, FileAccess.Read)) 16 // ..and call helper method that parses that XML and returns an array of strings. 17 stringTable = XlsxRW.ReadStringTable(stream); 18 19 // Open XML file with worksheet data.. 20 using (var stream = new FileStream(Path.Combine(tempDir, @"xl\worksheets\sheet1.xml"), 21 FileMode.Open, FileAccess.Read)) 22 // ..and call helper method that parses that XML and fills DataTable with values. 23 XlsxRW.ReadWorksheet(stream, stringTable, this.data); 24 } 25 26 private void WriteOutput(object sender, RoutedEventArgs e) 27 { 28 // Get the output file name from the text box. 29 string fileName = this.outputTextBox.Text; 30 31 // Delete contents of the temporary directory. 32 XlsxRW.DeleteDirectoryContents(tempDir); 33 34 // Unzip template XLSX file to the temporary directory. 35 XlsxRW.UnzipFile(templateFile, tempDir); 36 37 // We will need two string tables; a lookup IDictionary<string, int> for fast searching 38 // an ordinary IList<string> where items are sorted by their index. 39 IDictionary<string, int> lookupTable; 40 41 // Call helper methods which creates both tables from input data. 42 var stringTable = XlsxRW.CreateStringTables(this.data, out lookupTable); 43 44 // Create XML file.. 45 using (var stream = new FileStream(Path.Combine(tempDir, @"xl\sharedStrings.xml"), 46 FileMode.Create)) 47 // ..and fill it with unique strings used in the workbook 48 XlsxRW.WriteStringTable(stream, stringTable); 49 50 // Create XML file.. 51 using (var stream = new FileStream(Path.Combine(tempDir, @"xl\worksheets\sheet1.xml"), 52 FileMode.Create)) 53 // ..and fill it with rows and columns of the DataTable. 54 XlsxRW.WriteWorksheet(stream, this.data, lookupTable); 55 56 // ZIP temporary directory to the XLSX file. 57 XlsxRW.ZipDirectory(tempDir, fileName); 58 59 // If checkbox is checked, show XLSX file in Microsoft Excel. 60 if (this.openFileCheckBox.IsChecked == true) 61 System.Diagnostics.Process.Start(fileName); 62 }
你所要做的只是對XML進行操作,僅此而以!
支持.NET操作zip壓縮包的類庫應該還有很多,任何一個都可以,因為Excel的OpenXML文件本身就是一個標准的zip壓縮包。但是上面的方法還是有一個局限性,那就是需要臨時目錄來存放解壓之后的文件,以及重新打包時所指定的源文件。對普通的Windows應用程序或asp.net應用程序而言這個並沒有什么困難,只要權限允許,讀寫臨時目錄沒有任何問題,但是在Silverlight中則有所不同,因為在Silverlight中讀寫客戶端文件需要比較高的安全級別和認證,這就導致解壓文件會有困難,一個簡單的方法就是直接在文件的流中進行解壓和修改,然后再將流打包成zip文件。下一篇文章中我會向大家介紹如何在Silverlight中使用,以及如何定義一個類來完成Excel文件中某些字符串樣式的修改。