Обработка строки CSV

Типичный способ создания строки CSV (псевдокода):

  1. Создайте объект-контейнер CSV (например, StringBuilder в C#).
  2. Прокрутите строки, которые вы хотите добавить, добавляя запятую после каждой.
  3. После цикла удалите последнюю лишнюю запятую.

Пример кода:

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    foreach (Contact c in contactList)
    {
        sb.Append(c.Name + ",");
    }

    sb.Remove(sb.Length - 1, 1);
    //sb.Replace(",", "", sb.Length - 1, 1)

    return sb.ToString();
}

Мне нравится идея добавления запятой, проверяя, пуст ли контейнер, но разве это не означает дополнительной обработки, поскольку необходимо проверять длину строки в каждом случае?

Я считаю, что должен быть более простой / более чистый / эффективный способ удалить последнюю запятую. Любые идеи?

Ответов (13)

Решение

Вы можете использовать LINQ to Objects :

string [] strings = contactList.Select(c => c.Name).ToArray();
string csv = string.Join(",", strings);

Очевидно, что все это можно сделать в одной строке, но немного яснее в двух.

Ваш код не совсем совместим с полным форматом CSV . Если вы просто генерируете CSV из данных без запятых, начальных / конечных пробелов, табуляций, новых строк или кавычек, все должно быть в порядке. Однако в большинстве реальных сценариев обмена данными вам действительно нужна полная реализация.

Для создания правильного CSV вы можете использовать это:

public static String EncodeCsvLine(params String[] fields)
{
    StringBuilder line = new StringBuilder();

    for (int i = 0; i < fields.Length; i++)
    {
        if (i > 0)
        {
            line.Append(DelimiterChar);
        }

        String csvField = EncodeCsvField(fields[i]);
        line.Append(csvField);
    }

    return line.ToString();
}

static String EncodeCsvField(String field)
{
    StringBuilder sb = new StringBuilder();
    sb.Append(field);

    // Some fields with special characters must be embedded in double quotes
    bool embedInQuotes = false;

    // Embed in quotes to preserve leading/tralining whitespace
    if (sb.Length > 0 && 
        (sb[0] == ' ' || 
         sb[0] == '\t' ||
         sb[sb.Length-1] == ' ' || 
         sb[sb.Length-1] == '\t' ))
    {
        embedInQuotes = true;
    }

    for (int i = 0; i < sb.Length; i++)
    {
        // Embed in quotes to preserve: commas, line-breaks etc.
        if (sb[i] == DelimiterChar || 
            sb[i]=='\r' || 
            sb[i]=='\n' || 
            sb[i] == '"') 
        { 
            embedInQuotes = true;
            break;
        }
    }

    // If the field itself has quotes, they must each be represented 
    // by a pair of consecutive quotes.
    sb.Replace("\"", "\"\"");

    String rv = sb.ToString();

    if (embedInQuotes)
    {
        rv = "\"" + rv + "\"";
    }

    return rv;
}

Возможно, это не самый эффективный код в мире, но он был протестирован. Реальный мир отстой по сравнению с быстрым образцом кода :)

Я написал для этого небольшой класс на тот случай, если кто-то сочтет это полезным ...

public class clsCSVBuilder
{
    protected int _CurrentIndex = -1;
    protected List<string> _Headers = new List<string>();
    protected List<List<string>> _Records = new List<List<string>>();
    protected const string SEPERATOR = ",";

    public clsCSVBuilder() { }

    public void CreateRow()
    {
        _Records.Add(new List<string>());
        _CurrentIndex++;
    }

    protected string _EscapeString(string str)
    {
        return string.Format("\"{0}\"", str.Replace("\"", "\"\"")
                                            .Replace("\r\n", " ")
                                            .Replace("\n", " ")
                                            .Replace("\r", " "));
    }

    protected void _AddRawString(string item)
    {
        _Records[_CurrentIndex].Add(item);
    }

    public void AddHeader(string name)
    {
        _Headers.Add(_EscapeString(name));
    }

    public void AddRowItem(string item)
    {
        _AddRawString(_EscapeString(item));
    }

    public void AddRowItem(int item)
    {
        _AddRawString(item.ToString());
    }

    public void AddRowItem(double item)
    {
        _AddRawString(item.ToString());
    }

    public void AddRowItem(DateTime date)
    {
        AddRowItem(date.ToShortDateString());
    }

    public static string GenerateTempCSVPath()
    {
        return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().ToLower().Replace("-", "") + ".csv");
    }

    protected string _GenerateCSV()
    {
        StringBuilder sb = new StringBuilder();

        if (_Headers.Count > 0)
        {
            sb.AppendLine(string.Join(SEPERATOR, _Headers.ToArray()));
        }

        foreach (List<string> row in _Records)
        {
            sb.AppendLine(string.Join(SEPERATOR, row.ToArray()));
        }

        return sb.ToString();
    }

    public void SaveAs(string path)
    {
        using (StreamWriter sw = new StreamWriter(path))
        {
            sw.Write(_GenerateCSV());
        }
    }
}

Я использую CSVHelper - это отличная библиотека с открытым исходным кодом, которая позволяет вам генерировать совместимые потоки CSV по одному элементу за раз или настраивать ваши классы:

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    using (StringWriter stringWriter = new StringWriter(sb))
    {
        using (var csvWriter = new CsvHelper.CsvWriter(stringWriter))
        {
            csvWriter.Configuration.HasHeaderRecord = false;
            foreach (Contact c in contactList)
            {
                csvWriter.WriteField(c.Name);
            }
        }
    }
    return sb.ToString();
}

или если вы сопоставите то что-то вроде этого: csvWriter.WriteRecords<ContactList>(contactList);

Вместо этого вы можете в первую очередь добавить запятую в свой foreach.

if (sb.Length > 0) sb.Append(",");

Мне нравится идея добавления запятой, проверяя, пуст ли контейнер, но разве это не означает дополнительной обработки, поскольку необходимо проверять длину строки в каждом случае?

Вы преждевременно оптимизируете, снижение производительности будет незначительным.

Как насчет обрезки?

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();

    foreach (Contact c in contactList)
    {
        sb.Append(c.Name + ",");
    }

    return sb.ToString().Trim(',');
}

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

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    bool isFirst = true;

    foreach (Contact c in contactList) {
        if (!isFirst) { 
          // Only add comma before item if it is not the first item
          sb.Append(","); 
        } else {
          isFirst = false;
        }

        sb.Append(c.Name);
    }

    return sb.ToString();
}

Не забывайте нашего старого друга "за". Он не так красив, как foreach, но имеет то преимущество, что он может начинаться со второго элемента.

public string ReturnAsCSV(ContactList contactList)
{
    if (contactList == null || contactList.Count == 0)
        return string.Empty;

    StringBuilder sb = new StringBuilder(contactList[0].Name);

    for (int i = 1; i < contactList.Count; i++)
    {
        sb.Append(",");
        sb.Append(contactList[i].Name);
    }

    return sb.ToString();
}

Вы также можете заключить второе Append в «if», которое проверяет, содержит ли свойство Name двойные кавычки или запятую, и если да, экранировать их соответствующим образом.

Вы также можете создать массив данных c.Name и использовать метод String.Join для создания своей строки.

public string ReturnAsCSV(ContactList contactList)
{
    List<String> tmpList = new List<string>();

    foreach (Contact c in contactList)
    {
        tmpList.Add(c.Name);
    }

    return String.Join(",", tmpList.ToArray());
}

Это может быть не так производительно, как подход StringBuilder , но определенно выглядит чище.

Кроме того, вы можете рассмотреть возможность использования .CurrentCulture.TextInfo.ListSeparator вместо жестко запрограммированной запятой - если ваш вывод будет импортирован в другие приложения, у вас могут возникнуть проблемы с ним. ListSeparator может отличаться в разных культурах, и MS Excel, по крайней мере, учитывает этот параметр. Так:

return String.Join(
    System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator,
    tmpList.ToArray());

Просто подумайте, но не забывайте обрабатывать запятые и кавычки (") в значениях полей, иначе ваш CSV-файл может сломать читатель потребителей.

Я использовал этот метод раньше. Свойство Length StringBuilder НЕ предназначено только для чтения, поэтому вычитание его одним способом обрезает последний символ. Но вы должны убедиться, что ваша длина не равна нулю для начала (что произойдет, если ваш список пуст), потому что установка длины меньше нуля является ошибкой.

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();

    foreach (Contact c in contactList)       
    { 
        sb.Append(c.Name + ",");       
    }

    if (sb.Length > 0)  
        sb.Length -= 1;

    return sb.ToString();  
}

Почему бы не использовать одну из имеющихся CSV-библиотек с открытым исходным кодом?

Я знаю, что это звучит излишне для чего-то, что кажется таким простым, но, как вы можете понять по комментариям и фрагментам кода, это больше, чем кажется на первый взгляд. Помимо обработки полного соответствия CSV, вы в конечном итоге захотите обрабатывать как чтение, так и запись CSV ... и вы можете захотеть манипулировать файлами.

Раньше я использовал Open CSV в одном из своих проектов (но есть и другие варианты). Это определенно облегчило мне жизнь. ;)