Lectura de archivos CSV usando C #

169

Estoy escribiendo una aplicación de importación simple y necesito leer un archivo CSV, mostrar el resultado en DataGridy mostrar líneas corruptas del archivo CSV en otra cuadrícula. Por ejemplo, muestre las líneas que son más cortas que 5 valores en otra cuadrícula. Estoy tratando de hacer eso así:

StreamReader sr = new StreamReader(FilePath);
importingData = new Account();
string line;
string[] row = new string [5];
while ((line = sr.ReadLine()) != null)
{
    row = line.Split(',');

    importingData.Add(new Transaction
    {
        Date = DateTime.Parse(row[0]),
        Reference = row[1],
        Description = row[2],
        Amount = decimal.Parse(row[3]),
        Category = (Category)Enum.Parse(typeof(Category), row[4])
    });
}

pero es muy difícil operar en matrices en este caso. ¿Hay una mejor manera de dividir los valores?

ilkin
fuente
Gracias por tu solución. Considere publicarlo como una publicación de respuesta; incluirlo en la pregunta no ayuda a su legibilidad.
BartoszKP

Respuestas:

363

No reinventes la rueda. Aproveche lo que ya está en .NET BCL.

  • agregue una referencia a Microsoft.VisualBasic(sí, dice VisualBasic pero también funciona en C #, recuerde que al final todo es solo IL)
  • usa la Microsoft.VisualBasic.FileIO.TextFieldParserclase para analizar el archivo CSV

Aquí está el código de ejemplo:

using (TextFieldParser parser = new TextFieldParser(@"c:\temp\test.csv"))
{
    parser.TextFieldType = FieldType.Delimited;
    parser.SetDelimiters(",");
    while (!parser.EndOfData) 
    {
        //Processing row
        string[] fields = parser.ReadFields();
        foreach (string field in fields) 
        {
            //TODO: Process field
        }
    }
}

Funciona muy bien para mí en mis proyectos de C #.

Aquí hay algunos enlaces / información más:

David Pokluda
fuente
18
REALMENTE desearía que hubiera una manera que no usara las bibliotecas VB, ¡pero esto funcionó perfectamente! ¡Gracias!
gillonba
55
+1: Acabo de romper el lector CSV rápido de lumenworks en un archivo de 53Mb. Parece que el almacenamiento en caché de línea falló después de 43,000 filas y revuelto el búfer. Probé el VB TextFieldParsere hizo el truco. Gracias
Gone Coding
10
+1 Gran respuesta, ya que encuentro que muchas personas no saben que esta clase existe. Una cosa que los futuros espectadores deben tener en cuenta es que la configuración parser.TextFieldType = FieldType.Delimited;no es necesaria si llama parser.SetDelimiters(",");, ya que el método establece la TextFieldTypepropiedad por usted.
Brian
10
También mira esto: dotnetperls.com/textfieldparser . TextFieldParser tiene peor rendimiento que String.Split y StreamReader. Sin embargo, hay una gran diferencia entre string.Split y TextFieldParser. TextFieldParser maneja casos extraños como tener una coma en una columna: puede nombrar una columna como "text with quote"", and comma", y puede obtener el valor correcto en text with quote", and commalugar de valores separados incorrectamente. Por lo tanto, es posible que desee optar por String.Split si su csv es muy simple.
Yongwei Wu
55
Tenga en cuenta que es posible que deba agregar una referencia a Microsoft.VisualBasic para usar esto. Haga clic con el botón derecho en su proyecto en Visual Studio, luego elija Agregar> Referencia y marque la casilla Microsoft.VisualBasic.
Derek Kurth
37

Mi experiencia es que hay muchos formatos diferentes de CSV. Especialmente cómo manejan el escape de comillas y delimitadores dentro de un campo.

Estas son las variantes con las que me he encontrado:

  • las comillas se cotizan y duplican (excel), es decir, 15 "-> campo1," 15 "" ", campo3
  • las comillas no se cambian a menos que el campo se cite por alguna otra razón. es decir, 15 "-> campo1,15", campos3
  • las comillas se escapan con \. es decir, 15 "-> campo1," 15 \ "", campo3
  • las comillas no cambian en absoluto (esto no siempre es posible analizar correctamente)
  • delimitador se cita (excel). es decir, a, b -> campo1, "a, b", campo3
  • delimitador se escapa con \. es decir, a, b -> campo1, a \, b, campo3

He probado muchos de los analizadores csv existentes, pero no hay ninguno que pueda manejar las variantes con las que me he encontrado. También es difícil averiguar en la documentación qué variantes de escape admiten los analizadores.

En mis proyectos ahora uso el VB TextFieldParser o un divisor personalizado.

adrianm
fuente
1
¡Me encanta esta respuesta para los casos de prueba que proporcionó!
Matthew Rodatus
2
El principal problema es que a la mayoría de las implementaciones no les importa RFC 4180 que describe el formato CSV y cómo se deben escapar los delimitadores.
Jenny O'Reilly
RFC-4180 es de 2005, que parece viejo ahora, pero recuerde: el marco .Net fue lanzado por primera vez en 2001. Además, los RFC no siempre son estándares oficiales, y en este caso no tiene el mismo peso que, digamos , ISO-8601 o RFC-761.
Joel Coehoorn
23

Recomiendo CsvHelper de Nuget .

(Agregar una referencia a Microsoft.VisualBasic simplemente no se siente bien, no solo es feo, probablemente ni siquiera sea multiplataforma).

knocte
fuente
2
Es exactamente tan multiplataforma como lo es C #.
PRMan
mal, Microsoft.VisualBasic.dll en Linux proviene de fuentes Mono, que tiene una implementación diferente a la de Microsoft y hay algunas cosas que no se implementan, por ejemplo: stackoverflow.com/questions/6644165/…
knocte
(Idioma +, VB nunca ha tenido ningún foco en virtud de las empresas que han participado en la creación / desarrollo del proyecto Mono, por lo que es muy por detrás en términos de esfuerzos, en comparación con el C # ecosistema / herramientas.)
knocte
1
Después de jugar con ambos, agregaría que CsvHelperviene con una fila incorporada en el asignador de clases; permite variaciones en los encabezados de las columnas (si están presentes), e incluso variaciones aparentemente en el orden de las columnas (aunque no he probado este último). En general, se siente mucho más "alto nivel" que TextFieldParser.
David
1
sí, el espacio de nombres Microsoft.VisualBasic no está disponible en .NET Core 2.1
N4ppeL
13

A veces, usar bibliotecas es genial cuando no desea reinventar la rueda, pero en este caso uno puede hacer el mismo trabajo con menos líneas de código y más fácil de leer en comparación con el uso de bibliotecas. Aquí hay un enfoque diferente que encuentro muy fácil de usar.

  1. En este ejemplo, uso StreamReader para leer el archivo
  2. Regex para detectar el delimitador de cada línea (s).
  3. Una matriz para recopilar las columnas del índice 0 a n

using (StreamReader reader = new StreamReader(fileName))
    {
        string line; 

        while ((line = reader.ReadLine()) != null)
        {
            //Define pattern
            Regex CSVParser = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");

            //Separating columns to array
            string[] X = CSVParser.Split(line);

            /* Do something with X */
        }
    }
Maná
fuente
44
¿Seguramente eso tiene problemas con los datos que contienen nuevas líneas?
Doogal
Ahora los archivos de datos CSV no son conocidos por contener líneas vacías entre datos, pero si tiene una fuente que lo hace, en ese caso simplemente haría una simple prueba de expresión regular para eliminar espacios en blanco o líneas que no contengan nada antes de ejecutar el lector. revise aquí para ver diferentes ejemplos: stackoverflow.com/questions/7647716/…
Mana
1
Sin duda, un enfoque basado en char es más natural para este tipo de problema que una expresión regular. Dependiendo de la presencia de comillas, se supone que el comportamiento es diferente.
Casey
6

CSV puede complicarse muy rápido.

Use algo robusto y bien probado:
FileHelpers: www.filehelpers.net

FileHelpers es una biblioteca .NET gratuita y fácil de usar para importar / exportar datos de registros de longitud fija o delimitados en archivos, cadenas o secuencias.

Keith Blows
fuente
55
Creo que FileHelper está tratando de hacer mucho de una vez. El análisis de archivos es un proceso de 2 pasos en el que primero divide las líneas en campos y luego analiza los campos en datos. La combinación de las funciones hace que sea difícil manejar cosas como los detalles maestros y el filtrado de líneas.
adrianm
4

Otro a esta lista, Cinchoo ETL , una biblioteca de código abierto para leer y escribir archivos CSV

Para un archivo CSV de muestra a continuación

Id, Name
1, Tom
2, Mark

Rápidamente puede cargarlos usando la biblioteca como se muestra a continuación

using (var reader = new ChoCSVReader("test.csv").WithFirstLineHeader())
{
   foreach (dynamic item in reader)
   {
      Console.WriteLine(item.Id);
      Console.WriteLine(item.Name);
   }
}

Si tiene una clase POCO que coincide con el archivo CSV

public class Employee
{
   public int Id { get; set; }
   public string Name { get; set; }
}

Puede usarlo para cargar el archivo CSV como se muestra a continuación

using (var reader = new ChoCSVReader<Employee>("test.csv").WithFirstLineHeader())
{
   foreach (var item in reader)
   {
      Console.WriteLine(item.Id);
      Console.WriteLine(item.Name);
   }
}

Por favor revisa los artículos en CodeProject sobre cómo usarlo.

Descargo de responsabilidad: soy el autor de esta biblioteca

RajN
fuente
Hola, ¿puedes cargar csv a la tabla SQL? No conozco el encabezado de la tabla CSV de antemano. Simplemente refleje lo que está en csv en la tabla SQL
aggie
Sí tu puedes. consulte este enlace stackoverflow.com/questions/20759302/…
RajN
2
private static DataTable ConvertCSVtoDataTable(string strFilePath)
        {
            DataTable dt = new DataTable();
            using (StreamReader sr = new StreamReader(strFilePath))
            {
                string[] headers = sr.ReadLine().Split(',');
                foreach (string header in headers)
                {
                    dt.Columns.Add(header);
                }
                while (!sr.EndOfStream)
                {
                    string[] rows = sr.ReadLine().Split(',');
                    DataRow dr = dt.NewRow();
                    for (int i = 0; i < headers.Length; i++)
                    {
                        dr[i] = rows[i];
                    }
                    dt.Rows.Add(dr);
                }

            }

            return dt;
        }

        private static void WriteToDb(DataTable dt)
        {
            string connectionString =
                "Data Source=localhost;" +
                "Initial Catalog=Northwind;" +
                "Integrated Security=SSPI;";

            using (SqlConnection con = new SqlConnection(connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("spInsertTest", con))
                    {
                        cmd.CommandType = CommandType.StoredProcedure;

                        cmd.Parameters.Add("@policyID", SqlDbType.Int).Value = 12;
                        cmd.Parameters.Add("@statecode", SqlDbType.VarChar).Value = "blagh2";
                        cmd.Parameters.Add("@county", SqlDbType.VarChar).Value = "blagh3";

                        con.Open();
                        cmd.ExecuteNonQuery();
                    }
                }

         }
anongf4gsdfg54564533
fuente
¿De dónde copiaste esta solución?
MindRoasterMir
0

En primer lugar, debe comprender qué es CSV y cómo escribirlo.

  1. Cada siguiente cadena ( /r/n) es la siguiente fila de "tabla".
  2. Las celdas de "tabla" están separadas por algún símbolo delimitador. Los símbolos más utilizados son \to,
  3. Cada celda posiblemente puede contener este símbolo delimitador (la celda debe comenzar con el símbolo de comillas y termina con este símbolo en este caso)
  4. Cada celda posiblemente puede contener /r/nsímbolos (la celda debe comenzar con un símbolo de comillas y termina con este símbolo en este caso)

La forma más fácil para que C # / Visual Basic trabaje con archivos CSV es usar una Microsoft.VisualBasicbiblioteca estándar . Solo necesita agregar la referencia necesaria y la siguiente cadena a su clase:

using Microsoft.VisualBasic.FileIO;

Sí, puedes usarlo en C #, no te preocupes. Esta biblioteca puede leer archivos relativamente grandes y admite todas las reglas necesarias, por lo que podrá trabajar con todos los archivos CSV.

Hace algún tiempo escribí una clase simple para lectura / escritura CSV basada en esta biblioteca. Con esta clase simple, podrá trabajar con CSV como con una matriz de 2 dimensiones. Puedes encontrar mi clase en el siguiente enlace: https://github.com/ukushu/DataExporter

Ejemplo simple de uso:

Csv csv = new Csv("\t");//delimiter symbol

csv.FileOpen("c:\\file1.csv");

var row1Cell6Value = csv.Rows[0][5];

csv.AddRow("asdf","asdffffff","5")

csv.FileSave("c:\\file2.csv");
Andrés
fuente
0

Para completar las respuestas anteriores, uno puede necesitar una colección de objetos de su archivo CSV, ya sea analizados por TextFieldParserel string.Splitmétodo o por el método, y luego cada línea convertida en un objeto a través de Reflection. Obviamente, primero debe definir una clase que coincida con las líneas del archivo CSV.

Utilicé el serializador CSV simple de Michael Kropat que se encuentra aquí: clase genérica a CSV (todas las propiedades) y reutilicé sus métodos para obtener los campos y propiedades de la clase deseada.

Deserializo mi archivo CSV con el siguiente método:

public static IEnumerable<T> ReadCsvFileTextFieldParser<T>(string fileFullPath, string delimiter = ";") where T : new()
{
    if (!File.Exists(fileFullPath))
    {
        return null;
    }

    var list = new List<T>();
    var csvFields = GetAllFieldOfClass<T>();
    var fieldDict = new Dictionary<int, MemberInfo>();

    using (TextFieldParser parser = new TextFieldParser(fileFullPath))
    {
        parser.SetDelimiters(delimiter);

        bool headerParsed = false;

        while (!parser.EndOfData)
        {
            //Processing row
            string[] rowFields = parser.ReadFields();
            if (!headerParsed)
            {
                for (int i = 0; i < rowFields.Length; i++)
                {
                    // First row shall be the header!
                    var csvField = csvFields.Where(f => f.Name == rowFields[i]).FirstOrDefault();
                    if (csvField != null)
                    {
                        fieldDict.Add(i, csvField);
                    }
                }
                headerParsed = true;
            }
            else
            {
                T newObj = new T();
                for (int i = 0; i < rowFields.Length; i++)
                {
                    var csvFied = fieldDict[i];
                    var record = rowFields[i];

                    if (csvFied is FieldInfo)
                    {
                        ((FieldInfo)csvFied).SetValue(newObj, record);
                    }
                    else if (csvFied is PropertyInfo)
                    {
                        var pi = (PropertyInfo)csvFied;
                        pi.SetValue(newObj, Convert.ChangeType(record, pi.PropertyType), null);
                    }
                    else
                    {
                        throw new Exception("Unhandled case.");
                    }
                }
                if (newObj != null)
                {
                    list.Add(newObj);
                }
            }
        }
    }
    return list;
}

public static IEnumerable<MemberInfo> GetAllFieldOfClass<T>()
{
    return
        from mi in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
        where new[] { MemberTypes.Field, MemberTypes.Property }.Contains(mi.MemberType)
        let orderAttr = (ColumnOrderAttribute)Attribute.GetCustomAttribute(mi, typeof(ColumnOrderAttribute))
        orderby orderAttr == null ? int.MaxValue : orderAttr.Order, mi.Name
        select mi;            
}
EricBDev
fuente
0

Recomiendo encarecidamente usar CsvHelper.

Aquí hay un ejemplo rápido:

public class csvExampleClass
{
    public string Id { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
}

var items = DeserializeCsvFile<List<csvExampleClass>>( csvText );

public static List<T> DeserializeCsvFile<T>(string text)
{
    CsvReader csv = new CsvReader( new StringReader( text ) );
    csv.Configuration.Delimiter = ",";
    csv.Configuration.HeaderValidated = null;
    csv.Configuration.MissingFieldFound = null;
    return (List<T>)csv.GetRecords<T>();
}

La documentación completa se puede encontrar en: https://joshclose.github.io/CsvHelper

Kieran
fuente