Analizar archivos CSV en C #, con encabezado

¿Hay alguna forma predeterminada / oficial / recomendada para analizar archivos CSV en C #? No quiero rodar mi propio analizador.

Además, he visto instancias de personas que utilizan ODBC / OLE DB para leer CSV a través del controlador de texto, y mucha gente desalienta esto debido a sus “inconvenientes”. ¿Cuáles son estos inconvenientes?

Idealmente, estoy buscando un camino a través del cual pueda leer el CSV por nombre de columna, usando el primer registro como los nombres de encabezado / campo. Algunas de las respuestas son correctas, pero funcionan para básicamente deserializar el archivo en clases.

Deje que una biblioteca maneje todos los detalles esenciales para usted. 🙂

Eche un vistazo a FileHelpers y quédese SECO – No se repita – no necesita volver a inventar la rueda una vez.

Básicamente, solo necesita definir esa forma de sus datos (los campos en su línea individual en el CSV) mediante una clase pública (y atributos tan bien pensados ​​como valores predeterminados, reemplazos para valores NULOS, etc.), señale el motor de FileHelpers en un archivo y el bingo: recupera todas las entradas de ese archivo. Una operación simple: ¡excelente rendimiento!

Un analizador de CSV ahora es parte de .NET Framework.

Agregue una referencia a Microsoft.VisualBasic.dll (funciona bien en C #, no importa el nombre)

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

Los documentos están aquí – TextFieldParser Class

CsvHelper (una biblioteca que mantengo) leerá un archivo CSV en objetos personalizados.

 var csv = new CsvReader( File.OpenText( "file.csv" ) ); var myCustomObjects = csv.GetRecords(); 

A veces no eres dueño de los objetos que estás tratando de leer. En este caso, puede usar un mapeo fluido porque no puede poner atributos en la clase.

 public sealed class MyCustomObjectMap : CsvClassMap { public MyCustomObjectMap() { Map( m => m.Property1 ).Name( "Column Name" ); Map( m => m.Property2 ).Index( 4 ); Map( m => m.Property3 ).Ignore(); Map( m => m.Property4 ).TypeConverter(); } } 

En una aplicación comercial, utilizo el proyecto Open Source en codeproject.com, CSVReader .

Funciona bien y tiene un buen rendimiento. Hay algunos puntos de referencia en el enlace que proporcioné.

Un ejemplo simple, copiado de la página del proyecto:

 using (CsvReader csv = new CsvReader(new StreamReader("data.csv"), true)) { int fieldCount = csv.FieldCount; string[] headers = csv.GetFieldHeaders(); while (csv.ReadNextRecord()) { for (int i = 0; i < fieldCount; i++) Console.Write(string.Format("{0} = {1};", headers[i], csv[i])); Console.WriteLine(); } } 

Como puede ver, es muy fácil trabajar con él.

Sé que es un poco tarde, pero acabo de encontrar una biblioteca Microsoft.VisualBasic.FileIO que tiene clase TextFieldParser para procesar archivos csv.

Si solo necesita leer archivos csv, le recomiendo esta biblioteca: Un lector rápido de CSV
Si también necesita generar archivos csv, utilice este: FileHelpers

Ambos son gratuitos y de código abierto.

Aquí hay una clase de ayuda que uso a menudo, en caso de que alguien vuelva a este hilo (quería compartirlo).

Utilizo esto por la simplicidad de portarlo en proyectos listos para usar:

 public class CSVHelper : List { protected string csv = string.Empty; protected string separator = ","; public CSVHelper(string csv, string separator = "\",\"") { this.csv = csv; this.separator = separator; foreach (string line in Regex.Split(csv, System.Environment.NewLine).ToList().Where(s => !string.IsNullOrEmpty(s))) { string[] values = Regex.Split(line, separator); for (int i = 0; i < values.Length; i++) { //Trim values values[i] = values[i].Trim('\"'); } this.Add(values); } } } 

Y úsalo como:

 public List GetPeople(string csvContent) { List people = new List(); CSVHelper csv = new CSVHelper(csvContent); foreach(string[] line in csv) { Person person = new Person(); person.Name = line[0]; person.TelephoneNo = line[1]; people.Add(person); } return people; } 

[Ayudante csv actualizado: error corregido donde el último carácter de la nueva línea creó una nueva línea]

Esta solución usa el ensamblado oficial Microsoft.VisualBasic para analizar CSV.

Ventajas:

  • delimitador escapar
  • ignora el encabezado
  • espacios recortados
  • ignorar comentarios

Código:

  using Microsoft.VisualBasic.FileIO; public static List> ParseCSV (string csv) { List> result = new List>(); // To use the TextFieldParser a reference to the Microsoft.VisualBasic assembly has to be added to the project. using (TextFieldParser parser = new TextFieldParser(new StringReader(csv))) { parser.CommentTokens = new string[] { "#" }; parser.SetDelimiters(new string[] { ";" }); parser.HasFieldsEnclosedInQuotes = true; // Skip over header line. //parser.ReadLine(); while (!parser.EndOfData) { var values = new List(); var readFields = parser.ReadFields(); if (readFields != null) values.AddRange(readFields); result.Add(values); } } return result; } 

Escribí TinyCsvParser para .NET, que es uno de los analizadores de .NET más rápidos y altamente configurable para analizar casi cualquier formato CSV.

Se lanza bajo licencia de MIT:

Puedes usar NuGet para instalarlo. Ejecute el siguiente comando en la consola del Administrador de paquetes .

 PM> Install-Package TinyCsvParser 

Uso

Imagine que tenemos una lista de personas en un archivo CSV persons.csv con su nombre, apellido y fecha de nacimiento.

 FirstName;LastName;BirthDate Philipp;Wagner;1986/05/12 Max;Musterman;2014/01/02 

El modelo de dominio correspondiente en nuestro sistema podría verse así.

 private class Person { public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } } 

Al usar TinyCsvParser, debe definir la asignación entre las columnas en los datos CSV y la propiedad en su modelo de dominio.

 private class CsvPersonMapping : CsvMapping { public CsvPersonMapping() : base() { MapProperty(0, x => x.FirstName); MapProperty(1, x => x.LastName); MapProperty(2, x => x.BirthDate); } } 

Y luego podemos usar el mapeo para analizar los datos CSV con un CsvParser .

 namespace TinyCsvParser.Test { [TestFixture] public class TinyCsvParserTest { [Test] public void TinyCsvTest() { CsvParserOptions csvParserOptions = new CsvParserOptions(true, new[] { ';' }); CsvPersonMapping csvMapper = new CsvPersonMapping(); CsvParser csvParser = new CsvParser(csvParserOptions, csvMapper); var result = csvParser .ReadFromFile(@"persons.csv", Encoding.ASCII) .ToList(); Assert.AreEqual(2, result.Count); Assert.IsTrue(result.All(x => x.IsValid)); Assert.AreEqual("Philipp", result[0].Result.FirstName); Assert.AreEqual("Wagner", result[0].Result.LastName); Assert.AreEqual(1986, result[0].Result.BirthDate.Year); Assert.AreEqual(5, result[0].Result.BirthDate.Month); Assert.AreEqual(12, result[0].Result.BirthDate.Day); Assert.AreEqual("Max", result[1].Result.FirstName); Assert.AreEqual("Mustermann", result[1].Result.LastName); Assert.AreEqual(2014, result[1].Result.BirthDate.Year); Assert.AreEqual(1, result[1].Result.BirthDate.Month); Assert.AreEqual(1, result[1].Result.BirthDate.Day); } } } 

Guía del usuario

Una guía de usuario completa está disponible en:

No hay una forma oficial que yo sepa, pero deberían usar las bibliotecas existentes. Aquí hay uno que encontré realmente útil de CodeProject:

http://www.codeproject.com/KB/database/CsvReader.aspx

Aquí está mi implementación de KISS …

 using System; using System.Collections.Generic; using System.Text; class CsvParser { public static List Parse(string line) { const char escapeChar = '"'; const char splitChar = ','; bool inEscape = false; bool priorEscape = false; List result = new List(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < line.Length; i++) { char c = line[i]; switch (c) { case escapeChar: if (!inEscape) inEscape = true; else { if (!priorEscape) { if (i + 1 < line.Length && line[i + 1] == escapeChar) priorEscape = true; else inEscape = false; } else { sb.Append(c); priorEscape = false; } } break; case splitChar: if (inEscape) //if in escape sb.Append(c); else { result.Add(sb.ToString()); sb.Length = 0; } break; default: sb.Append(c); break; } } if (sb.Length > 0) result.Add(sb.ToString()); return result; } } 

Hace algún tiempo, escribí una clase simple de lectura / escritura CSV basada en la biblioteca Microsoft.VisualBasic . Usando esta clase simple podrás trabajar con CSV como con 2 dimensiones de matriz. Puede 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"); 

Para leer el encabezado solo lo que necesita es leer las csv.Rows[0] 🙂

¿Basado en la publicación de unlimit sobre Cómo dividir correctamente un CSV usando la función C # split ()? :

 string[] tokens = System.Text.RegularExpressions.Regex.Split(paramString, ","); 

NOTA: esto no maneja comillas escapadas / anidadas, etc., y por lo tanto solo es adecuado para ciertas listas CSV simples.

Este código lee csv a DataTable:

 public static DataTable ReadCsv(string path) { DataTable result = new DataTable("SomeData"); using (TextFieldParser parser = new TextFieldParser(path)) { parser.TextFieldType = FieldType.Delimited; parser.SetDelimiters(","); bool isFirstRow = true; //IList headers = new List(); while (!parser.EndOfData) { string[] fields = parser.ReadFields(); if (isFirstRow) { foreach (string field in fields) { result.Columns.Add(new DataColumn(field, typeof(string))); } isFirstRow = false; } else { int i = 0; DataRow row = result.NewRow(); foreach (string field in fields) { row[i++] = field; } result.Rows.Add(row); } } } return result; } 

Otra a esta lista, Cinchoo ETL – una biblioteca de código abierto para leer y escribir múltiples formatos de archivo (CSV, archivo plano, Xml, JSON, etc.)

La muestra a continuación muestra cómo leer un archivo CSV rápidamente (no se requiere objeto POCO)

 static void ReadCSV() { using (var stream = new MemoryStream()) using (var reader = new StreamReader(stream)) using (var writer = new StreamWriter(stream)) using (var parser = new ChoCSVReader(reader)) { writer.WriteLine("id,name"); writer.WriteLine("1,Carl"); writer.WriteLine("2,Mark"); writer.WriteLine("3,Tom"); writer.Flush(); stream.Position = 0; foreach (dynamic dr in parser) { Console.WriteLine("Id: {0}, Name: {1}", dr.id, dr.name); } } } 

La muestra a continuación muestra cómo leer un archivo CSV usando un objeto POCO

 public partial class EmployeeRec { public int Id { get; set; } public string Name { get; set; } } static void ReadCSV() { using (var stream = new MemoryStream()) using (var reader = new StreamReader(stream)) using (var writer = new StreamWriter(stream)) using (var parser = new ChoCSVReader(reader)) { writer.WriteLine("id,name"); writer.WriteLine("1,Carl"); writer.WriteLine("2,Mark"); writer.WriteLine("3,Tom"); writer.Flush(); stream.Position = 0; foreach (var dr in parser) { Console.WriteLine("Id: {0}, Name: {1}", dr.id, dr.name); } } } 

Consulte los artículos en CodeProject sobre cómo usarlo.

Solución de archivo de fuente única para necesidades de análisis directo, útil. Se ocupa de todos los casos desagradables. Como la nueva normalización de línea y el manejo de nuevas líneas en literales de cadena entrecomillados. ¡De nada!

Si su archivo CSV tiene un encabezado, solo debe leer los nombres de las columnas (y calcular los índices de las columnas) desde la primera fila. Simple como eso.

Tenga en cuenta que Dump es un método LINQPad, es posible que desee eliminarlo si no está utilizando LINQPad.

 void Main() { var file1 = "a,b,c\r\nx,y,z"; CSV.ParseText(file1).Dump(); var file2 = "a,\"b\",c\r\nx,\"y,z\""; CSV.ParseText(file2).Dump(); var file3 = "a,\"b\",c\r\nx,\"y\r\nz\""; CSV.ParseText(file3).Dump(); var file4 = "\"\"\"\""; CSV.ParseText(file4).Dump(); } static class CSV { public struct Record { public readonly string[] Row; public string this[int index] => Row[index]; public Record(string[] row) { Row = row; } } public static List ParseText(string text) { return Parse(new StringReader(text)); } public static List ParseFile(string fn) { using (var reader = File.OpenText(fn)) { return Parse(reader); } } public static List Parse(TextReader reader) { var data = new List(); var col = new StringBuilder(); var row = new List(); for (; ; ) { var ln = reader.ReadLine(); if (ln == null) break; if (Tokenize(ln, col, row)) { data.Add(new Record(row.ToArray())); row.Clear(); } } return data; } public static bool Tokenize(string s, StringBuilder col, List row) { int i = 0; if (col.Length > 0) { col.AppendLine(); // continuation if (!TokenizeQuote(s, ref i, col, row)) { return false; } } while (i < s.Length) { var ch = s[i]; if (ch == ',') { row.Add(col.ToString().Trim()); col.Length = 0; i++; } else if (ch == '"') { i++; if (!TokenizeQuote(s, ref i, col, row)) { return false; } } else { col.Append(ch); i++; } } if (col.Length > 0) { row.Add(col.ToString().Trim()); col.Length = 0; } return true; } public static bool TokenizeQuote(string s, ref int i, StringBuilder col, List row) { while (i < s.Length) { var ch = s[i]; if (ch == '"') { // escape sequence if (i + 1 < s.Length && s[i + 1] == '"') { col.Append('"'); i++; i++; continue; } i++; return true; } else { col.Append(ch); i++; } } return false; } }