2

我有使用字符串将 DataTable 导出到 XLS 的简单方法。列数为 5 - 30,数量或行数可能为 1 到 1000。有时性能会出现问题,请咨询我可以在代码中更改哪些内容。我正在使用 .net 4.0

public string FormatCell(string columnName, object value)
        {
        StringBuilder builder = new StringBuilder();
        string formattedValue = string.Empty;
        string type = "String";
        string style = "s21";

        if (!(value is DBNull) && columnName.Contains("GIS"))
            formattedValue = Convert.ToDouble(value).ToString("##.00000000°");
        else if (value is DateTime)
        {
            style = "s22";
            type = "DateTime";
            DateTime date = (DateTime)value;
            formattedValue = date.ToString("yyyy-MM-ddTHH:mm:ss.fff");
        }
        else if (value is double || value is float || value is decimal)
        {
            formattedValue = Convert.ToDecimal(value).ToString("#.00").Replace(',', '.');
            type = "Number";
        }
        else if (value is int)
        {
            formattedValue = value.ToString();
            type = "Number";
        }
        else
            formattedValue = value.ToString();

        builder.Append(string.Format("<Cell ss:StyleID=\"{0}\"><Data ss:Type=\"{1}\">", style, type));

        builder.Append(formattedValue);
        builder.AppendLine("</Data></Cell>");

        return builder.ToString();
    }

    public string ConvertToXls(DataTable table)
    {
        StringBuilder builder = new StringBuilder();

        int rows = table.Rows.Count + 1;
        int cols = table.Columns.Count;

        builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        builder.AppendLine("<?mso-application progid=\"Excel.Sheet\"?>");
        builder.AppendLine("<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"");
        builder.AppendLine(" xmlns:o=\"urn:schemas-microsoft-com:office:office\"");
        builder.AppendLine(" xmlns:x=\"urn:schemas-microsoft-com:office:excel\"");
        builder.AppendLine(" xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\"");
        builder.AppendLine(" xmlns:html=\"http://www.w3.org/TR/REC-html40/\">");
        builder.AppendLine(" <DocumentProperties xmlns=\"urn:schemas-microsoft-com:office:office\">;");
        builder.AppendLine("  <Author>Author</Author>");
        builder.AppendLine(string.Format("  <Created>{0}T{1}Z</Created>", DateTime.Now.ToString("yyyy-mm-dd"), DateTime.Now.ToString("HH:MM:SS")));
        builder.AppendLine("  <Company>Company</Company>");
        builder.AppendLine("  <Version>1.0</Version>");
        builder.AppendLine(" </DocumentProperties>");
        builder.AppendLine(" <ExcelWorkbook xmlns=\"urn:schemas-microsoft-com:office:excel\">");
        builder.AppendLine("  <WindowHeight>8955</WindowHeight>");
        builder.AppendLine("  <WindowWidth>11355</WindowWidth>");
        builder.AppendLine("  <WindowTopX>480</WindowTopX>");
        builder.AppendLine("  <WindowTopY>15</WindowTopY>");
        builder.AppendLine("  <ProtectStructure>False</ProtectStructure>");
        builder.AppendLine("  <ProtectWindows>False</ProtectWindows>");
        builder.AppendLine(" </ExcelWorkbook>");
        builder.AppendLine(" <Styles>");
        builder.AppendLine("  <Style ss:ID=\"Default\" ss:Name=\"Normal\">");
        builder.AppendLine("   <Alignment ss:Vertical=\"Bottom\"/>");
        builder.AppendLine("   <Borders/>");
        builder.AppendLine("   <Font/>");
        builder.AppendLine("   <Interior/>");
        builder.AppendLine("   <Protection/>");
        builder.AppendLine("  </Style>");
        builder.AppendLine("  <Style ss:ID=\"s21\">");
        builder.AppendLine("   <Alignment ss:Vertical=\"Bottom\" ss:WrapText=\"1\"/>");
        builder.AppendLine("  </Style>");
        builder.AppendLine("  <Style ss:ID=\"s22\">");
        builder.AppendLine("    <NumberFormat ss:Format=\"Short Date\"/>");
        builder.AppendLine("  </Style>");
        builder.AppendLine(" </Styles>");
        builder.AppendLine(" <Worksheet ss:Name=\"Export\">");
        builder.AppendLine(string.Format("  <Table ss:ExpandedColumnCount=\"{0}\" ss:ExpandedRowCount=\"{1}\" x:FullColumns=\"1\"", cols.ToString(), rows.ToString()));
        builder.AppendLine("   x:FullRows=\"1\">");

        //generate title
        builder.AppendLine("<Row>");
        foreach (DataColumn eachColumn in table.Columns)  // you can write a half columns of table and put the remaining columns in sheet2
        {
            if (eachColumn.ColumnName != "ID")
            {
                builder.Append("<Cell ss:StyleID=\"s21\"><Data ss:Type=\"String\">");
                builder.Append(eachColumn.ColumnName.ToString());
                builder.AppendLine("</Data></Cell>");
            }
        }
        builder.AppendLine("</Row>");

        //generate data
        foreach (DataRow eachRow in table.Rows)
        {
            builder.AppendLine("<Row>");
            foreach (DataColumn eachColumn in table.Columns)
            {
                if (eachColumn.ColumnName != "ID")
                {
                    builder.AppendLine(FormatCell(eachColumn.ColumnName, eachRow[eachColumn]));
                }
            }
            builder.AppendLine("</Row>");
        }
        builder.AppendLine("  </Table>");
        builder.AppendLine("  <WorksheetOptions xmlns=\"urn:schemas-microsoft-com:office:excel\">");
        builder.AppendLine("   <Selected/>");
        builder.AppendLine("   <Panes>");
        builder.AppendLine("    <Pane>");
        builder.AppendLine("     <Number>3</Number>");
        builder.AppendLine("     <ActiveRow>1</ActiveRow>");
        builder.AppendLine("    </Pane>");
        builder.AppendLine("   </Panes>");
        builder.AppendLine("   <ProtectObjects>False</ProtectObjects>");
        builder.AppendLine("   <ProtectScenarios>False</ProtectScenarios>");
        builder.AppendLine("  </WorksheetOptions>");
        builder.AppendLine(" </Worksheet>");
        builder.AppendLine(" <Worksheet ss:Name=\"Sheet2\">");
        builder.AppendLine("  <WorksheetOptions xmlns=\"urn:schemas-microsoft-com:office:excel\">");
        builder.AppendLine("   <ProtectObjects>False</ProtectObjects>");
        builder.AppendLine("   <ProtectScenarios>False</ProtectScenarios>");
        builder.AppendLine("  </WorksheetOptions>");
        builder.AppendLine(" </Worksheet>");
        builder.AppendLine(" <Worksheet ss:Name=\"Sheet3\">");
        builder.AppendLine("  <WorksheetOptions xmlns=\"urn:schemas-microsoft-com:office:excel\">");
        builder.AppendLine("   <ProtectObjects>False</ProtectObjects>");
        builder.AppendLine("   <ProtectScenarios>False</ProtectScenarios>");
        builder.AppendLine("  </WorksheetOptions>");
        builder.AppendLine(" </Worksheet>");
        builder.AppendLine("</Workbook>");

        return builder.ToString();
    }

使用这个:

string xlsData= ConvertToXls(someTable)


System.CodeDom.Compiler.TempFileCollection fileCollection = new System.CodeDom.Compiler.TempFileCollection();

                    string tempFileName = fileCollection.AddExtension("xls", true);

                    if (File.Exists(tempFileName))
                        File.Delete(tempFileName);

                    using (StreamWriter writer = new StreamWriter(tempFileName, false, Encoding.UTF8))
                        writer.Write(xlsData);
4

4 回答 4

2

您可以做的最简单的事情是使用默认值以外的容量声明 StringBuilder,例如

StringBuilder builder = new StringBuilder(100000);

默认分配为 16 字节,每次需要重新分配时都会翻倍。这意味着如果您使用默认值,它将被多次重新分配。

除非您的系统内存很紧,或者这真的非常非常大,否则我怀疑像之前建议的那样直接流式传输它会产生很大的不同。我怀疑它实际上可能会使事情变得更糟,因为我怀疑文件流写入与将数据添加到已经分配的 StreamBuilder 对象相比开销更少(假设它不需要经常重新分配!)

最佳解决方案可能是当它增长到某个大小(基于系统的内存)时,如果它可能超过 10 或 20 兆字节,则定期将字符串构建器输出发送到流。这样,您将避免内存问题,并避免与对输出流的许多小写入相关的任何潜在开销。

更新 - 测试说明:

我运行了一些测试来创建非常大的字符串(> 50 兆字节),并且提前分配内存几乎没有明显差异。

但更重要的是,使用最简单的形式创建这样一个字符串所需的时间:

  for (int i = 0; i < 10000000; i++)
  {
     builder.AppendLine("a whole bunch of text designed to see how long it takes to build huge strings ");
  }

几乎无关紧要。我可以在几秒钟内填满我所有台式电脑的内存。

这意味着 StringBuilder 的开销根本不是您的问题。也可以由此推断,切换到流式写入也绝对不会帮助您。

相反,您需要查看您正在执行数千或数万次的某些操作。这个循环::

foreach (DataRow eachRow in table.Rows)
        {
            builder.AppendLine("<Row>");
            foreach (DataColumn eachColumn in table.Columns)
            {
                if (eachColumn.ColumnName != "ID")
                {
                    builder.AppendLine(FormatCell(eachColumn.ColumnName, eachRow[eachColumn]));
                }
            }
            builder.AppendLine("</Row>");
        }
  • 通过从您的选择中删除来消除对 ColumnName!="ID" 的检查
  • FormatCell 为每个数据元素运行一次。对此效率的微小改变可能会产生巨大影响
  • 之前没有考虑过这个,但是如果你的 DataTable 来自 SQL 数据源,直接使用 DataReader 而不是内存中的 DataTable

FormatCell 改进建议:

  • 提前为每列的数据类型建立索引,这样您就不必每次都进行昂贵的类型比较
  • 为 Type 和 Style 设置字符串值并根据数据类型更改它们的成本很高。改为使用枚举,然后使用基于枚举值的硬编码字符串输出值。
  • 将 FormatCell 中的任何变量移动到主类,这样就不需要在每次调用过程时都创建/分配它们

要构建索引,我认为最有效的方法是将列号映射到定义每列类型的数组,如下面的代码,然后在 FormatCell 中使用预先构建的列号映射到数据类型。

enum DataTypes
    {
        DateTime = 1,
        Float = 2,
        Int = 3,
        String = 4
    }
    DataTypes[] types = new DataTypes[tbl.Columns.Count];
    for (int col=0;i<tbl.Columns.Count;col++) {
        object value = tbl.Rows[0][col];
        if (value is double || value is float || value is decimal) {
            types[col]=DataTypes.Float;
        } else if (value is DateTime) {
            types[col]=DataTypes.DateTime;
        } else if (value is int) {
            types[col]=DataTypes.Int;
        } else {
            types[col]=DataTypes.String;
        }
    }

然后将列号传递给 FormatCell,它可以从数组中查找数据类型,只需使用开关进行检查:

switch(types[colNumber]) {
   case DataTypes.DateTime:
       ...
       break;
   case DataTypes.Int:
...
 /// and so on
}

我认为这会减少很多开销。

于 2010-10-20T17:26:43.643 回答
1

您应该使用 dotTrace 之类的东西来分析您的代码,以查看时间的去向。至少设置计时器以查看每个部分需要多长时间。在不知道瓶颈在哪里的情况下进行优化可能是浪费时间。例如:

   DateTime startTime = DateTime.Now;
   Debug.WriteLine("Start : " + startTime);

   //some code

   Debug.WriteLine("End: " + DateTime.Now);
   Debug.WriteLine("Elapsed : " + (DateTime.Now - startTime));

我认为上面的约翰是正确的。使用流。例如。

StreamWriter streamWriter = System.IO.File.CreateText("c:\\mynewfile.xls");

streamWriter.AutoFlush = false;

//lots of writes

streamWriter.Flush();
streamWriter.Close();

您应该使用 autoflush false 和 true 进行测试。您可能还想尝试内存流。

StreamWriter streamWriter = new StreamWriter(new MemoryStream());
于 2010-10-19T23:39:59.027 回答
0

不要将这些行写出两次,一次在内存中,然后写入磁盘,而是尝试将其归结为一次写操作。直接上磁盘。

我不知道 .net 中的 xml 对象和 stringbuilder 之间的性能比较是什么样的,但如果我知道我正在写出 Xml,我会倾向于使用 xml 对象解决方案、xmlwriter xlinq 等。知道这一点的安慰您每次生成的数据都准时符合 xml,这非常令人放心。

SS 上的其他帖子表示,他们认为使用 XmlTextWriter 比使用 StringBuilder 更快。

StringBuilder 与 XmlTextWriter

关于更改缓冲区大小和延迟写入的答案将起作用,但可能会非常失败,是的,如果您在内存中完成所有操作,您的操作会变得更快,但是您的内存占用可能会变得非常大,因此操作系统可能会做一些影响整个机器的磁盘交换。(取决于您在机器上运行的内容)。找到满意的折衷方案,然后以生产系统满意的写入速度流式传输数据。

于 2010-10-21T10:53:03.823 回答
0

好吧,你只是在内存中创建一个越来越大的字符串......所以随着大小的增加,它会变得越来越糟。

您是否有任何理由不将其流式传输到文件中,而不是构建一个 GIANT 字符串,然后将其序列化到文件中?

添加详细信息后进行编辑:

不要让 ConvertToXLS 返回一个字符串,而是将该 streamwriter 传递给您的 convertToXLS 方法。

public void ConvertToXLS( DataTable table, StreamWriter stream )
{
    ...
}

在 ConverToXLS 内部,去掉那个 StringBuilder,并替换所有对builder.AppendLine( x )to的调用

stream.WriteLine(x); 

这样一来,您就可以写入流而不是创建一个巨大的字符串。

于 2010-10-11T21:12:37.393 回答