Convertendo uma lista genérica em uma sequência CSV


139

Eu tenho uma lista de valores inteiros (Lista) e gostaria de gerar uma seqüência de valores delimitados por vírgula. Todos os itens da lista são gerados em uma única lista delimitada por vírgula.

Meus pensamentos ... 1. passar a lista para um método. 2. Use stringbuilder para iterar a lista e acrescentar vírgulas. 3. Teste o último caractere e, se for vírgula, exclua-o.

Quais são seus pensamentos? É este o melhor caminho?

Como meu código mudaria se eu quisesse lidar não apenas com números inteiros (meu plano atual), mas com strings, longs, doubles, bools, etc etc. no futuro? Eu acho que fazê-lo aceitar uma lista de qualquer tipo.

Respostas:


243

É incrível o que o Framework já faz por nós.

List<int> myValues;
string csv = String.Join(",", myValues.Select(x => x.ToString()).ToArray());

Para o caso geral:

IEnumerable<T> myList;
string csv = String.Join(",", myList.Select(x => x.ToString()).ToArray());

Como você pode ver, efetivamente não é diferente. Lembre-se de que pode ser necessário agrupar x.ToString()aspas (ou seja, "\"" + x.ToString() + "\"") caso x.ToString()contenha vírgulas.

Para uma leitura interessante sobre uma ligeira variante disso: veja Comma Quibbling no blog de Eric Lippert.

Nota: Isso foi escrito antes do .NET 4.0 ser lançado oficialmente. Agora podemos apenas dizer

IEnumerable<T> sequence;
string csv = String.Join(",", sequence);

usando a sobrecarga String.Join<T>(string, IEnumerable<T>). Este método projetará automaticamente cada elemento xpara x.ToString().


List<int>não possui método Selectna estrutura 3.5, a menos que esteja faltando alguma coisa.
ajeh

2
@ ajeh: Você provavelmente está perdendo uma usingdeclaração.
Jason

Qual importação específica?
ajeh

1
Tente System.Linq.Enumerable(e é claro que você precisará de System.Core.dllmontagem, mas presumivelmente você já a possui). Você vê, List<int> nunca tem Selectcomo método. Em vez disso, System.Linq.Enumerabledefine Selectcomo um método de extensão em IEnumerable<T>, do qual List<int>é um exemplo de. Assim, você precisa System.Linq.Enumerableem suas importações escolher esse método de extensão.
jason

Se você está lidando com valores numéricos e vírgulas são um problema (dependendo da localidade), uma alternativa é x.ToString(CultureInfo.InvariantCulture). Isso usará o período como separador decimal.
22815 heltonbiker

15

no 3.5, eu ainda era capaz de fazer isso. É muito mais simples e não precisa de lambda.

String.Join(",", myList.ToArray<string>());

ToArray()O método de List<int>não pode ser usado com o argumento type no framework 3.5, a menos que esteja faltando alguma coisa.
23414 Ajeh

Brilhante. Não há necessidade de ToArray <string>, pois o filho ToString () é usado.
Christian

11

Você pode criar um método de extensão que você pode chamar em qualquer IEnumerable:

public static string JoinStrings<T>(
    this IEnumerable<T> values, string separator)
{
    var stringValues = values.Select(item =>
        (item == null ? string.Empty : item.ToString()));
    return string.Join(separator, stringValues.ToArray());
}

Então você pode simplesmente chamar o método na lista original:

string commaSeparated = myList.JoinStrings(", ");

7

Você pode usar String.Join.

String.Join(
  ",",
  Array.ConvertAll(
     list.ToArray(),
     element => element.ToString()
  )
);

Não há necessidade de especificar parâmetros de tipo genérico na chamada para ConvertAllaqui - ambos inte stringserão inferidos.
Pavel Minaev 11/12/2009

1
Em vez de fazer Array.ConvertAll(...' you can just do list.ConvertAll (e => e.ToString ()). ToArray) `, apenas menos digitação.
David

string.Join (",", lista); vai fazer muito bem :)
Christian

6

Se alguém desejar converter a lista de objetos de classe personalizados em vez da lista de cadeias , substitua o método ToString da sua classe pela representação de linha csv da sua classe.

Public Class MyClass{
   public int Id{get;set;}
   public String PropertyA{get;set;}
   public override string ToString()
   {
     return this.Id+ "," + this.PropertyA;
   }
}

Em seguida, o código a seguir pode ser usado para converter essa lista de classes em CSV com a coluna do cabeçalho

string csvHeaderRow = String.Join(",", typeof(MyClass).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(x => x.Name).ToArray<string>()) + Environment.NewLine;
string csv= csvHeaderRow + String.Join(Environment.NewLine, MyClass.Select(x => x.ToString()).ToArray());

myExampleCollection.Select vez MyClass.Select
Piotr Ferenc

5

Como o código no link fornecido pelo @Frank Crie um arquivo CSV a partir de uma lista genérica do .NET, houve um pequeno problema ao terminar cada linha com um ,modifiquei o código para se livrar dele. Espero que ajude alguém.

/// <summary>
/// Creates the CSV from a generic list.
/// </summary>;
/// <typeparam name="T"></typeparam>;
/// <param name="list">The list.</param>;
/// <param name="csvNameWithExt">Name of CSV (w/ path) w/ file ext.</param>;
public static void CreateCSVFromGenericList<T>(List<T> list, string csvCompletePath)
{
    if (list == null || list.Count == 0) return;

    //get type from 0th member
    Type t = list[0].GetType();
    string newLine = Environment.NewLine;

    if (!Directory.Exists(Path.GetDirectoryName(csvCompletePath))) Directory.CreateDirectory(Path.GetDirectoryName(csvCompletePath));

    if (!File.Exists(csvCompletePath)) File.Create(csvCompletePath);

    using (var sw = new StreamWriter(csvCompletePath))
    {
        //make a new instance of the class name we figured out to get its props
        object o = Activator.CreateInstance(t);
        //gets all properties
        PropertyInfo[] props = o.GetType().GetProperties();

        //foreach of the properties in class above, write out properties
        //this is the header row
        sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);

        //this acts as datarow
        foreach (T item in list)
        {
            //this acts as datacolumn
            var row = string.Join(",", props.Select(d => item.GetType()
                                                            .GetProperty(d.Name)
                                                            .GetValue(item, null)
                                                            .ToString())
                                                    .ToArray());
            sw.Write(row + newLine);

        }
    }
}

Informações adicionais: O processo não pode acessar o arquivo 'c: \ temp \ matchingMainWav.csv' porque está sendo usado por outro processo. a pasta devexiste, mas não o arquivo ... não estou usando isso certo?
Tom Stickel

O método File.Create cria o arquivo e abre um FileStream no arquivo. Portanto, seu arquivo já está aberto. Você realmente não precisa o método File.Create em tudo:
David

Se alguma propriedade for nula, existe uma maneira de contornar isso?
Daniel Jackson

@DanielJackson Você pode escrever uma cláusula WHERE nesta declaração sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);não testado, mas não sei o que você está tentando alcançar
Ali Umair

4

Eu explico isso em profundidade neste post . Vou colar o código aqui com breves descrições.

Aqui está o método que cria a linha do cabeçalho. Ele usa os nomes de propriedades como nomes de colunas.

private static void CreateHeader<T>(List<T> list, StreamWriter sw)
    {
        PropertyInfo[] properties = typeof(T).GetProperties();
        for (int i = 0; i < properties.Length - 1; i++)
        {
            sw.Write(properties[i].Name + ",");
        }
        var lastProp = properties[properties.Length - 1].Name;
        sw.Write(lastProp + sw.NewLine);
    }

Este método cria todas as linhas de valor

private static void CreateRows<T>(List<T> list, StreamWriter sw)
    {
        foreach (var item in list)
        {
            PropertyInfo[] properties = typeof(T).GetProperties();
            for (int i = 0; i < properties.Length - 1; i++)
            {
                var prop = properties[i];
                sw.Write(prop.GetValue(item) + ",");
            }
            var lastProp = properties[properties.Length - 1];
            sw.Write(lastProp.GetValue(item) + sw.NewLine);
        }
    }

E aqui está o método que os reúne e cria o arquivo real.

public static void CreateCSV<T>(List<T> list, string filePath)
    {
        using (StreamWriter sw = new StreamWriter(filePath))
        {
            CreateHeader(list, sw);
            CreateRows(list, sw);
        }
    }

1
Isso funciona muito bem. Aprimorei isso para passar o delimitador como parâmetro, para que qualquer tipo de arquivo delimitado possa ser gerado. É difícil lidar com CSVs se o texto contiver vírgulas; por isso, |gero arquivos delimitados usando a versão aprimorada. Obrigado!
Shiva


3

Eu gosto de um bom método de extensão simples

 public static string ToCsv(this List<string> itemList)
         {
             return string.Join(",", itemList);
         }

Então você pode simplesmente chamar o método na lista original:

string CsvString = myList.ToCsv();

Mais limpo e fácil de ler do que algumas das outras sugestões.


2

O problema com String.Join é que você não está manipulando o caso de uma vírgula já existente no valor. Quando existe uma vírgula, você coloca o valor entre aspas e substitui todas as cotações existentes por aspas duplas.

String.Join(",",{"this value has a , in it","This one doesn't", "This one , does"});

Consulte Módulo CSV


2

A biblioteca CsvHelper é muito popular no Nuget. Você vale a pena, cara! https://github.com/JoshClose/CsvHelper/wiki/Basics

Usar o CsvHelper é realmente fácil. Suas configurações padrão são definidas para os cenários mais comuns.

Aqui estão alguns dados de configuração.

Actors.csv:

Id,FirstName,LastName  
1,Arnold,Schwarzenegger  
2,Matt,Damon  
3,Christian,Bale

Actor.cs (objeto de classe personalizado que representa um ator):

public class Actor
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Lendo o arquivo CSV usando CsvReader:

var csv = new CsvReader( new StreamReader( "Actors.csv" ) );

var atoresList = csv.GetRecords ();

Gravando em um arquivo CSV.

using (var csv = new CsvWriter( new StreamWriter( "Actors.csv" ) )) 
{
    csv.WriteRecords( actorsList );
}

2

Por qualquer motivo, @AliUmair reverteu a edição para sua resposta que corrige seu código que não é executado como está, então aqui está a versão de trabalho que não possui o erro de acesso ao arquivo e lida adequadamente com valores de propriedades de objetos nulos:

/// <summary>
/// Creates the CSV from a generic list.
/// </summary>;
/// <typeparam name="T"></typeparam>;
/// <param name="list">The list.</param>;
/// <param name="csvNameWithExt">Name of CSV (w/ path) w/ file ext.</param>;
public static void CreateCSVFromGenericList<T>(List<T> list, string csvCompletePath)
{
    if (list == null || list.Count == 0) return;

    //get type from 0th member
    Type t = list[0].GetType();
    string newLine = Environment.NewLine;

    if (!Directory.Exists(Path.GetDirectoryName(csvCompletePath))) Directory.CreateDirectory(Path.GetDirectoryName(csvCompletePath));

    using (var sw = new StreamWriter(csvCompletePath))
    {
        //make a new instance of the class name we figured out to get its props
        object o = Activator.CreateInstance(t);
        //gets all properties
        PropertyInfo[] props = o.GetType().GetProperties();

        //foreach of the properties in class above, write out properties
        //this is the header row
        sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);

        //this acts as datarow
        foreach (T item in list)
        {
            //this acts as datacolumn
            var row = string.Join(",", props.Select(d => $"\"{item.GetType().GetProperty(d.Name).GetValue(item, null)?.ToString()}\"")
                                                    .ToArray());
            sw.Write(row + newLine);

        }
    }
}


1

Um método de extensão ToCsv () de uso geral:

  • Suporta Int16 / 32/64, float, double, decimal e qualquer coisa que suporte ToString ()
  • Separador de junção personalizado opcional
  • Seletor personalizado opcional
  • Especificação de manipulação nula / vazia opcional (* Sobrecargas Opt ())

Exemplos de uso:

"123".ToCsv() // "1,2,3"
"123".ToCsv(", ") // "1, 2, 3"
new List<int> { 1, 2, 3 }.ToCsv() // "1,2,3"

new List<Tuple<int, string>> 
{ 
    Tuple.Create(1, "One"), 
    Tuple.Create(2, "Two") 
}
.ToCsv(t => t.Item2);  // "One,Two"

((string)null).ToCsv() // throws exception
((string)null).ToCsvOpt() // ""
((string)null).ToCsvOpt(ReturnNullCsv.WhenNull) // null

Implementação

/// <summary>
/// Specifies when ToCsv() should return null.  Refer to ToCsv() for IEnumerable[T]
/// </summary>
public enum ReturnNullCsv
{
    /// <summary>
    /// Return String.Empty when the input list is null or empty.
    /// </summary>
    Never,

    /// <summary>
    /// Return null only if input list is null.  Return String.Empty if list is empty.
    /// </summary>
    WhenNull,

    /// <summary>
    /// Return null when the input list is null or empty
    /// </summary>
    WhenNullOrEmpty,

    /// <summary>
    /// Throw if the argument is null
    /// </summary>
    ThrowIfNull
}   

/// <summary>
/// Converts IEnumerable list of values to a comma separated string values.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="values">The values.</param>        
/// <param name="joinSeparator"></param>
/// <returns>System.String.</returns>
public static string ToCsv<T>(
    this IEnumerable<T> values,            
    string joinSeparator = ",")
{
    return ToCsvOpt<T>(values, null /*selector*/, ReturnNullCsv.ThrowIfNull, joinSeparator);
}

/// <summary>
/// Converts IEnumerable list of values to a comma separated string values.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="values">The values.</param>
/// <param name="selector">An optional selector</param>
/// <param name="joinSeparator"></param>
/// <returns>System.String.</returns>
public static string ToCsv<T>(
    this IEnumerable<T> values,
    Func<T, string> selector,            
    string joinSeparator = ",") 
{
    return ToCsvOpt<T>(values, selector, ReturnNullCsv.ThrowIfNull, joinSeparator);
}

/// <summary>
/// Converts IEnumerable list of values to a comma separated string values.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="values">The values.</param>
/// <param name="returnNullCsv">Return mode (refer to enum ReturnNullCsv).</param>
/// <param name="joinSeparator"></param>
/// <returns>System.String.</returns>
public static string ToCsvOpt<T>(
    this IEnumerable<T> values,
    ReturnNullCsv returnNullCsv = ReturnNullCsv.Never,
    string joinSeparator = ",")
{
    return ToCsvOpt<T>(values, null /*selector*/, returnNullCsv, joinSeparator);
}

/// <summary>
/// Converts IEnumerable list of values to a comma separated string values.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="values">The values.</param>
/// <param name="selector">An optional selector</param>
/// <param name="returnNullCsv">Return mode (refer to enum ReturnNullCsv).</param>
/// <param name="joinSeparator"></param>
/// <returns>System.String.</returns>
public static string ToCsvOpt<T>(
    this IEnumerable<T> values, 
    Func<T, string> selector,
    ReturnNullCsv returnNullCsv = ReturnNullCsv.Never,
    string joinSeparator = ",")
{
    switch (returnNullCsv)
    {
        case ReturnNullCsv.Never:
            if (!values.AnyOpt())
                return string.Empty;
            break;

        case ReturnNullCsv.WhenNull:
            if (values == null)
                return null;
            break;

        case ReturnNullCsv.WhenNullOrEmpty:
            if (!values.AnyOpt())
                return null;
            break;

        case ReturnNullCsv.ThrowIfNull:
            if (values == null)
                throw new ArgumentOutOfRangeException("ToCsvOpt was passed a null value with ReturnNullCsv = ThrowIfNull.");
            break;

        default:
            throw new ArgumentOutOfRangeException("returnNullCsv", returnNullCsv, "Out of range.");
    }

    if (selector == null)
    {
        if (typeof(T) == typeof(Int16) || 
            typeof(T) == typeof(Int32) || 
            typeof(T) == typeof(Int64))
        {                   
            selector = (v) => Convert.ToInt64(v).ToStringInvariant();
        }
        else if (typeof(T) == typeof(decimal))
        {
            selector = (v) => Convert.ToDecimal(v).ToStringInvariant();
        }
        else if (typeof(T) == typeof(float) ||
                typeof(T) == typeof(double))
        {
            selector = (v) => Convert.ToDouble(v).ToString(CultureInfo.InvariantCulture);
        }
        else
        {
            selector = (v) => v.ToString();
        }            
    }

    return String.Join(joinSeparator, values.Select(v => selector(v)));
}

public static string ToStringInvariantOpt(this Decimal? d)
{
    return d.HasValue ? d.Value.ToStringInvariant() : null;
}

public static string ToStringInvariant(this Decimal d)
{
    return d.ToString(CultureInfo.InvariantCulture);
}

public static string ToStringInvariantOpt(this Int64? l)
{
    return l.HasValue ? l.Value.ToStringInvariant() : null;
}

public static string ToStringInvariant(this Int64 l)
{
    return l.ToString(CultureInfo.InvariantCulture);
}

public static string ToStringInvariantOpt(this Int32? i)
{
    return i.HasValue ? i.Value.ToStringInvariant() : null;
}

public static string ToStringInvariant(this Int32 i)
{
    return i.ToString(CultureInfo.InvariantCulture);
}

public static string ToStringInvariantOpt(this Int16? i)
{
    return i.HasValue ? i.Value.ToStringInvariant() : null;
}

public static string ToStringInvariant(this Int16 i)
{
    return i.ToString(CultureInfo.InvariantCulture);
}

0

Aqui está o meu método de extensão, ele retorna uma string por simplicidade, mas minha implementação grava o arquivo em um data lake.

Ele fornece qualquer delimitador, adiciona aspas à string (caso elas contenham o delimitador) e as transações serão nulas e vazias.

    /// <summary>
    /// A class to hold extension methods for C# Lists 
    /// </summary>
    public static class ListExtensions
    {
        /// <summary>
        /// Convert a list of Type T to a CSV
        /// </summary>
        /// <typeparam name="T">The type of the object held in the list</typeparam>
        /// <param name="items">The list of items to process</param>
        /// <param name="delimiter">Specify the delimiter, default is ,</param>
        /// <returns></returns>
        public static string ToCsv<T>(this List<T> items, string delimiter = ",")
        {
            Type itemType = typeof(T);
            var props = itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);

            var csv = new StringBuilder();

            // Write Headers
            csv.AppendLine(string.Join(delimiter, props.Select(p => p.Name)));

            // Write Rows
            foreach (var item in items)
            {
                // Write Fields
                csv.AppendLine(string.Join(delimiter, props.Select(p => GetCsvFieldasedOnValue(p, item))));
            }

            return csv.ToString();
        }

        /// <summary>
        /// Provide generic and specific handling of fields
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="p"></param>
        /// <param name="item"></param>
        /// <returns></returns>
        private static object GetCsvFieldasedOnValue<T>(PropertyInfo p, T item)
        {
            string value = "";

            try
            {
                value = p.GetValue(item, null)?.ToString();
                if (value == null) return "NULL";  // Deal with nulls
                if (value.Trim().Length == 0) return ""; // Deal with spaces and blanks

                // Guard strings with "s, they may contain the delimiter!
                if (p.PropertyType == typeof(string))
                {
                    value = string.Format("\"{0}\"", value);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return value;
        }
    }

Uso:

 // Tab Delimited (TSV)
 var csv = MyList.ToCsv<MyClass>("\t");
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.