Convertir una lista genérica en una cadena CSV


139

Tengo una lista de valores enteros (Lista) y me gustaría generar una cadena de valores delimitados por comas. Es decir, todos los elementos de la lista salen a una sola lista delimitada por comas.

Mis pensamientos ... 1. pasar la lista a un método. 2. Use el generador de cadenas para iterar la lista y agregar comas. 3. Pruebe el último carácter y, si es una coma, elimínelo.

¿Cuáles son tus pensamientos? Es esta la mejor manera?

¿Cómo cambiaría mi código si quisiera manejar no solo enteros (mi plan actual) sino cadenas, largos, dobles, bools, etc., etc. en el futuro? Supongo que acepte una lista de cualquier tipo.

Respuestas:


243

Es sorprendente lo que el Framework ya hace por nosotros.

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

Para el caso general:

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

Como puede ver, efectivamente no es diferente. Tenga en cuenta que es posible que necesite incluir x.ToString()comillas (es decir, "\"" + x.ToString() + "\"") en caso de que x.ToString()contenga comas.

Para una lectura interesante sobre una ligera variante de esto: ver Comma Quibbling en el blog de Eric Lippert.

Nota: Esto fue escrito antes de que .NET 4.0 fuera lanzado oficialmente. Ahora solo podemos decir

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

usando la sobrecarga String.Join<T>(string, IEnumerable<T>). Este método proyectará automáticamente cada elemento xen x.ToString().


List<int>no tiene método Selecten el marco 3.5 a menos que me falte algo.
ajeh

2
@ajeh: Probablemente te estés perdiendo una usingdeclaración.
Jason

¿Qué importación específica?
ajeh

1
Pruébelo System.Linq.Enumerable(y, por supuesto, necesitará System.Core.dllensamblarlo, pero presumiblemente ya lo tiene). Ya ves, List<int> nunca tiene Selectcomo método. Más bien, se System.Linq.Enumerabledefine Selectcomo un método de extensión IEnumerable<T>, del cual List<int>es un ejemplo de. Por lo tanto, necesita System.Linq.Enumerableen sus importaciones elegir este método de extensión.
Jason

Si se trata de valores numéricos y las comas son un problema (según la configuración regional), una alternativa es x.ToString(CultureInfo.InvariantCulture). Esto usará el punto como separador decimal.
heltonbiker

15

en 3.5, todavía podía hacer esto. Es mucho más simple y no necesita lambda.

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

ToArray()El método de List<int>no se puede utilizar con el argumento de tipo en el marco 3.5 a menos que me falte algo.
ajeh

Brillante. No hay necesidad de ToArray <string> ya que se usa ToString () secundario.
Christian

11

Puede crear un método de extensión al que pueda llamar en cualquier 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());
}

Luego puede llamar al método en la lista original:

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

7

Puedes usar String.Join.

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

No es necesario especificar parámetros de tipo genérico en la llamada ConvertAllaquí, ambos inty stringse deducirán.
Pavel Minaev

1
En lugar de hacer Array.ConvertAll(...' you can just do list.ConvertAll (e => e.ToString ()). ToArray) `, simplemente escribe menos.
David

string.Join (",", lista); hará bien :)
Christian

6

Si algún cuerpo desea convertir una lista de objetos de clase personalizados en lugar de una lista de cadenas , anule el método ToString de su clase con la representación de fila csv de su clase.

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

Luego, el siguiente código se puede utilizar para convertir esta lista de clases a CSV con columna de encabezado

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 en su lugar MyClass.Select
Piotr Ferenc

5

Como el código en el enlace proporcionado por @Frank Create a CSV File from a .NET Generic List hubo un pequeño problema al finalizar cada línea con un ,modifiqué el código para deshacerme de él. Espero que ayude a alguien.

/// <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);

        }
    }
}

Información adicional: El proceso no puede acceder al archivo 'c: \ temp \ matchingMainWav.csv' porque está siendo utilizado por otro proceso. la carpeta devexiste, pero no el archivo ... ¿no estoy usando ese derecho?
Tom Stickel

El método File.Create crea el archivo y abre un FileStream en el archivo. Entonces su archivo ya está abierto. Realmente no necesita el archivo. Crear método en absoluto:
David

Si alguna propiedad es nula, ¿hay alguna forma de evitarla?
Daniel Jackson

@DanielJackson Puede escribir una cláusula where en esta declaración sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);No probado pero no sabe lo que está tratando de lograr
Ali Umair

4

Lo explico en profundidad en esta publicación . Pegaré el código aquí con breves descripciones.

Aquí está el método que crea la fila del encabezado. Utiliza los nombres de propiedad como nombres de columna.

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 crea todas las filas de valores

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

Y aquí está el método que los une y crea el archivo real.

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

1
Esto funciona muy bien Mejoré esto para pasar el delimitador como parámetro, por lo que se puede generar cualquier tipo de archivo delimitado. Los CSV son difíciles de manejar si el texto contiene comas, por lo que genero |archivos delimitados utilizando la versión mejorada. ¡Gracias!
Shiva


3

Me gusta un buen método de extensión simple

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

Luego puede llamar al método en la lista original:

string CsvString = myList.ToCsv();

Más limpio y más fácil de leer que algunas de las otras sugerencias.


2

El problema con String.Join es que no está manejando el caso de una coma que ya existe en el valor. Cuando existe una coma, rodea el valor en Cotizaciones y reemplaza todas las Cotizaciones existentes con Cotizaciones dobles.

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

Ver módulo CSV


2

La biblioteca CsvHelper es muy popular en Nuget. ¡Vale la pena, hombre! https://github.com/JoshClose/CsvHelper/wiki/Basics

Usar CsvHelper es realmente fácil. Su configuración predeterminada está configurada para los escenarios más comunes.

Aquí hay algunos datos de configuración.

Actors.csv:

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

Actor.cs (objeto de clase personalizado que representa un actor):

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

Lectura del archivo CSV usando CsvReader:

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

var actoresList = csv.GetRecords ();

Escribir en un archivo CSV.

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

2

Por alguna razón, @AliUmair revirtió la edición a su respuesta que corrige su código que no se ejecuta tal como está, así que aquí está la versión de trabajo que no tiene el error de acceso al archivo y maneja adecuadamente los valores de propiedad de objeto nulo:

/// <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

Un método de extensión ToCsv () de propósito general:

  • Admite Int16 / 32/64, flotante, doble, decimal y cualquier cosa que admita ToString ()
  • Separador de unión personalizado opcional
  • Selector personalizado opcional
  • Especificación de manejo nulo / vacío opcional (* Opt () sobrecargas)

Ejemplos 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

Implementación

/// <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

Aquí está mi método de extensión, devuelve una cadena por simplicidad, pero mi implementación escribe el archivo en un lago de datos.

Proporciona cualquier delimitador, agrega comillas a la cadena (en caso de que contengan el delimitador) y las ofertas serán nulas y en blanco.

    /// <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");
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.