還有什么不能做?——細談在C#中讀寫Excel系列文章之三


  在第二篇文章中我已經向大家詳細介紹了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文件中某些字符串樣式的修改。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM