将大数据查询(60k +行)导出到Excel

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

不幸的是我没想到,如果返回几天以上,GridView的大小会导致浏览器的超载问题。 该应用程序每天有几千条logging,比方说每个月有6万条logging,理想情况下,我希望能够将所有结果返回到一年。 行数造成浏览器挂起或崩溃。

我们使用SQL Server的Visual Studio 2010上的ASP.NET 3.5,预期的浏览器是IE8。 该报告由一个gridview组成,该gridview根据用户select的群体从less数存储过程中获取数据。 GridView在UpdatePanel中:

<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> 

我对我的团队相对来说比较陌生,所以我遵循了他们将sproc返回给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函数没有什么特别之处,只是将列表转换为数据表。

有几千条logging(最多几天),这工作正常。 GridView显示结果,并且有一个button让用户点击启动JScript导出器。 外部JavaScript函数将每行读入Excel工作表,然后使用它创build数据透视表。 数据透视表很重要!

 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 (ie 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++; } } 

我想要做的是:创build一个解决scheme(可能是客户端),可以处理这些数据并将其处理到Excel中。 有人可能会build议使用HtmlTextWriter ,但afaik不允许自动生成数据透视表,并创build一个令人讨厌的popup警告….

我试过了:

  • 填充JSON对象 – 我仍然认为这有潜力,但我还没有find一种方法使其工作。
  • 使用SQLDataSource – 我似乎无法使用它来获取任何数据。
  • 对页面进行分页和循环 – 混合进度。 一般来说很难看,我仍然有问题,整个数据集被查询并返回每个页面显示。

更新:我仍然非常乐意替代解决scheme,但是我一直在追求JSON理论。 我有一个工作的服务器端方法,从数据表生成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函数的例子吗? 或者任何其他解决scheme导出到Excel。

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

我们通常通过一个“导出”命令button来处理这个问题,该button连接到服务器端的方法来获取数据集并将其转换为CSV。 然后我们调整响应标题,浏览器将它视为下载。 我知道这是一个服务器端解决scheme,但你可能要考虑它,因为你将继续有超时和浏览器问题,直到你实现服务器端logging分页。

自从我开始这个问题以来,差不多一个半星期,我终于设法在一定程度上实现了这一切。 我会暂时等待回答,看看其他人是否有更高效,更好的“最佳实践”方法。

通过生成一个JSONstring,我已经离开了GridView的JavaScript。 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”:“传入”等),

{“Caller”:“Jane Doe”,“Office”:“7777”,“Type”:“发送”等等},{etc}]

我已经隐藏了这个string,通过使用下面的代码将文本分配给UpdatePanel中的Literal:

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

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

 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 

string输出和JavaScript的“eval”parsing都工作得非常快,但是循环遍历JSON对象比我想要的要慢一些。

我相信这种方法将被限制在大约10亿个字符的数据 – 可能更less取决于如何进行内存testing。 (我计算过,我可能每天最多会看一百万字,所以一年之内应该没问题。)

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

  1. 如果您尚未安装Microsoft Open SDK,请安装(“google microsoft open xml sdk”)
  2. 创build控制台应用程序
  3. 将引用添加到DocumentFormat.OpenXml
  4. 将引用添加到WindowsBase
  5. 尝试运行下面的一些testing代码(将需要一些使用的)

只要看看文森特谭的解决scheme在http://polymathprogrammer.com/2012/08/06/how-to-properly-use-openxmlwriter-to-write-large-excel-files/ (下面,我清理他的例子略帮助新用户。)

在我自己的使用中,我发现这个数据非常简单,但是我必须从我的真实数据中去除“\ 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(); } 

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

在你自己的改编中,你可以从你自己的数据中写入真正的string值到单元格中。 相反,在上面,我们只是使用行和列编号。

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

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

最后,如果您正在处理大量数据,请不要调用ToList() 。 使用stream式传输数据数据读取器风格的方法。 例如,你可以有一个IQueryable ,并在每个中使用它。 你永远不会想要同时拥有内存中的所有数据,否则你会遇到内存不足和/或内存使用率高的问题。