Повышение производительности при работе с XLS - PullRequest
2 голосов
/ 12 октября 2010

У меня есть простые методы для экспорта 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 ]

2 голосов
/ 20 октября 2010

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

StringBuilder builder = new StringBuilder(100000);

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

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

Оптимальным решением может быть периодическая отправка вывода stringbuilder в поток по мере его увеличения до некоторого размера (в зависимости от памяти вашей системы), если он может превышать, скажем, 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:

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

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

1 голос
/ 20 октября 2010

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

   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());
0 голосов
/ 21 октября 2010

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

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

В других публикациях на SS говорится, что они думают, что быстрее использовать XmlTextWriter, чем StringBuilder.

StringBuilder против XmlTextWriter .

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

0 голосов
/ 12 октября 2010

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

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

Изменить после того, как вы добавили свои данные:

Вместо того, чтобы ConvertToXLS возвращал строку, передайтеэтот streamwriter для вашего метода convertToXLS.

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

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

stream.WriteLine(x); 

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

...