将大型数据查询(60k+ 行)导出到 Excel
Posted
技术标签:
【中文标题】将大型数据查询(60k+ 行)导出到 Excel【英文标题】:Export a large data query (60k+ rows) to 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。 【参考方案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 多行。
-
如果您还没有 Microsoft Open SDK,请安装它(谷歌“下载 microsoft open xml sdk”)
创建控制台应用程序
添加对 DocumentFormat.OpenXml 的引用
添加对 WindowsBase 的引用
尝试运行一些测试代码,如下所示(需要一些使用)
只需在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("R0C1", 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 中使用它。您永远不会真的希望将所有数据同时保存在内存中,否则您会遇到内存不足限制和/或高内存利用率。
【讨论】:
以上是关于将大型数据查询(60k+ 行)导出到 Excel的主要内容,如果未能解决你的问题,请参考以下文章