Сведения о вопросе

Mathprofi

16:03, 1st July, 2020

Теги

c#   csv    

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

Просмотров: 429   Ответов: 13

Типичный способ создания строки 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();
}

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

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



  Сведения об ответе

JUST___

18:03, 1st July, 2020

Вы можете использовать LINQ для объектов :

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

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


  Сведения об ответе

baggs

18:03, 1st July, 2020

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

Для генерации правильного 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;
}

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


  Сведения об ответе

baggs

18:03, 1st July, 2020

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

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

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


  Сведения об ответе

VERSUION

18:03, 1st July, 2020

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

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


  Сведения об ответе

PROGA

18:03, 1st July, 2020

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

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


  Сведения об ответе

crush

18:03, 1st July, 2020

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


  Сведения об ответе

PAGE

18:03, 1st July, 2020

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

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


  Сведения об ответе

VCe znayu

18:03, 1st July, 2020

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

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


  Сведения об ответе

davran

18:03, 1st July, 2020

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


  Сведения об ответе

VCe znayu

18:03, 1st July, 2020

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

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


  Сведения об ответе

JUST___

18:03, 1st July, 2020

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

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


  Сведения об ответе

COOL

18:03, 1st July, 2020

Как насчет того, чтобы немного подстричься?

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

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

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


  Сведения об ответе

dumai

18:03, 1st July, 2020

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


Ответить на вопрос

Чтобы ответить на вопрос вам нужно войти в систему или зарегистрироваться