2010-10-11 2 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, который почти наверняка будет содержать все время обработки. –

ответ

2

Простейшая вещь, которую вы можете сделать, это объявить StringBuilder с емкостью, отличной от значения по умолчанию, например.

StringBuilder builder = new StringBuilder(100000); 

Распределение по умолчанию - 16 байт и удваивается каждый раз, когда его необходимо перераспределить. Это означает, что он будет перераспределен много раз, если вы используете значение по умолчанию.

Если ваша система не напряжена в памяти, или это действительно, действительно огромный, я сомневаюсь, что потоковая передача ее напрямую, как было предложено ранее, будет иметь большое значение. Я подозреваю, что это может на самом деле существенно ухудшить ситуацию, так как я сомневаюсь, что в потоке файлового потока меньше данных по сравнению с добавлением данных в объект StreamBuilder, который уже был выделен (при условии, что его не нужно перераспределять часто!)

оптимальным решением может быть отправка потока stringbuilder в поток периодически, поскольку он растет до некоторого размера (в зависимости от памяти вашей системы), если он может быть больше, чем, скажем, 10 или 20 мегабайт. Таким образом, вы избегаете проблем с памятью, а также избегаете каких-либо потенциальных издержек, связанных со многими небольшими записями в выходной поток.

Update - Тестирование Примечание:

Я провел несколько тестов, создавая очень большие строки (> 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:

  • Построение индекса типов данных каждого столбца заранее, так что вам не придется делать дорогостоящий тип сравнения каждый раз
  • Установка строковые значения для типа и стиля, и изменяющий они основаны на типе данных, стоят дорого. Вместо этого используйте перечисления, а затем выведите значения, используя строковые строки, основанные на значении перечисления.
  • Переместить любые переменные внутри 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 в columnumber и он может искать тип данных из массива, а просто проверить с помощью переключателя:

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

Я думаю, что это было бы сократить много накладных расходов.

0

Ну, вы просто создать большую и большую строку в памяти ... так что бы все хуже и хуже, так как размер Продолжается.

Есть ли причина, по которой вы не передаете это в файл по ходу, вместо того, чтобы строить строку GIANT, а затем сериализуете это в файл?

Edit после добавления ваших данных:

Вместо того, ConvertToXLS возвращает строку, передать эту StreamWriter вашей методы convertToXLS.

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

внутри ConverToXLS, избавиться от этого StringBuilder, и заменить все вызовы builder.AppendLine(x) к

stream.WriteLine(x); 

Таким образом, как вы идете, вы пишете в поток вместо создания гигантская струна.

+0

Потоковая передача? Что ты имеешь в виду? Не могли бы вы привести пример или ссылку? Я добавил код, как я использую этот метод. – user278618

+0

@ user278618: Streaming - это когда вместо отправки данных в один большой кусок он отправляется непрерывно небольшими кусками. Подобно тому, как поток воды в ваш дом через трубу вместо еженедельной доставки большого резервуара для воды. –

1

Вы должны прокомментировать свой код с чем-то вроде dotTrace, чтобы узнать, куда идет время. По крайней мере, поставьте таймеры, чтобы узнать, сколько времени занимает каждая часть. Оптимизация, не зная, где узкое место, вероятно, будет пустой тратой времени. EG:

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

    //some code 

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

Я думаю, что Джон выше правильно. Используйте Stream. например.

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

streamWriter.AutoFlush = false; 

//lots of writes 

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

Вы должны проверить с помощью фальшивого и истинного автозапуска. Вы также можете попробовать поток памяти.

StreamWriter streamWriter = new StreamWriter(new MemoryStream()); 
0

Вместо того чтобы записывать строки дважды, один раз в памяти и затем записывать на диск, попробуйте выполнить его до одной операции записи. Прямо на диск.

Я понятия не имею, что такое сравнение производительности между объектами xml.net и stringbuilder, но если бы я знал, что я пишу Xml, я бы хотел пойти на объектные решения xml, xmlwriter xlinq и т. д. Удовлетворение зная, что данные, которые ваш продюсер является xml-совместимым во времени, каждый раз очень обнадеживают.

Другие сообщения на SS заявили, что они считают, что быстрее, используя XmlTextWriter, чем StringBuilder.

StringBuilder vs XmlTextWriter.

Ответы об изменении размера буфера и задержки записи будут работать, но могут быть очень удачными и пропущенными, да, ваши операции будут быстрее, если вы сделаете все это в памяти, но тогда ваш объем памяти может стать очень большим, ОС может выполнить некоторую замену диска, которая влияет на всю машину (в зависимости от того, что вы на вашем компьютере). Найдите счастливый компромисс, а затем передайте данные со скоростью записи, с которой ваша производственная система довольна.

Смежные вопросы