C#读取Excel技术概览 (2)

5、自定义SDK,使用xmlReader文件流式处理

  第四章节中,总是感觉用别人的工具要受制于人。既然我 们知道了Excel的存储方式,问题便转换成从xml中取出数据,然后放入内存得到我们想要的东西,更重要的是,官方的sdk对xml的读取采用的 Document的方式,对于大文件xml执行速度必然降低,同时对 内存,数据量都有限制,若有几十亿,好几百T的数据,这种处理方式就很难发挥作用了,所以我们决定自己做一个sdk出来,只是将原来读取xml的方式,改 为用xmlReader的方式来读取,采用文件流的方式进行处理。

  就不需要将整个的xml文件树完全的读进去,而是按照流的方式进行处理,按照节点一个一个的读取,速度就会更进一步,并且不受数据量以及内存的限制。

   网上关于这样的处理的文章较多,大概整理了一下,但是都不全面,在处理的时候还会遇到很多的问题,网上给的代码也只是一个思路,碰到bug,还需要 自己去更多了解Excel到xml的转换过程到底是怎么样的,而本文将站在巨人的肩膀上,告诉你,应该注意的细节。首先还是转载下前人的文档,然后在对文 档中的问题,进行介绍,注意这些问题是必须要注意的,不然会耗费你大量的时间却找不到自己想要的结果!

  最近开发的一个网页查询的项目 中,客户提供的数据是多个 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 吧:

using System;
  using System.IO;
  using System.Xml;
  using Skyiv.Ben.Common;

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

     /// <summary>
     /// Excel 2007 文件的构造函数
     /// </summary>
     /// <param name="fileName">Excel 2007 文件的文件名</param>
     public XlsxFile(string fileName)
     {
       this.fileName = fileName;
     }

    /// <summary>
     /// Excel 2007 文件的各工作表
     /// </summary>
     public Sheet[] Sheets
     {
       get
       {
         if (sheets == null)
         {
           using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/workbook.xml"))
           {
             // xl/workbook.xml 文件的内容举例如下:
             // <workbook>
             //   <sheets>
             //     <sheet name="好人" sheetId="1" r:id="rId1" />
             //     <sheet name="坏人" sheetId="2" r:id="rId2" />
             //   </sheets>
             // </workboo>
             XmlDocument xmlDocument = new XmlDocument();
             xmlDocument.Load(zs);
             XmlNodeList elms = xmlDocument.DocumentElement["sheets"].ChildNodes;
             sheets = new Sheet[elms.Count];
             for (int i = 0; i < elms.Count; i++)
             {
               XmlAttributeCollection attrs = elms[i].Attributes;
               sheets[i] = new Sheet(attrs["name"].Value, GetXmlFileName(attrs["r:id"].Value), SharedStrings, fileStream);
             }
           }
         }
         return sheets;
       }
    }

     /// <summary>
     /// 根据“标识”给出表示工作表的 XML 文件名
     /// </summary>
     /// <param name="id">标识</param>
     /// <returns>表示工作表的 XML 文件名</returns>
     string GetXmlFileName(string id)
     {
       string value;
       using (Stream zs = Zip.GetZipInputStream(fileStream, "xl/_rels/workbook.xml.rels"))
       {
         // xl/_rels/workbook.xml.rels 文件的内容举例如下:
         // <Relationships>
         //   <Relationship Id="rId1" Target="worksheets/sheet1.xml" />
         //   <Relationship Id="rId2" Target="worksheets/sheet2.xml" />
         // </Relationships>
         XmlDocument xmlDocument = new XmlDocument();
         xmlDocument.Load(zs);
         value = XmlHelper.GetElementById(xmlDocument, id).Attributes["Target"].Value;
       }
       return value;
     }

     public void Dispose()
     {
       if (sheets == null) return;
       foreach (Sheet sheet in sheets) sheet.Dispose();
     }
   }
 }

XlsxFile

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

  using System;
  using System.IO;
  using System.Xml;
  using Skyiv.Ben.Common;

  namespace Skyiv.OfficeHelper
  {
    partial class XlsxFile
    {
     string[] sharedStrings;

     /// <summary>
     /// Excel 2007 文件中各工作表共享的字符串
     /// </summary>
     string[] SharedStrings
     {
       get
       {
         if (sharedStrings == null)
         {
           Stream zs = null;
          try
           {
             zs = Zip.GetZipInputStream(fileStream, "xl/sharedStrings.xml"); // 可能引发(FileNotFoundException)
             // xl/sharedStrings.xml 文件的内容举例如下:
             // <sst count="56" uniqueCount="2">
             //   <si><t>任盈盈</t><phoneticPr fontId="1" type="noConversion" /></si>
             //   <si><t /></si>
             // </sst>
             using (XmlReader reader = XmlReader.Create(zs))
             {
               while (reader.Read()) if (reader.IsStartElement("sst")) break;
               sharedStrings = new string[Convert.ToInt32(reader.GetAttribute("uniqueCount"))];
               for (int count = 0; ; count++)
               {
                 reader.Read();                                               // 执行后(reader)的值: <si> or </sst>
                 if (!reader.IsStartElement("si")) break;
                 reader.ReadStartElement("si");                               // 执行后(reader)的值: <t>  or <t />
                 sharedStrings[count] = reader.ReadElementString("t").Trim(); // 执行后(reader)的值: </si> or <与<t>同级的元素>
                 if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("t"); // 执行后(reader)的值: </si>
               }
             }
           }
           catch (FileNotFoundException)
           {
             sharedStrings = new string[0]; // 如果没有找到 xl/sharedStrings.xml 文件
           }
           finally
           {
             if (zs != null) zs.Close();
           }
         }
         return sharedStrings;
       }
     }
   }
 }

XlsxFile.SharedStrings

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

 using System;
   using System.IO;
   using System.Xml;
   using System.Collections.Generic;
   using Skyiv.Ben.Common;

   namespace Skyiv.OfficeHelper
   {
     partial class XlsxFile
    {
      /// <summary>
      /// Execl 2007 文件中的工作表
      /// </summary>
      public sealed class Sheet : IDisposable
      {
        string[] sharedStrings; // 各工作表共享的字符串
        Stream stream;          // 用于读取本工作表的文件流
        XmlReader reader;       // 用于读取本工作表的 XML 数据读取器
        string dimension;       // 本工作表的范围,如:“A1”、“B2:C4”
        int rowCount;           // 本工作表的有效行数
        string name;            // 本工作表的名称

        public string Dimension { get { return dimension; } }
        public int RowCount { get { return rowCount; } }
        public string Name { get { return name; } }

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

        /// <summary>
        /// 读取本工作表的中一行
        /// </summary>
        /// <returns>读取的行的各字段的内容。如果已经没有可读的行则返回 null。</returns>
        public string[] ReadRow()
        {
          // 表示工作表的 XML 文件(如:xl/worksheets/sheet1.xml)的内容举例如下:
         // <worksheet>
          //   <dimension ref="B2:C4" />
          //   <sheetData>
          //     <row r="2" spans="2:3" />
          //     <row r="4" spans="2:3">
          //       <c r="B4" />
          //       <c r="C4" t="s"><v>1</v></c>
          //     </row>
          //   </sheetData>
          // </worksheet>
          // 注意:在该 XML 文件中可能省略某些空行和空单元格,而本方法忽略这些空行和空单元格。
          // 但本方法不忽略 XML 文件中的空行“<row />”和空单元格“<c />”。
          if (!reader.IsStartElement("row")) reader.Read();
          if (!reader.IsStartElement("row")) return null; // 没有可读的行
          List<string> list = new List<string>();
          for (; ; )
          {
            reader.Read(); // 执行后(reader)的值: <c> or </row> or (other for <row />)
            if (!reader.IsStartElement("c")) break; // 没有可读的单元格
            if (reader.IsEmptyElement) list.Add(""); // 空单元格“<c />”
            else                                     // “<c><v>1</v></c>”
            {
              string attr = reader.GetAttribute("t"); // 如果“<c>”元素的“t”属性不为空,则“<v>”元素的值指向各工作表共享的字符串
              reader.ReadStartElement("c");                            // 执行后(reader)的值: <v> or <v />
              list.Add(GetValue(attr, reader.ReadElementString("v"))); // 执行后(reader)的值: </c> or <与<v>同级的元素>
              if (reader.NodeType != XmlNodeType.EndElement) reader.ReadToNextSibling("v"); // 执行后(reader)的值: </c>
            }
          }
          return list.ToArray();
        }

        /// <summary>
        /// 根据工作表的范围计算有效行数
        /// </summary>
        /// <param name="dimension">工作表的范围,如:“A1”、“B2:C4”</param>
        /// <returns>有效行数</returns>
        int GetRowCount(string dimension)
        {
          if (string.IsNullOrEmpty(dimension)) return -1;
          string[] ss = dimension.Split(‘:‘);
          if (ss.Length == 1) return 1;
          if (ss.Length != 2) return -1;
          return GetRowNumber(ss[1]) - GetRowNumber(ss[0]) + 1;
        }

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

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

       public void Dispose()
       {
         if (reader != null) reader.Close();
         if (stream != null) stream.Close();
         reader = null;
         stream = null;
       }
     }
   }
 }

XlsxFile.Sheet

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

备注:

1.时间的问题,非文本的时间,xml中会自动转换成浮点数存放的,这个浮点数是通过ToOAtime()得到的,这个函数是干嘛的?自己查sdk去。

2.sharedStrings 中<t></t>并不是完全跟<si></si>一对一对应关系的,本文的代码中是默认一对一的,其实 是错误的,因为当单元格中内容若有颜色,字体,大小属性限制某个字符的,会将这个单元格的内容拆分成多个t标签来存放,具体可以自己测试去,因为涉及到保 密,就不提供源码了,处理也很简单,我这里就是告诉下你,这种情况会出问题,要注意!

时间: 2024-10-09 21:35:49

C#读取Excel技术概览 (2)的相关文章

C#读取Excel技术概览

参考文章 C#读取Excel的五种方式体会 1. OleDb 用这种方法读取Excel速度还是非常的快的,但这种方式读取数据的时候不太灵活.不过可以在 DataTable 中对数据进行一些删减.修改.这种方式将Excel作为一个数据源,直接用Sql语句获取数据了.所以读取之前要知道此次要读取的Sheet(当然也可以用序号,类似dt.Row[0][0].这样倒是不需要知道Sheet). if (fileType == ".xls") connStr = "Provider=Mi

java的poi技术读取Excel[2003-2007,2010]

这篇blog主要是讲述java中poi读取excel,而excel的版本包括:2003-2007和2010两个版本, 即excel的后缀名为:xls和xlsx. 读取excel和MySQL相关: java的poi技术读取Excel数据到MySQL 你也可以在 : java的poi技术读取和导入Excel了解到写入Excel的方法信息 使用JXL技术 :java的jxl技术导入Excel  下面是本文的项目结构: 项目中所需要的jar文件: 所用的Excel数据(2003-2007,2010都是一

java的poi技术读取Excel数据

这篇blog主要是讲述java中poi读取excel,而excel的版本包括:2003-2007和2010两个版本, 即excel的后缀名为:xls和xlsx. 读取excel和MySQL相关: java的poi技术读取Excel数据到MySQL 你也可以在 : java的poi技术读取和导入Excel了解到写入Excel的方法信息 使用JXL技术 :java的jxl技术导入Excel  下面是本文的项目结构: 项目中所需要的jar文件: 所用的Excel数据(2003-2007,2010都是一

java的poi技术读取Excel数据到MySQL

这篇blog是介绍java中的poi技术读取Excel数据,然后保存到MySQL数据中. 你也可以在 : java的poi技术读取和导入Excel了解到写入Excel的方法信息 使用JXL技术可以在 :java的jxl技术导入Excel  项目结构: Excel中的测试数据: 数据库结构: 对应的SQL: 1 CREATE TABLE `student_info` ( 2 `id` int(11) NOT NULL AUTO_INCREMENT, 3 `no` varchar(20) DEFAU

Delphi中使用python脚本读取Excel数据

Delphi中使用python脚本读取Excel数据2007-10-18 17:28:22标签:Delphi Excel python原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://seewind.blog.51cto.com/249547/46669前段时间,在正式项目中使用Python来读取Excel表格的数据.具体需求是,项目数据库中有些数据需要根据Excel表格里面的数据进行一些调整,功能应该比较简单.为了学习Pyth

程序员的量化交易之路(2)----Esper文档学习之技术概览(1)

转载请注明出处:http://blog.csdn.net/minimicall/ 在接下来的20个工作日中,我将坚持翻译或者略翻译Esper的官方文档. 为什么需要学习Esper,因为我们需要理解复合事件处理 Complex Event Processing (CEP).在量化交易系统中,CEP是必不可少的.它负责处理海量的实时事件. 关于CEP更多知识,大家可以翻阅网络相关资料.我这里集中在学习开源的CEP系统,Esper.. 今天开始第一篇:技术概览. 1. CEP和事件序列分析 Esper

C#读取excel文件的内容(使用DataSet)

C#读取Excel文件的内容,通过OLEDB来连接,关键是连接的路径,如:string strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filePath + ";Extended Properties=Excel 12.0;";      连接的路径涉及3方面: 1. Provider:使用的是OLEDB连接,但是这个技术会不时更新,使用前查询最新的版本: 2. Data Source: 就是

转:Linux网络IO并行化技术概览

转:http://codinginet.com/articles/view/201605-linux_net_parallel?simple=1&from=timeline&isappinstalled=0 Linux网络IO并行化技术概览 By mikewei at 2016-05-21 00:30 阅读(276) 过去的十年中互联网经历了爆发式的增长,这背后有什么技术平台起了最为关键的作用,我认为是Linux,即使在云计算流行的今天,它依然是最重要的一块基石.我们或许经常听到关于什么是

OLDB读取excel的数据类型不匹配的解决方案(ZT)

1 引言  在应用程序的设计中,经常需要读取Excel数据或将Excel数据导入转换到其他数据载体中,例如将Excel数据通过应用程序导入SQL Sever等数据库中以备使用.笔者在开发“汽车产业链ASP协同商务平台”中遇到了类似需求.某汽车整车生产企业需要将其车辆发车信息发布到汽车产业链平台上去,其数据为内部ERP系统生成的Excel数据表,用户首先将该数据表上传至汽车产业链平台,平台将此Excel数据读取导入到平台内部的SQL Sever数据库中,以供其它应用使用.汽车产业链平台的开发使用的