Обработка строки 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();
}

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

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

7.08.2008 05:49:04
13 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

21
26.03.2010 17:29:31
Менее очевидно, что он на самом деле не реализует спецификацию CSV. Это отличный способ поместить запятые в строку, но это не то же самое, что CSV, формат.
rcreswick 22.09.2008 19:21:44

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

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

3
26.11.2011 08:25:56

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

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();
}
0
7.02.2016 14:34:02

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

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

1
7.08.2008 06:25:57

Вы также можете создать массив данных 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());
3
7.02.2016 14:33:28

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

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

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

    return sb.ToString().Trim(',');
}
0
7.02.2016 14:33:50

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

1
7.08.2008 11:18:09

Не забудь нашего старого друга "за". Это не так красиво, как 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();
}

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

5
7.02.2016 14:33:04

Ваш код не совсем совместим с полным форматом 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;
}

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

9
7.02.2016 14:32:47
Как написано в другом ответе, для этого есть библиотеки (например, OpenCSV), и у них есть тестовые рамки / модульные тесты.
rcreswick 22.09.2008 19:19:12
Эти две подпрограммы, наконец, решили проблему, которую я преследовал. Правда, немного дольше, чем просто лизать и вставлять все данные вместе с запятыми, но обработал мой экспорт 400 000 строк без проблем.
Lloyd 13.03.2013 02:04:26

Я использовал этот метод раньше. Свойство 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();  
}
1
7.02.2016 14:33:38

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

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

Я использовал Open CSV в одном из своих проектов ранее (но есть много других на выбор). Это, безусловно, сделало мою жизнь проще. ;)

5
20.08.2008 02:14:42

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

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());
        }
    }
}
1
26.06.2012 19:34:54

Я использую 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);

0
26.06.2012 20:06:39