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

将HTML表格内容导出到Excel - 服务器端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (5投票s)

2017年5月9日

CPOL

4分钟阅读

viewsIcon

36263

获取表格的HTML结构并导出到真正的Excel文件 - 服务器端处理。

引言

任何报告解决方案的一个非常普遍的需求是,一旦用户在浏览器中以 HTML 格式查看了报告,就允许用户将报告内容导出为 Excel 或 PDF。有一些 Nuget 包可以很好地处理 PDF(例如 wkhtmltopdf),但我从未找到一个像样的 Excel 导出工具。市面上有一些 jQuery 库声称可以完成这项工作,但对于任何客户端工具,我最大的问题是用户尝试打开下载的 Excel 文件时收到的“文件格式”警告。

背景

我决定编写一些 C# 代码,它接受一个包含表格 HTML 代码的字符串,最终将生成为 Excel 文件供用户下载。用户只需在其浏览器中单击导出按钮,即可将已渲染的 HTML 报告以真正的 xlsx 文件格式下载到用户的浏览器窗口中。

我们将使用 NPOI v2.3.0,它作为 nuget 包在 Visual Studio 中可用。网址是 http://npoi.codeplex.com/。这是著名的 Apache POI 库的 .NET 实现,用于读取和写入 MS Office 文档。

我们将使用 System.Xml.LinqXElement 及其他相关类)在服务器端(C#)解析 HTML 表格结构。

每个 Excel 单元格将由一个名为 ExcelCellMeta 的 POCO 类表示,该类具有 5 个 public 属性。

    public class ExcelCellMeta
    {
        public string Content { get; set; }
        public string StyleName { get; set; }
        public int ColumnIndex { get; set; }
        public int ColSpan { get; set; }
        public int RowSpan { get; set; }
    }   
  1. Content 代表 Excel 单元格的内容。
  2. StyleName 是代码中定义的 Excel 样式对象名称。目前我只有有限数量的 Excel 样式。如果未指定,我们的引擎将应用默认样式,我在代码中将其命名为“Content”。
  3. ColumnIndex 是 Excel 单元格的起始索引。如果一个 Excel 单元格跨越两个 Excel 列 C 和 D,则其索引为 2(对应于 C,即单元格的起始列)。
  4. ColSpan 是一个单元格跨越的列数。
  5. RowSpan 是一个单元格跨越的行数。

整个 Excel 工作表由一个名为 ExcelMeta 的 POCO 类表示,该类具有两个 public 属性。

    public class ExcelMeta
    {
        public List<List<ExcelCellMeta>> Meta { get; set; }
        public double[] ColumnWidths { get; set; }
    }
  1. 名为 Meta 的属性是 ExcelCellMeta 的列表。内部列表代表一个 Excel 行(ExcelCellMeta 对象的集合);外部列表代表一组行或一个 Excel 工作表。
  2. ColumnWidths 是一个数组,用于设置每个 Excel 列的宽度。它应包含与您需要的 Excel 列数完全相同的元素。大多数情况下,您会希望将这些宽度保持在 1.0 到 2.0 之间,因为在代码中,我将把这些宽度乘以 5120。这只是我的偏好。

为了使用此工具,您当然可以在代码中设置 ExcelMeta 对象,完全无需设置 HTML 表格结构并将其解析为 ExcelMeta 对象,但这正是我着手要做的事情。

还有另一个类,名为 ExcelHelper。这个类负责将 HTML 表格结构解析为 ExcelMeta 并生成 Excel 文件。它有 5 个样式对象

    public ICellStyle heading1 { get; set; }
    public ICellStyle heading2 { get; set; }
    public ICellStyle rowHead { get; set; }
    public ICellStyle columnHead { get; set; }
    public ICellStyle content { get; set; }

和两个方法

    public  ExcelMeta GetExcelMeta(string theTableHtml);
    public  byte[] GetExcelDocument(ExcelMeta excelMeta);

您将调用第一个方法将 HTML 表格结构转换为 ExcelMeta 对象,然后将其传递给第二个方法以获取字节数组形式的 Excel 文档。

Using the Code

为了装饰表格的 HTML,我们需要使用一些自定义 HTML 属性。

table”标签必须“属性化”为 'data-xls-columns' 和 'data-xls-column-widths',如下所示

<table class="table table-bordered" data-xls-columns="5" 
       data-xls-column-widths="1,1.5,1.5,1,1">

在这里,我们告诉我们的 ExcelMeta 解析器,我们希望在 Excel 工作表中包含 5 列,并且第 2 列和第 3 列的宽度应该是其他列的 1.5 倍。

如果希望从 Excel 工作表中排除某个表格行,则“tr”可以属性化为 'data-xls-exclude="True"'。

    <tr data-xls-exclude="True">

td”可以属性化为 rowspancolspandata-xls-col-indexdata-xls-classrowspancolspan 的作用与它们在 HTML 中的作用相同,是我们使用的唯一两个标准的 HTML 属性。data-xls-col-index 用于指示 Excel 单元格的起始列索引。A 列对应 0,B 列对应 1,C 列对应 2,依此类推。data-xls-class 的值必须是我们在 ExcelHelper 类中定义的 5 个 ICellStyle 对象之一,即 heading1heading2rowHeadcolumnHeadcontent

    <tr>
                <td colspan=2 rowspan=2 data-xls-col-index="0" 
                    data-xls-class="rowHead" class="rowHead"> A 2x2 cell </td>
                <td colspan=3 data-xls-col-index="2" > A 1X3 cell </td>
            </tr>
            <tr>
                <td colspan=3 data-xls-col-index="2" >A 1X3 cell </td>
            </tr>

您注意到第二行中的第一列是如何被“data-xls-col-index”设置为 2(C 列)的吗?这是因为第一行中的 2x2 单元格在第一行和第二行中都占据了“data-xls-col-index”0 和 1(A 和 B)。

一个示例 HTML 表格可能如下所示

    <table class="table table-bordered" data-xls-columns="5" 
           data-xls-column-widths="1,1.5,1.5,1,1">
            <tbody><tr data-xls-exclude="True">
                <td colspan="5" style="text-align:right;">
                <a id="exportLink" href="#" class="btn btn-default">
                <span class="glyphicon glyphicon-th"></span> Export to Excel</a></td>
            </tr>
            
            <tr>
                <td colspan="5" data-xls-col-index="0" 
                 data-xls-class="heading1" class="heading1">Searching criteria</td>
            </tr>
           
            <tr>
                <td colspan="2" data-xls-col-index="0" data-xls-class="rowHead" 
                 class="rowHead">Search Param 1</td>
                <td colspan="3" data-xls-col-index="2">8821650318629</td>
            </tr>
            <tr>
                <td colspan="2" data-xls-col-index="0" data-xls-class="rowHead" 
                 class="rowHead">Period</td>
                <td colspan="3" data-xls-col-index="2">04/04/2017 - 03/05/2017</td>
            </tr>
            <tr>
                <td colspan="2" rowspan="2" data-xls-col-index="0" 
                 data-xls-class="rowHead" class="rowHead">Search Param 2</td>
                <td colspan="3" data-xls-col-index="2">Call Details</td>
            </tr>
            <tr>
                <td colspan="3" data-xls-col-index="2">GPS Location Information</td>
            </tr>
            <tr>
                <td colspan="2" rowspan="2" data-xls-col-index="0" 
                 data-xls-class="rowHead" class="rowHead">Search Param 3</td>
                <td colspan="3" data-xls-col-index="2">MS - ORIGINATING</td>
            </tr>
            <tr>
                <td colspan="3" data-xls-col-index="2">MS - TERMINATING</td>
            </tr>
            <tr>
                <td colspan="5" data-xls-col-index="0" 
                 data-xls-class="heading1" class="heading1">Summary Report</td>
            </tr>
            <tr>
                <td colspan="5" data-xls-col-index="0" 
                 data-xls-class="heading2" class="heading2">Originating Call Details</td>
            </tr>
            <tr>
                <td data-xls-col-index="0" data-xls-class="columnHead" 
                 class="columnHead">Number calling </td>
                <td data-xls-col-index="1" data-xls-class="columnHead" 
                 class="columnHead">Number called</td>
                <td data-xls-col-index="2" data-xls-class="columnHead" 
                 class="columnHead">Date(YYYY/MM/DD HH:MM:SS) </td>
                <td data-xls-col-index="3" data-xls-class="columnHead" 
                 class="columnHead">Call Duration(sec) </td>
                <td data-xls-col-index="4" data-xls-class="columnHead" 
                 class="columnHead">Country </td>
            </tr>
            <tr>
                <td data-xls-col-index="0">8821650318629 </td>
                <td data-xls-col-index="1">393662555630 </td>
                <td data-xls-col-index="2">2017/05/02 16:31:17 </td>
                <td data-xls-col-index="3">0 </td>
                <td data-xls-col-index="4">Mediterranean Sea </td>
            </tr>
            <tr>
                <td data-xls-col-index="0">8821650318629 </td>
                <td data-xls-col-index="1">393662555630 </td>
                <td data-xls-col-index="2">2017/05/02 03:40:30 </td>
                <td data-xls-col-index="3">0 </td>
                <td data-xls-col-index="4">Italy </td>
            </tr>
            <tr>
                <td colspan="5" data-xls-col-index="0" 
                 data-xls-class="heading2" class="heading2">Terminating Call Details</td>
            </tr>
            <tr>
                <td data-xls-col-index="0" data-xls-class="columnHead" 
                 class="columnHead">Number calling </td>
                <td data-xls-col-index="1" data-xls-class="columnHead" 
                 class="columnHead">Number called </td>
                <td data-xls-col-index="2" data-xls-class="columnHead" 
                 class="columnHead">Date(YYYY/MM/DD HH:MM:SS) </td>
                <td data-xls-col-index="3" data-xls-class="columnHead" 
                 class="columnHead">Call Duration(sec) </td>
                <td data-xls-col-index="4" data-xls-class="columnHead" 
                 class="columnHead">Country </td>
            </tr>
            <tr>
                <td data-xls-col-index="0" colspan="5" class="NoRecord">No Record found.</td>
            </tr>
            <tr>
                <td data-xls-col-index="0" colspan="5" 
                 data-xls-class="heading2" class="heading2">GPS Location Information </td>
            </tr>
            <tr>
                <td data-xls-col-index="0" data-xls-class="columnHead" 
                 class="columnHead">IMEI </td>
                <td data-xls-col-index="1" data-xls-class="columnHead" 
                 class="columnHead">Date(YYYY/MM/DD HH:MM:SS) </td>
                <td data-xls-col-index="2" data-xls-class="columnHead" 
                 class="columnHead">Country </td>
                <td data-xls-col-index="3" data-xls-class="columnHead" 
                 class="columnHead">Latitude </td>
                <td data-xls-col-index="4" data-xls-class="columnHead" 
                 class="columnHead">Longitude </td>
            </tr>
            <tr>
                <td data-xls-col-index="0">3560130010789500 </td>
                <td data-xls-col-index="1">2017/05/03 16:02:51 </td>
                <td data-xls-col-index="2">ITALY </td>
                <td data-xls-col-index="3">45.220586 </td>
                <td data-xls-col-index="4">12.282395 </td>
            </tr>
             
        </tbody></table>

在我的 MVC 应用程序中,我使用 class 来方便地将 HTML 代码从 View 提交到 Controller

public class ExcelModel
    {
        [AllowHtml]
        public string Data { get; set; }
    }
    /

您可以使用此 jQuery 来提交 HTML

    function exportToExcel(id)
    {
        $('#Data').remove();
        $(id).attr("action","@Url.Action("ExportExcel")");
        tableHtml = $(id).html();
        $(id).append("<input id="Data" name="Data" type="hidden" />");
        $('#Data').val(tableHtml);
        $(id).submit();
    }
    $(document).ready(function () {
        $("#exportLink").click(function () { exportToExcel("#exportTable");});
    });

用于将 Excel 文件提供给视图的 MVC 控制器方法如下所示

    public ActionResult ExportExcel(ExcelModel excelModel)
        {
            ExcelHelper excelHelper= new ExcelHelper();
            ExcelMeta meta = excelHelper.GetExcelMeta(excelModel.Data);


            byte[] content = excelHelper.GetExcelDocument(meta);
            FileContentResult result = new FileContentResult
            (content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
            {
                FileDownloadName = "CARS.xlsx"
            };

            return result;
        }

您可以在此处下载 VS2017 解决方案。

历史

  • 2017 年 5 月 9 日:初始版本
© . All rights reserved.