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

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

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

Пример кода:

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

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

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

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

Не забывайте, что наш старый друг "за". Это не так приятно, как 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 двойную кавычку или запятую, и если да, то избегайте их соответственно.

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

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

Ответ 11

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

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

Ответ 12

Извините, конкретный пример PHP, но может помочь кому-то.

Ответ 13

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

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

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

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

Ответ 14

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