【问题标题】:Export a large data query (60k+ rows) to Excel将大型数据查询(60k+ 行)导出到 Excel
【发布时间】:2012-07-07 09:53:55
【问题描述】:

我创建了一个报告工具作为内部网络应用程序的一部分。报告在 GridView 中显示所有结果,我使用 JavaScript 将 GridView 的内容逐行读取到 Excel 对象中。 JavaScript 继续在不同的工作表上创建数据透视表。

不幸的是,我没想到如果返回超过几天,GridView 的大小会导致浏览器出现过载问题。该应用程序每天有几千条记录,假设每月 60k,理想情况下,我希望能够返回长达一年的所有结果。行数导致浏览器挂起或崩溃。

我们在带有 SQL Server 的 Visual Studio 2010 上使用 ASP.NET 3.5,预期的浏览器是 IE8。该报告由一个gridview 组成,该gridview 根据用户选择的人群从少数几个存储过程中的一个中获取数据。网格视图位于更新面板中:

<asp:UpdatePanel ID="update_ResultSet" runat="server">
<Triggers>
    <asp:AsyncPostBackTrigger ControlID="btn_Submit" />
</Triggers>
<ContentTemplate>
<asp:Panel ID="pnl_ResultSet" runat="server" Visible="False">
    <div runat="server" id="div_ResultSummary">
        <p>This Summary Section is Automatically Completed from Code-Behind</p>
    </div>
        <asp:GridView ID="gv_Results" runat="server" 
            HeaderStyle-BackColor="LightSkyBlue" 
            AlternatingRowStyle-BackColor="LightCyan"  
            Width="100%">
        </asp:GridView>
    </div>
</asp:Panel>
</ContentTemplate>
</asp:UpdatePanel>

我对我的团队来说相对较新,所以我遵循他们将存储过程返回到 DataTable 并将其用作后面代码中的 DataSource 的典型做法:

    List<USP_Report_AreaResult> areaResults = new List<USP_Report_AreaResult>();
    areaResults = db.USP_Report_Area(ddl_Line.Text, ddl_Unit.Text, ddl_Status.Text, ddl_Type.Text, ddl_Subject.Text, minDate, maxDate).ToList();
    dtResults = Common.LINQToDataTable(areaResults);

    if (dtResults.Rows.Count > 0)
    {
        PopulateSummary(ref dtResults);
        gv_Results.DataSource = dtResults;
        gv_Results.DataBind();

(我知道你在想什么!但是,是的,从那以后我学到了更多关于参数化的知识。)

LINQToDataTable 函数没有什么特别之处,只是将列表转换为数据表。

有几千条记录(最多几天),这可以正常工作。 GridView 显示结果,并且有一个按钮供用户单击以启动 JScript 导出器。外部 JavaScript 函数将每一行读入 Excel 工作表,然后使用它来创建数据透视表。数据透视表很重要!

function exportToExcel(sMyGridViewName, sTitleOfReport, sHiddenCols) {
//sMyGridViewName = the name of the grid view, supplied as a text
//sTitleOfReport = Will be used as the page header if the spreadsheet is printed
//sHiddenCols = The columns you want hidden when sent to Excel, separated by semicolon (i.e. 1;3;5).
//              Supply an empty string if all columns are visible.

var oMyGridView = document.getElementById(sMyGridViewName);

//If no data is on the GridView, display alert.
if (oMyGridView == null)
    alert('No data for report');
else {
    var oHid = sHiddenCols.split(";");  //Contains an array of columns to hide, based on the sHiddenCols function parameter
    var oExcel = new ActiveXObject("Excel.Application");
    var oBook = oExcel.Workbooks.Add;
    var oSheet = oBook.Worksheets(1);
    var iRow = 0;
    for (var y = 0; y < oMyGridView.rows.length; y++)
    //Export all non-hidden rows of the HTML table to excel.
    {
        if (oMyGridView.rows[y].style.display == '') {
            var iCol = 0;
            for (var x = 0; x < oMyGridView.rows(y).cells.length; x++) {
                var bHid = false;
                for (iHidCol = 0; iHidCol < oHid.length; iHidCol++) {
                    if (oHid[iHidCol].length !=0 && oHid[iHidCol] == x) {
                        bHid = true;
                        break; 
                    } 
                }
                if (!bHid) {
                    oSheet.Cells(iRow + 1, iCol + 1) = oMyGridView.rows(y).cells(x).innerText;
                    iCol++;
                }
            }
            iRow++;
        }
    }

我正在尝试做的事情:创建一个可以处理这些数据并将其处理到 Excel 中的解决方案(可能是客户端)。有人可能会建议使用HtmlTextWriter,但 afaik 不允许自动生成数据透视表并创建令人讨厌的弹出警告......

我的尝试:

  • 填充 JSON 对象 -- 我仍然认为这很有潜力,但我还没有找到让它工作的方法。
  • 使用 SQLDataSource -- 我似乎无法使用它来获取任何数据。
  • 分页和循环浏览页面 -- 混合进度。虽然总体上很难看,但我仍然有一个问题,即为每个显示的页面查询并返回整个数据集。

更新: 我仍然对替代解决方案持开放态度,但我一直在追求 JSON 理论。我有一个可以从 DataTable 生成 JSON 对象的有效服务器端方法。我不知道如何将该 JSON 传递到(外部)exportToExcel JavaScript 函数中......

    protected static string ConstructReportJSON(ref DataTable dtResults)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("var sJSON = [");
        for (int r = 0; r < dtResults.Rows.Count; r++)
        {
            sb.Append("{");
            for (int c = 0; c < dtResults.Columns.Count; c++)
            {
                sb.AppendFormat("\"{0}\":\"{1}\",", dtResults.Columns[c].ColumnName, dtResults.Rows[r][c].ToString());
            }
            sb.Remove(sb.Length - 1, 1); //Truncate the trailing comma
            sb.Append("},");
        }
        sb.Remove(sb.Length - 1, 1);
        sb.Append("];");
        return sb.ToString();
    }

谁能展示一个如何将这个 JSON 对象携带到外部 JS 函数中的示例?或任何其他用于导出到 Excel 的解决方案。

【问题讨论】:

  • "填充 JSON 对象" ...我在阅读问题时的第一个想法 :)
  • 也许您可以直接使用 Excel 对象? msdn.microsoft.com/en-us/library/wss56bz7(v=vs.80).aspx
  • 你真的是通过客户端js创建一个Excel对象吗?似乎您需要为此设置非常低的浏览器安全设置。创建一个将 Excel 文件填充到服务器然后下载到客户端的过程不是更容易吗?
  • @Chandu - 这也是我的想法!我觉得我已经接近了——我有一个生成 JSON 对象的工作方法——但我仍然不知道如何将 JSON 插入到上面的 JS 中。 :-( Aerik - 也许。如果这是一个胖应用程序,我会这样做。是否可以让 Excel 与 Web 应用程序互操作?约翰 - 糟糕,我的错。
  • TimWilliams - 浏览器设置为中高。因为它是一个仅限 IE 的环境,所以客户端可以使用 ActiveX JS。

标签: c# javascript asp.net excel gridview


【解决方案1】:

我会尝试使用displaytag 来显示结果。您可以将其设置为每页显示一定数量,这应该可以解决您的超载问题。然后,您可以设置 displaytag 以允许 Excel 导出。

【讨论】:

  • 不错的主意,但我想尽可能避免使用第三方插件。
  • @RJB 为什么要避免使用第三方库?特别是像 displaytag 这样的轻量级库。他们可以成为你最好的朋友。为什么要重新发明轮子?
【解决方案2】:

我们通常使用“导出”命令按钮来处理此问题,该按钮连接到服务器端方法以获取数据集并将其转换为 CSV。然后我们调整响应头,浏览器会将其视为下载。我知道这是一个服务器端解决方案,但您可能需要考虑它,因为在您实施服务器端记录分页之前,您将继续遇到超时和浏览器问题。

【讨论】:

  • 我认为这将类似于 HtmlTextWriter 方法,具有一些类似的缺点。但我并不反对服务器端解决方案,只要它仍然能够以编程方式操作 Excel 以生成数据透视表。
  • 我相信有一个 API 可以创建高级工作簿 excel 文件,例如以编程方式创建数据透视表。在我们的情况下,我们通常在导出为 CSV 之前在 C# 中对数据进行透视和操作。
【解决方案3】:

自从我开始这个问题以来已经将近一个半星期了,我终于设法让这一切都在某种程度上发挥了作用。我将暂时等待标记答案,看看是否有其他人有更有效、更好的“最佳实践”方法。

通过生成 JSON 字符串,我将 JavaScript 与 GridView 分离。 JSON 是在填充数据时在代码中生成的:

    protected static string ConstructReportJSON(ref DataTable dtResults)
    {
        StringBuilder sb = new StringBuilder();
        for (int r = 0; r < dtResults.Rows.Count; r++)
        {
            sb.Append("{");
            for (int c = 0; c < dtResults.Columns.Count; c++)
            {
                sb.AppendFormat("\"{0}\":\"{1}\",", dtResults.Columns[c].ColumnName, dtResults.Rows[r][c].ToString());
            }
            sb.Remove(sb.Length - 1, 1); //Truncate the trailing comma
            sb.Append("},");
        }
        sb.Remove(sb.Length - 1, 1);
        return String.Format("[{0}]", sb.ToString());
    }

返回一串数据如

[ {"Caller":"John Doe", "Office":"5555","Type":"Incoming", etc},

{"Caller":"Jane Doe", "Office":"7777", "Type":"Outgoing", etc}, {etc} ]

我通过在 UpdatePanel 中将文本分配给 Literal 隐藏了这个字符串:

    <div id="div_JSON" style="display: none;">
            <asp:Literal id="lit_JSON" runat="server" /> 
    </div>

JavaScript 通过读取 div 的内容来解析输出:

function exportToExcel_Pivot(sMyJSON, sTitleOfReport, sReportPop) {
     //sMyJSON = the name, supplied as a text, of the hidden element that houses the JSON array.
     //sTitleOfReport = Will be used as the page header if the spreadsheet is printed.
     //sReportPop = Determines which business logic to create a pivot table for.

var sJSON = document.getElementById(sMyJSON).innerHTML;
var oJSON = eval("(" + sJSON + ")");

 //    DEBUG Example Test Code
 //    for (x = 0; x < oJSON.length; x++) {
 //        for (y in oJSON[x])
 //            alert(oJSON[x][y]); //DEBUG, returns field value
 //            alert(y); //DEBUG, returns column name
 //    }


//If no data is in the JSON object array, display alert.
if (oJSON == null)
    alert('No data for report');
else {
    var oExcel = new ActiveXObject("Excel.Application");
    var oBook = oExcel.Workbooks.Add;
    var oSheet = oBook.Worksheets(1);
    var oSheet2 = oBook.Worksheets(2);
    var iRow = 0;
    var iCol = 0;

        //Take the column names of the JSON object and prepare them in Excel
        for (header in oJSON[0])
        {
            oSheet.Cells(iRow + 1, iCol + 1) = header;
            iCol++;
        }

        iRow++;

        //Export all rows of the JSON object to excel
        for (var r = 0; r < oJSON.length; r++)
        {
            iCol = 0;
            for (c in oJSON[r]) 
                    {
                        oSheet.Cells(iRow + 1, iCol + 1) = oJSON[r][c];
                        iCol++;
                    } //End column loop
            iRow++;
        } //End row

字符串输出和 JavaScript 'eval' 解析都运行得非常快,但循环遍历 JSON 对象比我想要的要慢一些。

我相信这种方法将被限制在大约 10 亿个字符的数据中——可能更少,这取决于内存测试的结果。 (我计算过,我可能每天最多查看 100 万个字符,所以在报告后的一年内应该没问题。)

【讨论】:

    【解决方案4】:

    编写 CSV 文件既简单又高效。但是,如果您需要 Excel,也可以以相当有效的方式完成,通过使用 Microsoft Open XML SDK 的开放式 XML 编写器可以处理 60,000 多行。

    1. 如果您还没有 Microsoft Open SDK,请安装它(谷歌“下载 microsoft open xml sdk”)
    2. 创建控制台应用程序
    3. 添加对 DocumentFormat.OpenXml 的引用
    4. 添加对 WindowsBase 的引用
    5. 尝试运行一些测试代码,如下所示(需要一些使用)

    只需在http://polymathprogrammer.com/2012/08/06/how-to-properly-use-openxmlwriter-to-write-large-excel-files/ 上查看 Vincent Tan 的解决方案(下面,我稍微清理了他的示例以帮助新用户。)

    在我自己的使用中,我发现常规数据非常简单,但我确实必须从我的真实数据中去除“\0”字符。

    using DocumentFormat.OpenXml;
    using DocumentFormat.OpenXml.Packaging;
    using DocumentFormat.OpenXml.Spreadsheet;
    

    ...

            using (var workbook = SpreadsheetDocument.Create("SomeLargeFile.xlsx", SpreadsheetDocumentType.Workbook))
            {
                List<OpenXmlAttribute> attributeList;
                OpenXmlWriter writer;
    
                workbook.AddWorkbookPart();
                WorksheetPart workSheetPart = workbook.WorkbookPart.AddNewPart<WorksheetPart>();
    
                writer = OpenXmlWriter.Create(workSheetPart);
                writer.WriteStartElement(new Worksheet());
                writer.WriteStartElement(new SheetData());
    
                for (int i = 1; i <= 50000; ++i)
                {
                    attributeList = new List<OpenXmlAttribute>();
                    // this is the row index
                    attributeList.Add(new OpenXmlAttribute("r", null, i.ToString()));
    
                    writer.WriteStartElement(new Row(), attributeList);
    
                    for (int j = 1; j <= 100; ++j)
                    {
                        attributeList = new List<OpenXmlAttribute>();
                        // this is the data type ("t"), with CellValues.String ("str")
                        attributeList.Add(new OpenXmlAttribute("t", null, "str"));
    
                        // it's suggested you also have the cell reference, but
                        // you'll have to calculate the correct cell reference yourself.
                        // Here's an example:
                        //attributeList.Add(new OpenXmlAttribute("r", null, "A1"));
    
                        writer.WriteStartElement(new Cell(), attributeList);
    
                        writer.WriteElement(new CellValue(string.Format("R{0}C{1}", i, j)));
    
                        // this is for Cell
                        writer.WriteEndElement();
                    }
    
                    // this is for Row
                    writer.WriteEndElement();
                }
    
                // this is for SheetData
                writer.WriteEndElement();
                // this is for Worksheet
                writer.WriteEndElement();
                writer.Close();
    
                writer = OpenXmlWriter.Create(workbook.WorkbookPart);
                writer.WriteStartElement(new Workbook());
                writer.WriteStartElement(new Sheets());
    
                // you can use object initialisers like this only when the properties
                // are actual properties. SDK classes sometimes have property-like properties
                // but are actually classes. For example, the Cell class has the CellValue
                // "property" but is actually a child class internally.
                // If the properties correspond to actual XML attributes, then you're fine.
                writer.WriteElement(new Sheet()
                {
                    Name = "Sheet1",
                    SheetId = 1,
                    Id = workbook.WorkbookPart.GetIdOfPart(workSheetPart)
                });
    
                writer.WriteEndElement(); // Write end for WorkSheet Element
                writer.WriteEndElement(); // Write end for WorkBook Element
                writer.Close();
    
                workbook.Close();
            }
    

    如果您查看该代码,您会注意到两个主要写入,首先是工作表,然后是包含工作表的工作簿。工作簿部分是最后无聊的部分,前面的工作表部分包含所有的行和列。

    在您自己的适应中,您可以从您自己的数据中将真实的字符串值写入单元格。相反,在上面,我们只是使用行和列编号。

    writer.WriteElement(new CellValue("SomeValue"));
    

    值得注意的是,Excel 中的行编号从 1 而不是 0 开始。从零索引开始编号的行将导致“文件损坏”错误消息。

    最后,如果您正在处理非常大的数据集,永远不要调用 ToList()。使用数据阅读器风格的数据流方法。例如,您可以有一个 IQueryable 并在 for each 中使用它。您永远不会真的希望将所有数据同时保存在内存中,否则您会遇到内存不足限制和/或高内存利用率。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-06-30
      • 1970-01-01
      • 2019-07-02
      • 2011-01-30
      • 2012-06-14
      • 2023-03-23
      • 1970-01-01
      相关资源
      最近更新 更多