Conversione di un elenco generico in una stringa CSV


139

Ho un elenco di valori interi (Elenco) e vorrei generare una stringa di valori delimitati da virgole. Questo è tutti gli elementi dell'elenco di output in un elenco delimitato da una singola virgola.

I miei pensieri ... 1. passare l'elenco a un metodo. 2. Utilizzare stringbuilder per iterare l'elenco e aggiungere virgole 3. Testare l'ultimo carattere e, se è una virgola, eliminarlo.

Quali sono i tuoi pensieri? È questo il modo migliore?

Come cambierebbe il mio codice se volessi gestire non solo numeri interi (il mio piano attuale) ma stringhe, long, double, bool, ecc, ecc. In futuro? Immagino che faccia accettare un elenco di qualsiasi tipo.

Risposte:


243

È incredibile ciò che il Framework fa già per noi.

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

Per il caso generale:

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

Come puoi vedere, in realtà non è diverso. Attenzione che potrebbe essere necessario racchiudere effettivamente le x.ToString()virgolette (ovvero, "\"" + x.ToString() + "\"") nel caso x.ToString()contenga virgole.

Per una lettura interessante su una leggera variante di questo: vedi Comma Quibbling sul blog di Eric Lippert.

Nota: questo è stato scritto prima del rilascio ufficiale di .NET 4.0. Ora possiamo solo dire

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

usando il sovraccarico String.Join<T>(string, IEnumerable<T>). Questo metodo proietterà automaticamente ogni elemento xsu x.ToString().


List<int>non ha metodo Selectnel framework 3.5 a meno che non mi manchi qualcosa.
ajeh,

2
@ajeh: probabilmente ti manca una usingdichiarazione.
Jason,

Quale importazione specifica?
ajeh,

1
Prova System.Linq.Enumerable(e ovviamente avrai bisogno System.Core.dlldell'assemblaggio, ma presumibilmente ce l'hai già). Vedi, List<int> non ha mai avuto Selectun metodo. Piuttosto, System.Linq.Enumerabledefinisce Selectcome un metodo di estensione su IEnumerable<T>, di cui List<int>è un esempio di. Pertanto, è necessario System.Linq.Enumerablenelle importazioni importare questo metodo di estensione.
Jason,

Se hai a che fare con valori numerici e virgole sono un problema (a seconda delle impostazioni locali), un'alternativa è x.ToString(CultureInfo.InvariantCulture). Questo utilizzerà il periodo come separatore decimale.
Heltonbiker,

15

in 3.5, ero ancora in grado di farlo. È molto più semplice e non ha bisogno di lambda.

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

ToArray()il metodo di List<int>non può essere usato con l'argomento type nel framework 3.5 a meno che non mi manchi qualcosa.
ajeh,

Brillante. Non è necessario ToArray <string> poiché viene utilizzato ToString () figlio.
Christian,

11

È possibile creare un metodo di estensione che è possibile chiamare su qualsiasi 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());
}

Quindi puoi semplicemente chiamare il metodo nell'elenco originale:

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

7

È possibile utilizzare String.Join.

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

Non è necessario specificare parametri di tipo generico in chiamata ConvertAllqui - entrambi inte stringverranno dedotti.
Pavel Minaev,

1
Invece di fare Array.ConvertAll(...' you can just do list.ConvertAll (e => e.ToString ()). ToArray) `, solo meno digitando.
David,

string.Join (",", list); andrà bene :)
Christian,

6

Se un corpo vuole convertire un elenco di oggetti di classe personalizzati anziché un elenco di stringhe, sovrascrivi il metodo ToString della tua classe con la rappresentazione della riga CSV della tua classe.

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

Quindi è possibile utilizzare il seguente codice per convertire questo elenco di classi in CSV con colonna di intestazione

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 invece MyClass.Select
Piotr Ferenc il

5

Dato che il codice nel link fornito da @Frank Crea un file CSV da un elenco generico .NET, c'era un piccolo problema di terminare ogni riga con un ,ho modificato il codice per sbarazzarsene. Spero che aiuti qualcuno.

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

        }
    }
}

Ulteriori informazioni: Il processo non può accedere al file 'c: \ temp \ matchingMainWav.csv' perché è utilizzato da un altro processo. la cartella devesiste, ma non il file ... non sto usando quello giusto?
Tom Stickel,

Il metodo File.Create crea il file e apre un FileStream sul file. Quindi il tuo file è già aperto. Non hai davvero bisogno del file. Crea il metodo:
David

Se qualche proprietà è nulla, c'è un modo per aggirare questo?
Daniel Jackson,

@DanielJackson Puoi scrivere una clausola where in questa affermazione sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);Non testato ma non sai cosa stai cercando di ottenere
Ali Umair,

4

Lo spiego in modo approfondito in questo post . Incollerò il codice qui con brevi descrizioni.

Ecco il metodo che crea la riga di intestazione. Usa i nomi delle proprietà come nomi di colonna.

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

Questo metodo crea tutte le righe del valore

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

Ed ecco il metodo che li unisce e crea il file effettivo.

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

1
Funziona molto bene Ho migliorato questo passaggio per passare il delimitatore come parametro, in modo da poter generare qualsiasi tipo di file delimitato. I CSV sono difficili da gestire se il testo contiene virgole, quindi genera |file delimitati utilizzando la versione migliorata. Grazie!
Shiva,


3

Mi piace un bel metodo di estensione semplice

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

Quindi puoi semplicemente chiamare il metodo nell'elenco originale:

string CsvString = myList.ToCsv();

Più pulito e più facile da leggere rispetto ad alcuni degli altri suggerimenti.


2

Il problema con String.Join è che non stai gestendo il caso di una virgola già esistente nel valore. Quando esiste una virgola, circondi il valore tra virgolette e sostituisci tutte le virgolette esistenti con virgolette doppie.

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

Vedi modulo CSV


2

La libreria CsvHelper è molto popolare nel Nuget. Ne vale la pena, amico! https://github.com/JoshClose/CsvHelper/wiki/Basics

L'uso di CsvHelper è davvero semplice. Le impostazioni predefinite sono configurate per gli scenari più comuni.

Ecco alcuni dati di installazione.

Actors.csv:

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

Actor.cs (oggetto di classe personalizzato che rappresenta un attore):

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

Lettura del file CSV utilizzando CsvReader:

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

var attoriList = csv.GetRecords ();

Scrittura in un file CSV.

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

2

Per qualsiasi motivo, @AliUmair ha ripristinato la modifica alla sua risposta che corregge il suo codice che non funziona così com'è, quindi ecco la versione funzionante che non ha l'errore di accesso al file e gestisce correttamente i valori delle proprietà degli oggetti null:

/// <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 metodo di estensione ToCsv () generico:

  • Supporta Int16 / 32/64, float, double, decimal e qualsiasi cosa supporti ToString ()
  • Separatore di join personalizzato opzionale
  • Selettore personalizzato opzionale
  • Specifiche di gestione nulle / vuote opzionali (* sovraccarichi Opt ())

Esempi di utilizzo:

"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

Implementazione

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

Ecco il mio metodo di estensione, restituisce una stringa per semplicità ma la mia implementazione scrive il file in un lago di dati.

Fornisce qualsiasi delimitatore, aggiunge virgolette alla stringa (nel caso contengano il delimitatore) e le offerte saranno nulle e vuote.

    /// <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");
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.