银河

SKYIV STUDIO

  博客园 :: 首页 ::  ::  :: 订阅 订阅 :: 管理 ::
  221 随笔 :: 2 文章 :: 2262 评论 :: 48 引用
在我最近开发的一个网页查询的项目中,客户提供的数据是多个 Excel 2007 文件,这些文件都很大,有的有十几万行(注意:Excel 2003 文件不能超过 65,536 行)。这些 Excel 2007 文件需要定期批量转换为网页程序可以读取的专用二进制格式文件。我们知道,Microsoft Office System 2007 引入了一个新的文件格式:Office Open XML 格式。她是基于 XML 和 ZIP 归档技术创建的,可以使用任何平台的能够处理 XML 或者 ZIP 文件的工具来访问并且修改文档内容。所以我们就可以使用 Microsoft .NET Framework 2.0 的强大 XML 类库来读取 Excel 2007 文件并转换为网页程序所需的专用二进制格式文件。当然,也可以使用 System.IO.Packaging 名称空间中的类库,但是她位于 .NET Framework 3.0 SDK (WinFX) 的 WindowsBase.dll 中。微软网站上有几篇很有用的文章:“Office (2007) Open XML 文件格式简介”和“如何操作 Office Open XML 格式文档”。

下面,就来看看 Excel 2007 Open XML 文件的结构吧:



  上图是一个名为 test1.xlsx 的 Excel 2007 文件。我没有 Office 2007 软件,只有正版的 Office 2003 软件。所以需要到微软网站下载一个“Microsoft Office Word、Excel 和 PowerPoint 2007 文件格式兼容包”,就可以在 Office 2003 中编辑 Office Open XML 文档了。test1.xlsx 文件其实是一个 zip 文件。为了分析其结构,我们现在把她解压到 D:/Test/test1/ 目录下。第一个重要的文件是 xl/workbook.xml,如下图所示:



该文件中的每个“<sheet>”元素都代表 Excel 2007 文件中的一个工作表,工作表的名称就是其“name”属性的值,在上图中是“好人”和“坏人”。然后根据“<sheet>”元素“r:id”属性的值(如上图中的“rId1”)到 xl/_rels/workbook.xml.rels 文件中寻找相应工作表数据实际存放的 xml 文件,如下图所示:



从图中可以看中,第一个工作表“好人”的数据实际存放在 worksheets/sheet1.xml 文件中,该文件的内容如下图所示:



上图中的“<dimension>”元素的“ref”属性的值(“B2:C4”)表示该工作表的范围。“<sheetData>”元素表示工作的数据,其子元素“<row>”表示工作表中的一行,“<row>”的子元素“<c>”表示该行中的单元格。如果“<c>”元素有“t”属性的话,“<c>”元素的子元素“<v>”的值就是各工作表共享的字符串的索引。否则的话,“<v>”元素的值就是该单元格的值。各工作表共享的字符串存放在 xl/sharedStrings.xml 文件中,如下图所示:



上图中,“<sst>”元素的子元素“<si>”就代表了共享的字符串,其值就是“<si>”元素的子元素“<t>”的值。

下面就看看我们的测试程序吧:



源程序的整体结构如下图所示:



我们先看看 XlsxFile.cs 吧:

 1 using System;
 2 using System.IO;
 3 using System.Xml;
 4 using Skyiv.Ben.Common;
 5 
 6 namespace Skyiv.OfficeHelper
 7 {
 8   /// <summary>
 9   /// Excel 2007 文件
10   /// </summary>
11   sealed partial class XlsxFile : IDisposable
12   {
13     string fileName; // Excel 2007 文件的文件名
14     Sheet[] sheets;  // Excel 2007 文件的各工作表
15     FileStream fileStream { get { return new FileStream(fileName, FileMode.Open, FileAccess.Read); } }
16 
17     /// <summary>
18     /// Excel 2007 文件的构造函数
19     /// </summary>
20     /// <param name="fileName">Excel 2007 文件的文件名</param>
21     public XlsxFile(string fileName)
22     {
23       this.fileName = fileName;
24     }
25 
26     /// <summary>
27     /// Excel 2007 文件的各工作表
28     /// </summary>
29     public Sheet[] Sheets
30     {
31       get
32       {
33         if (sheets == null)
34         {
35           using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/workbook.xml"))
36           {
37             // xl/workbook.xml 文件的内容举例如下:
38             // <workbook>
39             //   <sheets>
40             //     <sheet name="好人" sheetId="1" r:id="rId1" />
41             //     <sheet name="坏人" sheetId="2" r:id="rId2" />
42             //   </sheets>
43             // </workboo>
44             XmlDocument xmlDocument = new XmlDocument();
45             xmlDocument.Load(zs);
46             XmlNodeList elms = xmlDocument.DocumentElement["sheets"].ChildNodes;
47             sheets = new Sheet[elms.Count];
48             for (int i = 0; i < elms.Count; i++)
49             {
50               XmlAttributeCollection attrs = elms[i].Attributes;
51               sheets[i] = new Sheet(attrs["name"].Value, GetXmlFileName(attrs["r:id"].Value), SharedStrings, fileStream);
52             }
53           }
54         }
55         return sheets;
56       }
57     }
58 
59     /// <summary>
60     /// 根据“标识”给出表示工作表的 XML 文件名
61     /// </summary>
62     /// <param name="id">标识</param>
63     /// <returns>表示工作表的 XML 文件名</returns>
64     string GetXmlFileName(string id)
65     {
66       string value;
67       using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/_rels/workbook.xml.rels"))
68       {
69         // xl/_rels/workbook.xml.rels 文件的内容举例如下:
70         // <Relationships>
71         //   <Relationship Id="rId1" Target="worksheets/sheet1.xml" />
72         //   <Relationship Id="rId2" Target="worksheets/sheet2.xml" />
73         // </Relationships>
74         XmlDocument xmlDocument = new XmlDocument();
75         xmlDocument.Load(zs);
76         value = XmlHelper.GetElementById(xmlDocument, id).Attributes["Target"].Value;
77       }
78       return value;
79     }
80 
81     public void Dispose()
82     {
83       if (sheets == nullreturn;
84       foreach (Sheet sheet in sheets) sheet.Dispose();
85     }
86   }
87 }
88 

该程序已经有很详细的注释了。在该程序中用 XmlDocument 类来读 xl/workbook.xml 文件和 xl/_rels/workbook.xml.rels 文件,是因为这两个文件都是非常小的文件。然后再来看看 XlsxFile.SharedStrings.cs 吧:

 1 using System;
 2 using System.IO;
 3 using System.Xml;
 4 using Skyiv.Ben.Common;
 5 
 6 namespace Skyiv.OfficeHelper
 7 {
 8   partial class XlsxFile
 9   {
10     string[] sharedStrings;
11 
12     /// <summary>
13     /// Excel 2007 文件中各工作表共享的字符串
14     /// </summary>
15     string[] SharedStrings
16     {
17       get
18       {
19         if (sharedStrings == null)
20         {
21           Stream zs = null;
22           try
23           {
24             zs = Zip.GetZipInputStream(fileStream, "xl/sharedStrings.xml"); // 可能引发(FileNotFoundException)
25             // xl/sharedStrings.xml 文件的内容举例如下:
26             // <sst count="56" uniqueCount="2">
27             //   <si><t>任盈盈</t><phoneticPr fontId="1" type="noConversion" /></si>
28             //   <si><t /></si>
29             // </sst>
30             using (XmlReader reader = XmlReader.Create(zs))
31             {
32               while (reader.Read()) if (reader.IsStartElement("sst")) break;
33               sharedStrings = new string[Convert.ToInt32(reader.GetAttribute("uniqueCount"))];
34               for (int count = 0; ; count++)
35               {
36                 reader.Read();                                               // 执行后(reader)的值: <si> or </sst>
37                 if (!reader.IsStartElement("si")) break;
38                 reader.ReadStartElement("si");                               // 执行后(reader)的值: <t>  or <t />
39                 sharedStrings[count] = reader.ReadElementString("t").Trim(); // 执行后(reader)的值: </si> or <与<t>同级的元素>
40                 if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("t"); // 执行后(reader)的值: </si>
41               }
42             }
43           }
44           catch (FileNotFoundException)
45           {
46             sharedStrings = new string[0]; // 如果没有找到 xl/sharedStrings.xml 文件
47           }
48           finally
49           {
50             if (zs != null) zs.Close();
51           }
52         }
53         return sharedStrings;
54       }
55     }
56   }
57 }
58 

这下必须用 XmlReader 类来读取 xl/sharedStrings.xml 了,因为这个 xml 文件可能很大。最后是 XlsxFile.Sheet.cs 了:

  1 using System;
  2 using System.IO;
  3 using System.Xml;
  4 using System.Collections.Generic;
  5 using Skyiv.Ben.Common;
  6 
  7 namespace Skyiv.OfficeHelper
  8 {
  9   partial class XlsxFile
 10   {
 11     /// <summary>
 12     /// Execl 2007 文件中的工作表
 13     /// </summary>
 14     public sealed class Sheet : IDisposable
 15     {
 16       string[] sharedStrings; // 各工作表共享的字符串
 17       Stream stream;          // 用于读取本工作表的文件流
 18       XmlReader reader;       // 用于读取本工作表的 XML 数据读取器
 19       string dimension;       // 本工作表的范围,如:“A1”、“B2:C4”
 20       int rowCount;           // 本工作表的有效行数
 21       string name;            // 本工作表的名称
 22 
 23       public string Dimension { get { return dimension; } }
 24       public int RowCount { get { return rowCount; } }
 25       public string Name { get { return name; } }
 26 
 27       /// <summary>
 28       /// 表示 Excel 2007 文件中的工作表的类的构造函数
 29       /// </summary>
 30       /// <param name="name">本工作表的名称</param>
 31       /// <param name="fileName">工作表的 XML 文件名</param>
 32       /// <param name="sharedStrings">各工作表共享的字符串</param>
 33       /// <param name="fileStream">表示整个 Excel 2007 文件的流</param>
 34       public Sheet(string name, string fileName, string[] sharedStrings, Stream fileStream)
 35       {
 36         this.name = name;
 37         this.sharedStrings = sharedStrings;
 38         stream = Zip.GetZipInputStream(fileStream, "xl/" + fileName);
 39         reader = XmlReader.Create(stream);
 40         while (reader.Read()) if (reader.IsStartElement("dimension")) break;
 41         dimension = reader.GetAttribute("ref"); // 本工作表的范围:<dimension ref="B2:C4" />
 42         rowCount = GetRowCount(dimension); // 根据工作表的范围计算有效行数
 43         while (reader.Read()) if (reader.IsStartElement("sheetData")) break;
 44       }
 45 
 46       /// <summary>
 47       /// 读取本工作表的中一行
 48       /// </summary>
 49       /// <returns>读取的行的各字段的内容。如果已经没有可读的行则返回 null。</returns>
 50       public string[] ReadRow()
 51       {
 52         // 表示工作表的 XML 文件(如:xl/worksheets/sheet1.xml)的内容举例如下:
 53         // <worksheet>
 54         //   <dimension ref="B2:C4" />
 55         //   <sheetData>
 56         //     <row r="2" spans="2:3" />
 57         //     <row r="4" spans="2:3">
 58         //       <c r="B4" />
 59         //       <c r="C4" t="s"><v>1</v></c>
 60         //     </row>
 61         //   </sheetData>
 62         // </worksheet>
 63         // 注意:在该 XML 文件中可能省略某些空行和空单元格,而本方法忽略这些空行和空单元格。
 64         // 但本方法不忽略 XML 文件中的空行“<row />”和空单元格“<c />”。
 65         if (!reader.IsStartElement("row")) reader.Read();
 66         if (!reader.IsStartElement("row")) return null// 没有可读的行
 67         List<string> list = new List<string>();
 68         for (; ; )
 69         {
 70           reader.Read(); // 执行后(reader)的值: <c> or </row> or (other for <row />)
 71           if (!reader.IsStartElement("c")) break// 没有可读的单元格
 72           if (reader.IsEmptyElement) list.Add(""); // 空单元格“<c />”
 73           else                                     // “<c><v>1</v></c>”
 74           {
 75             string attr = reader.GetAttribute("t"); // 如果“<c>”元素的“t”属性不为空,则“<v>”元素的值指向各工作表共享的字符串
 76             reader.ReadStartElement("c");                            // 执行后(reader)的值: <v> or <v />
 77             list.Add(GetValue(attr, reader.ReadElementString("v"))); // 执行后(reader)的值: </c> or <与<v>同级的元素>
 78             if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("v"); // 执行后(reader)的值: </c>
 79           }
 80         }
 81         return list.ToArray();
 82       }
 83 
 84       /// <summary>
 85       /// 根据工作表的范围计算有效行数
 86       /// </summary>
 87       /// <param name="dimension">工作表的范围,如:“A1”、“B2:C4”</param>
 88       /// <returns>有效行数</returns>
 89       int GetRowCount(string dimension)
 90       {
 91         if (string.IsNullOrEmpty(dimension)) return -1;
 92         string[] ss = dimension.Split(':');
 93         if (ss.Length == 1return 1;
 94         if (ss.Length != 2return -1;
 95         return GetRowNumber(ss[1]) - GetRowNumber(ss[0]) + 1;
 96       }
 97 
 98       /// <summary>
 99       /// 根据单元格的坐标计算单元格的行号
100       /// </summary>
101       /// <param name="str">单元格的坐标,如“C4”</param>
102       /// <returns>单元格的行号</returns>
103       int GetRowNumber(string str)
104       {
105         int i;
106         for (i = 0; i < str.Length; i++if (char.IsDigit(str, i)) break;
107         return int.Parse(str.Substring(i));
108       }
109 
110       /// <summary>
111       /// 给出单元格的值,可能是各工作表共享的字符串
112       /// </summary>
113       /// <param name="attr"><c>”元素的“t”属性的值</param>
114       /// <param name="value"><v>”元素的值</param>
115       /// <returns>单元格的值</returns>
116       string GetValue(string attr, string value)
117       {
118         if (attr != null)
119         {
120           int index;
121           if (!int.TryParse(value, out index)) throw new Exception("共享字符串索引(" + value + ")必须是整数");
122           if (index < 0 || index >= sharedStrings.Length) throw new Exception("共享字符串索引("
123             + index + ")必须在(0)到(" + (sharedStrings.Length - 1+ ")之间");
124           value = sharedStrings[index];
125         }
126         return value;
127       }
128 
129       public void Dispose()
130       {
131         if (reader != null) reader.Close();
132         if (stream != null) stream.Close();
133         reader = null;
134         stream = null;
135       }
136     }
137   }
138 }
139 

在这个程序中也是用 XmlReader 类来读取 xl/worksheets/sheet1.xml 文件。

posted on 2007-09-23 23:12 银河 阅读(7552) 评论(33) 编辑 收藏

评论

#1楼[楼主] 2007-09-23 23:33 银河      
完整的源程序可到以下地址下载:

http://files.cnblogs.com/skyivben/OpenXmlTest-src.7z

编译后的可执行文件的下载地址:

http://files.cnblogs.com/skyivben/OpenXmlTest-bin.7z

本程序需要 ICSharpCode.SharpZipLib 0.85.4.369,可到以下网址下载:

http://www.icsharpcode.net/OpenSource/SharpZipLib/Download.aspx





 回复 引用 查看   

#2楼 2007-09-24 01:49 踏雪无痕      
office没有做一个.net的接口lib吗?用来读取office DOM。
 回复 引用 查看   

#3楼[楼主] 2007-09-24 08:05 银河      
@踏雪无痕
有呀,我在正文中不是说了吗:

当然,也可以使用 System.IO.Packaging 名称空间中的类库,但是她位于 .NET Framework 3.0 SDK (WinFX) 的 WindowsBase.dll 中。微软网站上有几篇很有用的文章:“Office (2007) Open XML 文件格式简介”和“如何操作 Office Open XML 格式文档”。

也就是说,如果使用 System.IO.Packaging 名称空间的类库,需要 .NET Framework 3.0 的环境。目前在 Windows Vista 还不是很普及的情况下,很少有人有安装 .NET Fraework 3.0 的。而 .NET Framework 2.0 就普及得多了。

另外,Office Open XML 的目标之一就是:可以使用任何平台的能够处理 XML 或者 ZIP 文件的工具来访问并且修改文档内容。

使用我提供的这个 Skyiv.OfficeHelper.XlsxFile 类来读取 Excel 2007 文件不是也很方便吗?而且速度非常快,读取一个十几万行的 Excel 2007 文件几秒钟就行了。
 回复 引用 查看   

#4楼 2007-09-24 08:42 121[未注册用户]
公式怎么处理的??
 回复 引用   

#5楼 2007-09-24 08:52 周银辉      
good work
 回复 引用 查看   

#6楼 2007-09-24 10:09 claudedb[未注册用户]
文件可以下载?
我这边都是不能下载,汗
找不到文件或目录
 回复 引用   

#7楼[楼主] 2007-09-24 10:11 银河      

@121
> 公式怎么处理的??

我遗漏公式的情况,这是一个 BUG。如果遇到有公式的 Excel 2007  文件,程序引发一个异常:
错误:找不到元素“v”。行 2,位置  2903。

查看一下 xl/worksheets/sheet1.xml  文件,相关的片断如下:

1 <r="L4" >
2   <f>SUM(C4:K4)</f>
3   <v>516609.32</v>
4 </c>
5 

也就是说,“<c>”元素表示一个单元格,“<v>”元素表示该单元格的值,而“<f>”元素表示该单元格中的公式。我的程序没有考虑到公式的情况,假设“<v>”元素是“<c>”元素的第一个子元素,所以出错了。

看来需要修改程序以处理公式。
 回复 引用 查看   

#8楼[楼主] 2007-09-24 10:35 银河      
@claudedb

我试了一下,点击下载时会提示以下错误:

The page cannot be found

The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.

而且在我的愽客的“管理 -> 文件”中点击“download”也提示同样的错误。但是“下载次数”分别有“30”和“17”次。

这是我第一次将文件上传到愽客园的服务器提供下载,不知是我的方法不正确,还是愽客园服务器的问题?

麻烦 dudu 帮助看一下是怎么回事?
 回复 引用 查看   

#9楼[楼主] 2007-09-24 16:04 银河      
根据 4 楼和 7 楼的评论,修改 xlsxFile.Sheet.cs 程序,以处理 Excel 2007 文件中的公式。具体做法是,将 xlsxFile.Sheet.cs 程序中的 76 行:
reader.ReadStartElement("c"); // 执行后(reader)的值: <v> or <v />
用以下两行替换:
reader.ReadStartElement("c"); // 执行后(reader)的值: “<c>”元素的第一个子元素
if (!reader.IsStartElement("v")) reader.ReadToNextSibling("v"); // 执行后(reader)的值: <v> or <v />
就可以了。
 回复 引用 查看   

#10楼 2007-09-24 20:18 Bruce Lee      
下在不了噢。
 回复 引用 查看   

#11楼[楼主] 2007-09-24 20:53 银河      
我将根据 9 楼的评论修改后的程序重新压缩为 zip 文件再上传到博客园服务器上。试试能不能点击以下链接下载:
http://www.cnblogs.com/Files/skyivben/OpenXmlTest-src.zip
http://www.cnblogs.com/Files/skyivben/OpenXmlTest-bin.zip

不过 zip 文件的尺寸比 7z 文件的尺寸大了不少,很浪费空间。

项目 src bytes bin bytes
未压缩 110,705 28,667
zip 29,107 8,667
7z 17,325 7,814


博客园是支持 7z 格式的文件上传,不知为什么不支持下载。
希望 dudu 能解决博客园不支持 7z 文件下载的问题。
因为用 7z 格式压缩文件可以比 zip 节省很多的空间。
 回复 引用 查看   

#12楼 2007-09-24 21:40 dudu      
是由于IIS 6默认不支持7z文件。
现在增加了MIME,可以下载了。
 回复 引用 查看   

#13楼[楼主] 2007-09-24 21:55 银河      
@dudu
谢谢 dudu
 回复 引用 查看   

#14楼[楼主] 2007-09-24 22:03 银河      
为了不浪费博客园的服务器空间,11 楼的两个 zip 文件已经被删除了。
请到 1 楼去下载 7z 文件,这些文件也已经根据 9 楼的评论修改过了。
 回复 引用 查看   

#15楼 2007-09-24 22:45 Cat Chen      
OpenXML的压缩格式好像是cab而不是zip吧?我也不清楚。
 回复 引用 查看   

#16楼[楼主] 2007-09-24 23:25 银河      
@Cat Chen
Office Open XML 的压缩格式是 zip,不是 cab。
 回复 引用 查看   

#17楼 2007-10-12 08:34 yunhuasheng      
这样读感觉和读xml文件的差别不是很大,不过也有很多不同点!
 回复 引用 查看   

#18楼[楼主] 2007-10-12 09:51 银河      
@yunhuasheng
实质上就是读压缩包中的特定格式的多个相关的 XML 文件。Office Open XML 的本质就是以 ZIP 格式保存 XML 文件。
 回复 引用 查看   

楼主,1楼地址下载不了,能否检查下,最近一个项目要求Excel 2007处理,找了半天,总算在楼主这边看到比较好的内容了。
 回复 引用   

#20楼[楼主] 2007-10-24 15:51 银河      
@Autumn.Panda
我已经给 dudu 留言请他处理一下。
 回复 引用 查看   

#21楼 2007-10-24 16:02 dudu      
现在好了。
 回复 引用 查看   

谢谢dudu了,也谢谢楼主,下载参考看看
 回复 引用   

#23楼[楼主] 2007-10-24 21:04 银河      
@Autumn.Panda
不客气。希望对你的项目有所帮助。 :)
 回复 引用 查看   

最近我在做一个BOM REPORT的EXCEL文件的项目,但使用了OPEN XML后明显感觉效率出了问题,读是一回事情,但写又是另外回事!
虽然可以跨不同平台,服务器也免去装OFFICE 2007,但如果生成万条数据以上的EXCEL文件非常慢,纯操作XML是会出问题的。
为什么微软到现在就EXCEL的OPEN XML处理DLL没出来就是因为效率存在问题,只是把开源的例子放在网上讨论。(www.codeplex.com)
要提高效率我个人觉得使用COM+或者直接HTML转换EXCEL文件是最快生成EXCEL文件的方法
 回复 引用   

#25楼[楼主] 2007-11-15 16:34 银河      
@Lichking
谢谢您的评论。
用 XmlReader 读取 Excel 2007 文件的效率还是非常高的,读取一个十几万条记录的 Excel 2007 文件只需一、两分钟。
目前我还没有涉及到 Office Open XML 文件的写入问题。
我想,可能写入通用的 Office Open XML 文件的效率会比较低。但是,如果只需生成特定格式的专用 Office Open XML 文件(例如,不需要复杂的字体控制、表格边框等等),可能还是会有比较高效的算法的。
 回复 引用 查看   

#26楼 2008-01-15 13:09 e狐[未注册用户]
我们之前也用到了这个,而且是相对比较复杂的操作,其中涉及到复制Excel表单。ExcelPackage不错的,大家可以去看看,开源的,不过我当时下来后,里面偏偏缺少Copy一段,没办法,最后自己把它补上去了。
 回复 引用   

#27楼 2008-02-05 15:16 fox23      
要是把导入和导出都做了就好了,不过那些符号图什么的怎么办?
 回复 引用 查看   

#28楼 2008-08-15 09:09 挖掘[未注册用户]
请问一下,谁知道用程序怎么读取excel公式的值,用java语言,如果不行,也可以用另的语言。
 回复 引用   

#29楼 2008-08-29 21:16 张磊.NET      
@银河
1楼的链接下载不了,请楼主帮忙

我的邮箱是zhangleipub@126.com,能否给我发一份源码,多谢!
 回复 引用 查看   

#30楼[楼主] 2008-08-30 10:35 银河      
@张磊.NET (29楼)
现在可以下载了。
 回复 引用 查看   

对于合并单元格的情况,楼主并没有考虑进去。
我进行了额外扩充,读取mergeCells,获得合并单元格区域,然后再判断cell单元格是否为合并单元格,如果是从已有的单元格数据中获取值。
 回复 引用   

#32楼[楼主] 2008-12-15 17:36 银河      
@浪击天涯
不错,这个程序是有很多情况没有考虑进去。只不过是仅实现该网页查询项目所需的功能而已。
如果需要,可以自行扩展其功能。
 回复 引用 查看   

#33楼 2009-07-21 11:01 moosdau[未注册用户]
不错,支持一个
 回复 引用