守望者--AIR技术交流

标题: 用 XmlReader 读取 Excel 2007 文件 [打印本页]

作者: 破晓    时间: 2015-3-31 15:27
标题: 用 XmlReader 读取 Excel 2007 文件
在我最近开发的一个网页查询的项目中,客户提供的数据是多个 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 文件的结构吧:


[attach]1004[/attach]

  上图是一个名为 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,如下图所示:


[attach]1005[/attach]

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


[attach]1006[/attach]

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


[attach]1007[/attach]

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


[attach]1008[/attach]

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

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


[attach]1009[/attach]

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


[attach]1010[/attach]

我们先看看 XlsxFile.cs 吧:

  1. using System;
  2. using System.IO;
  3. using System.Xml;
  4. using Skyiv.Ben.Common;

  5. namespace Skyiv.OfficeHelper
  6. {
  7.    /// <summary>
  8.    /// Excel 2007 文件
  9.    /// </summary>
  10.    sealed partial class XlsxFile : IDisposable
  11.    {
  12.      string fileName; // Excel 2007 文件的文件名
  13.      Sheet[] sheets;  // Excel 2007 文件的各工作表
  14.      FileStream fileStream { get { return new FileStream(fileName, FileMode.Open, FileAccess.Read); } }

  15.      /// <summary>
  16.      /// Excel 2007 文件的构造函数
  17.      /// </summary>
  18.      /// <param name="fileName">Excel 2007 文件的文件名</param>
  19.      public XlsxFile(string fileName)
  20.      {
  21.        this.fileName = fileName;
  22.      }

  23.      /// <summary>
  24.      /// Excel 2007 文件的各工作表
  25.      /// </summary>
  26.      public Sheet[] Sheets
  27.      {
  28.        get
  29.        {
  30.          if (sheets == null)
  31.          {
  32.            using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/workbook.xml"))
  33.            {
  34.              // xl/workbook.xml 文件的内容举例如下:
  35.              // <workbook>
  36.              //   <sheets>
  37.              //     <sheet name="好人" sheetId="1" r:id="rId1" />
  38.              //     <sheet name="坏人" sheetId="2" r:id="rId2" />
  39.              //   </sheets>
  40.              // </workboo>
  41.              XmlDocument xmlDocument = new XmlDocument();
  42.              xmlDocument.Load(zs);
  43.              XmlNodeList elms = xmlDocument.DocumentElement["sheets"].ChildNodes;
  44.              sheets = new Sheet[elms.Count];
  45.              for (int i = 0; i < elms.Count; i++)
  46.              {
  47.                XmlAttributeCollection attrs = elms[i].Attributes;
  48.                sheets[i] = new Sheet(attrs["name"].Value, GetXmlFileName(attrs["r:id"].Value), SharedStrings, fileStream);
  49.              }
  50.            }
  51.          }
  52.          return sheets;
  53.        }
  54.      }

  55.      /// <summary>
  56.      /// 根据“标识”给出表示工作表的 XML 文件名
  57.      /// </summary>
  58.      /// <param name="id">标识</param>
  59.      /// <returns>表示工作表的 XML 文件名</returns>
  60.      string GetXmlFileName(string id)
  61.      {
  62.        string value;
  63.        using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/_rels/workbook.xml.rels"))
  64.        {
  65.          // xl/_rels/workbook.xml.rels 文件的内容举例如下:
  66.          // <Relationships>
  67.          //   <Relationship Id="rId1" Target="worksheets/sheet1.xml" />
  68.          //   <Relationship Id="rId2" Target="worksheets/sheet2.xml" />
  69.          // </Relationships>
  70.          XmlDocument xmlDocument = new XmlDocument();
  71.          xmlDocument.Load(zs);
  72.          value = XmlHelper.GetElementById(xmlDocument, id).Attributes["Target"].Value;
  73.        }
  74.        return value;
  75.      }

  76.      public void Dispose()
  77.      {
  78.        if (sheets == null) return;
  79.        foreach (Sheet sheet in sheets) sheet.Dispose();
  80.      }
  81.    }
  82. }
复制代码
该程序已经有很详细的注释了。在该程序中用 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. namespace Skyiv.OfficeHelper
  6. {
  7.    partial class XlsxFile
  8.    {
  9.      string[] sharedStrings;

  10.      /// <summary>
  11.      /// Excel 2007 文件中各工作表共享的字符串
  12.      /// </summary>
  13.      string[] SharedStrings
  14.      {
  15.        get
  16.        {
  17.          if (sharedStrings == null)
  18.          {
  19.            Stream zs = null;
  20.            try
  21.            {
  22.              zs = Zip.GetZipInputStream(fileStream, "xl/sharedStrings.xml"); // 可能引发(FileNotFoundException)
  23.              // xl/sharedStrings.xml 文件的内容举例如下:
  24.              // <sst count="56" uniqueCount="2">
  25.              //   <si><t>任盈盈</t><phoneticPr fontId="1" type="noConversion" /></si>
  26.              //   <si><t /></si>
  27.              // </sst>
  28.              using (XmlReader reader = XmlReader.Create(zs))
  29.              {
  30.                while (reader.Read()) if (reader.IsStartElement("sst")) break;
  31.                sharedStrings = new string[Convert.ToInt32(reader.GetAttribute("uniqueCount"))];
  32.                for (int count = 0; ; count++)
  33.                {
  34.                  reader.Read();                                               // 执行后(reader)的值: <si> or </sst>
  35.                  if (!reader.IsStartElement("si")) break;
  36.                  reader.ReadStartElement("si");                               // 执行后(reader)的值: <t>  or <t />
  37.                  sharedStrings[count] = reader.ReadElementString("t").Trim(); // 执行后(reader)的值: </si> or <与<t>同级的元素>
  38.                  if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("t"); // 执行后(reader)的值: </si>
  39.                }
  40.              }
  41.            }
  42.            catch (FileNotFoundException)
  43.            {
  44.              sharedStrings = new string[0]; // 如果没有找到 xl/sharedStrings.xml 文件
  45.            }
  46.            finally
  47.            {
  48.              if (zs != null) zs.Close();
  49.            }
  50.          }
  51.          return sharedStrings;
  52.        }
  53.      }
  54.    }
  55. }
复制代码
这下必须用 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. namespace Skyiv.OfficeHelper
  7. {
  8.    partial class XlsxFile
  9.    {
  10.      /// <summary>
  11.      /// Execl 2007 文件中的工作表
  12.      /// </summary>
  13.      public sealed class Sheet : IDisposable
  14.      {
  15.        string[] sharedStrings; // 各工作表共享的字符串
  16.        Stream stream;          // 用于读取本工作表的文件流
  17.        XmlReader reader;       // 用于读取本工作表的 XML 数据读取器
  18.        string dimension;       // 本工作表的范围,如:“A1”、“B2:C4”
  19.        int rowCount;           // 本工作表的有效行数
  20.        string name;            // 本工作表的名称

  21.        public string Dimension { get { return dimension; } }
  22.        public int RowCount { get { return rowCount; } }
  23.        public string Name { get { return name; } }

  24.        /// <summary>
  25.        /// 表示 Excel 2007 文件中的工作表的类的构造函数
  26.        /// </summary>
  27.        /// <param name="name">本工作表的名称</param>
  28.        /// <param name="fileName">工作表的 XML 文件名</param>
  29.        /// <param name="sharedStrings">各工作表共享的字符串</param>
  30.        /// <param name="fileStream">表示整个 Excel 2007 文件的流</param>
  31.        public Sheet(string name, string fileName, string[] sharedStrings, Stream fileStream)
  32.        {
  33.          this.name = name;
  34.          this.sharedStrings = sharedStrings;
  35.          stream = Zip.GetZipInputStream(fileStream, "xl/" + fileName);
  36.          reader = XmlReader.Create(stream);
  37.          while (reader.Read()) if (reader.IsStartElement("dimension")) break;
  38.          dimension = reader.GetAttribute("ref"); // 本工作表的范围:<dimension ref="B2:C4" />
  39.          rowCount = GetRowCount(dimension); // 根据工作表的范围计算有效行数
  40.          while (reader.Read()) if (reader.IsStartElement("sheetData")) break;
  41.        }

  42.        /// <summary>
  43.        /// 读取本工作表的中一行
  44.        /// </summary>
  45.        /// <returns>读取的行的各字段的内容。如果已经没有可读的行则返回 null。</returns>
  46.        public string[] ReadRow()
  47.        {
  48.          // 表示工作表的 XML 文件(如:xl/worksheets/sheet1.xml)的内容举例如下:
  49.          // <worksheet>
  50.          //   <dimension ref="B2:C4" />
  51.          //   <sheetData>
  52.          //     <row r="2" spans="2:3" />
  53.          //     <row r="4" spans="2:3">
  54.          //       <c r="B4" />
  55.          //       <c r="C4" t="s"><v>1</v></c>
  56.          //     </row>
  57.          //   </sheetData>
  58.          // </worksheet>
  59.          // 注意:在该 XML 文件中可能省略某些空行和空单元格,而本方法忽略这些空行和空单元格。
  60.          // 但本方法不忽略 XML 文件中的空行“<row />”和空单元格“<c />”。
  61.          if (!reader.IsStartElement("row")) reader.Read();
  62.          if (!reader.IsStartElement("row")) return null; // 没有可读的行
  63.          List<string> list = new List<string>();
  64.          for (; ; )
  65.          {
  66.            reader.Read(); // 执行后(reader)的值: <c> or </row> or (other for <row />)
  67.            if (!reader.IsStartElement("c")) break; // 没有可读的单元格
  68.            if (reader.IsEmptyElement) list.Add(""); // 空单元格“<c />”
  69.            else                                     // “<c><v>1</v></c>”
  70.            {
  71.              string attr = reader.GetAttribute("t"); // 如果“<c>”元素的“t”属性不为空,则“<v>”元素的值指向各工作表共享的字符串
  72.              reader.ReadStartElement("c");                            // 执行后(reader)的值: <v> or <v />
  73.              list.Add(GetValue(attr, reader.ReadElementString("v"))); // 执行后(reader)的值: </c> or <与<v>同级的元素>
  74.              if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("v"); // 执行后(reader)的值: </c>
  75.            }
  76.          }
  77.          return list.ToArray();
  78.        }

  79.        /// <summary>
  80.        /// 根据工作表的范围计算有效行数
  81.        /// </summary>
  82.        /// <param name="dimension">工作表的范围,如:“A1”、“B2:C4”</param>
  83.        /// <returns>有效行数</returns>
  84.        int GetRowCount(string dimension)
  85.        {
  86.          if (string.IsNullOrEmpty(dimension)) return -1;
  87.          string[] ss = dimension.Split(':');
  88.          if (ss.Length == 1) return 1;
  89.          if (ss.Length != 2) return -1;
  90.          return GetRowNumber(ss[1]) - GetRowNumber(ss[0]) + 1;
  91.        }

  92.        /// <summary>
  93.        /// 根据单元格的坐标计算单元格的行号
  94.        /// </summary>
  95.        /// <param name="str">单元格的坐标,如“C4”</param>
  96.        /// <returns>单元格的行号</returns>
  97.        int GetRowNumber(string str)
  98.        {
  99.          int i;
  100.          for (i = 0; i < str.Length; i++) if (char.IsDigit(str, i)) break;
  101.          return int.Parse(str.Substring(i));
  102.        }

  103.        /// <summary>
  104.        /// 给出单元格的值,可能是各工作表共享的字符串
  105.        /// </summary>
  106.        /// <param name="attr">“<c>”元素的“t”属性的值</param>
  107.        /// <param name="value">“<v>”元素的值</param>
  108.        /// <returns>单元格的值</returns>
  109.        string GetValue(string attr, string value)
  110.        {
  111.          if (attr != null)
  112.          {
  113.            int index;
  114.            if (!int.TryParse(value, out index)) throw new Exception("共享字符串索引(" + value + ")必须是整数");
  115.            if (index < 0 || index >= sharedStrings.Length) throw new Exception("共享字符串索引("
  116.              + index + ")必须在(0)到(" + (sharedStrings.Length - 1) + ")之间");
  117.            value = sharedStrings[index];
  118.          }
  119.          return value;
  120.        }

  121.        public void Dispose()
  122.        {
  123.          if (reader != null) reader.Close();
  124.          if (stream != null) stream.Close();
  125.          reader = null;
  126.          stream = null;
  127.        }
  128.      }
  129.    }
  130. }
复制代码
在这个程序中也是用 XmlReader 类来读取 xl/worksheets/sheet1.xml 文件。






完整的源程序可到以下地址下载:

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




本文来自:http://www.cnblogs.com/skyivben/archive/2007/09/23/903582.html





欢迎光临 守望者--AIR技术交流 (http://www.airmyth.com/)