65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 访问 Excel 2003 工作簿的不同方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2012 年 1 月 31 日

CPOL

7分钟阅读

viewsIcon

83910

downloadIcon

2584

比较 Excel 对象库和原生 C# 库 NPOI 以从 .xls 文件中提取信息。

Excel 2003 和 Visual Studio 2010

在我们的一个项目中,需要从现有的 Excel 2003 工作簿中提取信息(*.xls* 文件扩展名)。客户方无法选择使用较新的基于 XML 的格式。所选 IDE Visual Studio 2010 的 Visual Studio Tools for Office 不再支持 Excel 2003(请参阅按 Office 应用程序和项目类型提供的功能,http://msdn.microsoft.com/en-us/library/aa942839.aspx)。

因此,在关注 Microsoft 的规定时,Microsoft Excel 11.0 对象库的 COM Interop 是唯一的方法。但是此解决方案取决于安装的 Excel 版本。因此,为了能够使用和测试 Excel 2003 绑定,首先需要安装 Excel 2003。

本文随附的下载(Compare_COM_vs_NPOI_v11_20120221.zip - 1 MB)也提供了一个示例 Excel 文件。它包含一些带有内容和计算的工作表。

322469/screenshot_excel.png

使用 Excel 11.0 对象库访问 Excel 2003 文件

以下代码片段展示了对 Excel 2003 文件全部内容的简单遍历。首先,使用 Excel `Application` 实例打开 *.xls* 文件。然后,逐行逐列遍历所有工作表。每个单元格的值和注释都打印到控制台屏幕上。

文章“使用 C# 打开和导航 Excel”(https://codeproject.org.cn/Articles/5123/Opening-and-Navigating-Excel-with-C)提供了关于 COM 互操作的全面分步教程。

try
{
    xlApp = new Excel.Application();

    if (null == xlApp)
    {
        Console.WriteLine("Excel could not be started. Check that your " + 
          "office installation and project references are correct.");
        return;
    }

    bool openReadOnly = true;
    xlWorkbook = xlApp.Workbooks.Open(extractFile,
        0, openReadOnly, 5, "", "", false, 
        Excel.XlPlatform.xlWindows, "",
        true, false, 0, true, false, false);
    if (null == xlWorkbook)
    {
        Console.WriteLine(string.Format(
          "Excel Workbook '{0}' could not be opened.", extractFile));
    }

    Console.WriteLine();
    foreach (Excel.Worksheet xlSheet in xlWorkbook.Worksheets)
    {
        if (null != xlSheet)
        {
            Console.WriteLine();
            Console.WriteLine("***   Worksheet " + xlSheet.Name + "   ***");

            Excel.Range usedRange = xlSheet.UsedRange;
            if ((null != usedRange) && (null != usedRange.Cells))
            {
                xlCells = usedRange.Cells;
                for (int rowNumber = 1; rowNumber <= usedRange.Rows.Count; ++rowNumber)
                {
                    if (1 == (rowNumber % 10)) { Console.WriteLine(); }
                    Console.Write(string.Format("Row {0,2}: ", rowNumber));
                    for (int colNumber = 1; colNumber <= usedRange.Columns.Count; ++colNumber)
                    {
                        dynamic cell = xlCells[rowNumber, colNumber];
                        if (null != cell)
                        {
                            string value = GetValue(cell);
                            string comment = GetComment(cell);
                            Console.Write(string.Format("{0}{1};",
                                string.IsNullOrWhiteSpace(value) ? "" : value,
                                string.IsNullOrWhiteSpace(comment) ? "" : " [" + comment + "]"));
                        }
                        else
                        {
                            Console.Write(";");
                        }
                    }
                    Console.WriteLine();
                }
            }
            Marshal.FinalReleaseComObject(xlSheet);
            Console.WriteLine();
        }
    }
    xlWorkbook.Close(Type.Missing, Type.Missing, Type.Missing);
    xlApp.Quit();
}
catch (Exception ex)
{
    Console.WriteLine("Exception: " + ex.Message);
    if (null != ex.InnerException)
    {
        Console.WriteLine("Inner Exception: " + ex.InnerException.Message);
    }
}
finally
{
    if (null != xlUsedRange) { Marshal.FinalReleaseComObject(xlUsedRange); xlUsedRange = null; }
    if (null != xlCells) { Marshal.FinalReleaseComObject(xlCells); xlCells = null; }
    if (null != xlWorkbook) { Marshal.FinalReleaseComObject(xlWorkbook); xlWorkbook = null; }
    if (null != xlApp) { Marshal.FinalReleaseComObject(xlApp); xlApp = null; }
}

要从单元格中提取内容,使用 `GetValue` 方法。

private static string GetValue(dynamic cell)
{
    string ret = string.Empty;
    if (null == cell) { return ret; }
    Excel.Range singleCell = cell as Excel.Range;
    if (null == singleCell) { return ret; }
    if (null != singleCell.Text)
    {
        ret = singleCell.Text as string;
    }
    if (null == ret) { return string.Empty; }
    return ret.Replace("\n", " "); // remove line break
}

使用 `GetComment` 方法提取单元格的注释。

private static string GetComment(dynamic cell)
{
    string ret = string.Empty;
    if ((null == cell) || (null == cell.Comment)) { return ret; }
    ret = cell.Comment.Text as string;
    if (null == ret) { return string.Empty; }
    return ret.Replace("\n", " "); // remove line break
}

使用 NPOI 库访问 Excel 2003 文件 - 分步说明

NPOI 库提供了另一种访问方法。该库在 Microsoft CodePlex (http://npoi.codeplex.com/) 上提供,并在 GitHub (https://github.com/tonyqus/npoi) 上开发。该实现仅用 C# 完成,因此提供了对 Excel 文件的纯托管访问。

NPOI 库本身是从一个名为 Apache POI 的 Java 项目移植过来的(Apache POI - Microsoft 文档的 Java API,http://poi.apache.org/)。这两个库都成熟且正在积极开发中。

以下代码片段使用 NPOI 库打印 Excel 2003 文件的全部内容。最初,使用标准流读取器读取 XLS 文件。可以根据需要将通常的标准标志应用于流读取器。从 `POIFSFileSystem` 类读取流。此类读取和写入 Microsoft 的 OLE 2 复合文档格式。这是 Excel 文件格式的基础格式。最后,`HSSFWorkbook` 反映了 Excel 用户可见的 Excel 文件结构。

using (StreamReader input = new StreamReader(extractFile))
{
    IWorkbook workbook = new HSSFWorkbook(new POIFSFileSystem(input.BaseStream));
    if (null == workbook)
    {
        Console.WriteLine(string.Format("Excel Workbook '{0}' could not be opened.", extractFile));
        return;
    }
    ...
}

接下来,评估工作簿的公式并创建数据格式化程序实例。

IFormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator(workbook);
DataFormatter dataFormatter = new HSSFDataFormatter(new CultureInfo("en-US"));

工作簿的各个工作表可以通过数字、名称或使用迭代器访问。这是一个基于迭代器的示例。

foreach (ISheet sheet in workbook)
{
    Console.WriteLine("\n\n***   Worksheet " + sheet.SheetName + "   ***");
    ...
}

再次使用迭代器访问给定工作表的行。行号从 `IRow` 类型变量中获取并打印到控制台输出。

foreach (IRow row in sheet)
{
    Console.Write(string.Format("Row {0,2}: ", row.RowNum + 1));
}

现在,从行的每个单元格中提取内容,进行格式化,并写入控制台。这同样适用于与单元格关联的注释。

foreach (ICell cell in row)
{
    string value = GetValue(cell, dataFormatter, formulaEvaluator);
    string commentText = GetComment(cell);
    Console.Write(string.Format("{0}{1};",
        string.IsNullOrWhiteSpace(value) ? "" : value,
        string.IsNullOrWhiteSpace(commentText) ? "" : " [" + commentText + "]"));
}

使用 NPOI 库访问 Excel 2003 文件 - 完整示例

这是完整的代码片段,可以与上面的 COM Interop 片段进行比较。

try
{
    using (StreamReader input = new StreamReader(extractFile))
    {
        IWorkbook workbook = new HSSFWorkbook(new POIFSFileSystem(input.BaseStream));
        if (null == workbook)
        {
            Console.WriteLine(string.Format("Excel Workbook '{0}' could not be opened.", extractFile));
            return;
        }
        // calculates/updates the formulas on the given workbook
        IFormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator(workbook);
        DataFormatter dataFormatter = new HSSFDataFormatter(new CultureInfo("en-US"));

        Console.WriteLine();
        foreach (ISheet sheet in workbook)
        {
            Console.WriteLine("\n\n***   Worksheet " + sheet.SheetName + "   ***");
            foreach (IRow row in sheet)
            {
                if (0 == row.RowNum % 10) { Console.WriteLine(); }
                Console.Write(string.Format("Row {0,2}: ", row.RowNum + 1));
                foreach (ICell cell in row)
                {
                    string value = GetValue(cell, dataFormatter, formulaEvaluator);
                    string commentText = GetComment(cell);
                    Console.Write(string.Format("{0}{1};",
                        string.IsNullOrWhiteSpace(value) ? "" : value,
                        string.IsNullOrWhiteSpace(commentText) ? "" : " [" + commentText + "]"));
                }
                Console.WriteLine();
            }
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine("Exception: " + ex.Message);
    if (null != ex.InnerException)
    {
        Console.WriteLine("Inner Exception: " + ex.InnerException.Message);
    }
}

再次使用 `GetValue` 方法从单元格中提取内容。但这次是 NPOI 变体

private static string GetValue(ICell cell, DataFormatter dataFormatter, 
                      IFormulaEvaluator formulaEvaluator)
{
    string ret = string.Empty;
    if (null == cell) { return ret; }
    ret = dataFormatter.FormatCellValue(cell, formulaEvaluator);
    return ret.Replace("\n", " "); // remove line break
}

使用 `GetComment` 方法提取单元格的注释。

private static string GetComment(ICell cell)
{
    string ret = string.Empty;
    if ((null == cell) || (null == cell.CellComment)) { return ret; }
    IRichTextString str = cell.CellComment.String;
    if (str != null && str.Length > 0)
    {
        ret = str.ToString();
    }
    return ret.Replace("\n", " "); // remove line break
}

这是显示部分提取信息的屏幕截图。

322469/screenshot_extract_beginning_600px.png

比较 NPOI 和 Excel 对象库

比较这两个代码片段,很明显 NPOI 提供了一个**现代接口**,例如,可以使用 `foreach` 迭代实例,并且资源会自动安全地释放。此外,NPOI 提供了一个强类型接口,这是大多数 C# 开发人员今天所期望的。库 API 以清晰的 C# 方式设计。这与 COM Interop 不同,COM Interop 中大多数参数类型都是 `object`。

令我们惊讶的是,NPOI 的实现**比 COM 替代方案快得多**。随附的存档中的程序包含用于测量的秒表。测试机是 Core i7 860@2.8GHz,3 GB RAM,Win7 32 位。多次执行这两个程序,COM Interop 大约需要 2500 - 4200 毫秒才能完成,而 NPOI 程序只需要 350 - 410 毫秒。即使我们项目中使用的更大样本仍然几乎立即返回。尤其是在避免冗长的控制台输出时更是如此。但请尝试使用您自己的样本。

NPOI 库方法的**文档**可提供与标准 .NET 类相同的水平,包括 Intellisense 帮助和导航到文档化方法。COM Interop DLL 仅提供方法的名称和参数列表,其中结构化类型都是无意义的基本 `object` 类型。NPOI 库提供完整的源代码。源代码在 Apache 许可证 2.0 下获得许可,可用于**任何项目,甚至是商业和专有项目**。

由于 `StreamReader` 的存在,以只读文件形式打开 Excel 文件进行提取在 NPOI 中变得**自然**。COM Interop 需要使用带有长参数列表的 `Open` 方法。当从客户提供的 *.xls* 文件中提取信息时,它会按预期打印内容,但使用 COM 互操作时,每次调用后都会弹出一个“保存更改”对话框。以只读文件形式打开 *.xls* 文件并不能避免这个不需要的对话框。原因似乎是在访问某些单元格期间公式的更新。这与 NPOI 不同,NPOI 不会更改 *.xls* 文件。

Excel 始终在 PC 上以单个实例运行。因此,它不得用于**服务器环境**。NPOI 没有此限制。

本文的下载**存档**提供了一个包含三个项目的 Visual Studio 2010 解决方案。每个项目都提取 Excel 文件的全部信息。第一个名为 *ReadExcelUsingCom* 的项目使用 COM Interop DLL 来执行任务。这是本文开头示例所提取的项目。第二个名为 *ReadExcelUsingNPOI* 的项目是上面详细解释的使用 NPOI 的示例。该程序显示 Excel 文件中的所有内容。但是 COM Interop 程序也显示空行。因此,开发了第三个程序,即使使用 NPOI,它也能生成与 COM 示例完全相同的输出。最后一个程序包含在 `ReadExcelUsingNPOICloneCom` 项目中。

除了这三个程序之外,解决方案文件还包含最新版本的 NPOI DLL。因此,这些示例有望开箱即用。当然,缺少 Excel 2003,必须单独安装才能使 COM Interop 示例正常工作。重复一遍,需要安装 Excel 2003,不支持其他版本。

NPOI 库和 Excel 11.0 对象库的 COM 互操作的比较可以**总结**如下:

  • NPOI 提供了一个强类型接口。
  • 独立的 NPOI 库不依赖于已安装的 Excel 版本。
  • 即使只用三个小工作表进行测试,NPOI 也明显更快。
  • NPOI 读取 *.xls* 文件时不会对其进行更改。
  • Microsoft 仅支持 Excel 11.0 对象库。
  • 开源 NPOI 库提供完整的源代码。

历史

  • 初始文章
  • 更新了下载和文章到 NPOI 1.2.5 预发布版,发布日期为 2012-02-21,svn r309
  • NPOI 库的源代码已移至 https://github.com/tonyqus/npoi。已修复上面的引用
© . All rights reserved.