2010-10-11 58 views
2

我有簡單的方法使用字符串將DataTable導出到XLS。列數是5 - 30,數量或行數可能是1到1000.有時候性能有問題,我請教我的建議,我可以在我的代碼中更改哪些內容。我使用.NET 4.0使用XLS操作提高性能

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); 
+0

我在下面更新了我的答案。 StringBuilder不是你的問題,我提出了一些建議來簡化FormatCell,這幾乎可以肯定所有的處理時間。 – 2010-10-21 15:44:03

回答

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獲取每一個數據元素運行一次。以這種效率的微小變化會產生巨大的影響
  • 之前沒有想到這一點,但如果你的數據表是從SQL數據源的到來,使用DataReader,而不是直接在內存中的數據表

建議以改善FormatCell:

  • 構建每一列的數據類型的指標提前,這樣你就不必做昂貴的類型,每次比較
  • 設置字符串值類型和樣式,並改變它們基於數據類型,價格昂貴。改爲使用枚舉,然後根據枚舉值使用硬編碼字符串輸出值。
  • 移動內部FormatCell任何變量主類,所以他們並不需要創建/分配調用程序

來建立索引每一次,我認爲最有效的方法是映射列號定義爲每個列的類型,如下面的代碼,然後在FormatCell中,只需使用預先構建的columnnumbers映射到數據類型即可。

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的columnumber,它可以從陣列中查找數據類型,只是一個開關檢查:

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

我認爲這將減少大量的開銷。

0

嗯,你只是在內存中創建一個越做越大串......,這樣會得到越來越糟糕的大小往上。

是否有任何理由不會將流式傳輸到文件中,而是建立一個GIANT字符串,然後將其序列化爲文件?您添加您的詳細信息

編輯後:

,而不必ConvertToXLS返回一個字符串,傳遞的StreamWriter到您convertToXLS方法。

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

ConverToXLS內,擺脫的StringBuilder,並更換的builder.AppendLine(x)所有來電

stream.WriteLine(x); 

這樣,你去了,你寫的流,而不是創建一個巨大的弦。

+0

流媒體?你是什​​麼意思?你能舉一些例子或鏈接嗎?我添加了如何使用這種方法的代碼。 – user278618 2010-10-11 21:19:57

+0

@ user278618:流式傳輸不是在一個大塊中發送數據,而是以小塊連續發送。就像通過管道向你的房子輸送一股水,而不是每週交付一個大水箱。 – 2010-10-11 21:59:55

1

你應該用類似dotTrace的東西來分析你的代碼,看看時間在哪裏。至少要投入計時器,以瞭解每個部件的使用時間。不知道瓶頸在哪裏進行優化可能會浪費時間。 EG:

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

    //some code 

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

我認爲上面的John是正確的。使用流。例如。

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()); 
0

而不是寫出行兩次,一次在內存中,然後寫入磁盤,嘗試將其寫入一個寫操作。直接到磁盤。

我不知道xml對象之間的性能比較是什麼樣子。net和stringbuilder,但是如果我知道我正在寫出Xml,我會傾向於使用xml對象解決方案,xmlwriter xlinq等。知道您生產的數據每次都符合xml規格的舒適度非常令人放心。

SS上的其他帖子表示,他們認爲使用XmlTextWriter比StringBuilder更快。

StringBuilder vs XmlTextWriter

有關更改緩衝區大小和延遲寫入的答案可以工作,但可能非常受打擊和遺漏,如果您在內存中完成所有操作,操作會變得更快,但是您的內存佔用空間可能會非常大,因此操作系統可能會做一些影響整個機器的磁盤交換(取決於你在機器上運行的是什麼)。找到令人滿意的妥協方案,然後以生產系統滿意的寫入速度對數據進行流式傳輸。