Analisando arquivos CSV em C #, com header

Existe uma maneira padrão / oficial / recomendada para analisar arquivos CSV em c #? Eu não quero rolar meu próprio parser.

Além disso, eu vi casos de pessoas usando ODBC / OLE DB para ler CSV através do driver de texto, e muitas pessoas desencorajam isso devido a suas “desvantagens”. Quais são esses inconvenientes?

Idealmente, estou procurando uma maneira através da qual eu possa ler o CSV por nome de coluna, usando o primeiro registro como os nomes de header / campo. Algumas das respostas dadas estão corretas, mas funcionam basicamente para desserializar o arquivo em classs.

Deixe uma biblioteca lidar com todos os detalhes essenciais para você! 🙂

Confira FileHelpers e fique seco – não se repita – não há necessidade de reinventar a roda uma milionésima vez ….

Você basicamente só precisa definir essa forma de seus dados – os campos em sua linha individual no CSV – por meio de uma class pública (e atributos bem pensados ​​como valores padrão, substituições para valores NULL e assim por diante), ponto o mecanismo FileHelpers em um arquivo e o bingo – você recupera todas as inputs desse arquivo. Uma operação simples – ótimo desempenho!

Um analisador CSV agora faz parte do .NET Framework.

Adicione uma referência ao Microsoft.VisualBasic.dll (funciona bem em C #, não se preocupe com o nome)

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

Os documentos estão aqui – TextFieldParser Class

CsvHelper (uma biblioteca que eu mantenho) lerá um arquivo CSV em objects personalizados.

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

Às vezes você não possui os objects que você está tentando ler. Nesse caso, você pode usar o mapeamento fluente porque não pode colocar atributos na class.

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

Em um aplicativo de negócios, eu uso o projeto Open Source no codeproject.com, CSVReader .

Funciona bem e tem bom desempenho. Existe algum benchmarking no link fornecido.

Um exemplo simples, copiado da página do projeto:

 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 você pode ver, é muito fácil trabalhar com ele.

Eu sei que é um pouco tarde, mas acaba de encontrar uma biblioteca Microsoft.VisualBasic.FileIO que tem class TextFieldParser para processar arquivos csv.

Se você precisa apenas ler arquivos csv, então eu recomendo esta biblioteca: A Fast CSV Reader
Se você também precisa gerar arquivos csv, use este: FileHelpers

Ambos são gratuitos e opensource.

Aqui está uma class auxiliar que eu uso com freqüência, no caso de alguém voltar a esse tópico (eu queria compartilhá-lo).

Eu uso isso pela simplicidade de portá-lo para projetos prontos para uso:

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

E usá-lo 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; } 

[Ajudante de csv atualizado: bug corrigido onde o último caractere de nova linha criou uma nova linha]

Esta solução está usando o assembly oficial Microsoft.VisualBasic para analisar o CSV.

Vantagens:

  • delimitador escapando
  • ignora o header
  • espaços de guarnição
  • ignorar comentários

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

Eu escrevi o TinyCsvParser for .NET, que é um dos analisadores mais rápidos do .NET e altamente configurável para analisar quase qualquer formato CSV.

É lançado sob licença MIT:

Você pode usar o NuGet para instalá-lo. Execute o seguinte comando no Console do Gerenciador de Pacotes .

 PM> Install-Package TinyCsvParser 

Uso

Imagine que temos uma lista de pessoas em um arquivo CSV persons.csv com seu primeiro nome, sobrenome e data de nascimento.

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

O modelo de domínio correspondente em nosso sistema pode se parecer com isso.

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

Ao usar o TinyCsvParser, você precisa definir o mapeamento entre as colunas nos dados CSV e a propriedade em seu modelo de domínio.

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

E então podemos usar o mapeamento para analisar os dados CSV com um 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); } } } 

Guia de usuario

Um Guia do Usuário completo está disponível em:

Não há nenhuma maneira oficial que eu conheça, mas você deve usar bibliotecas existentes. Aqui está uma que eu achei muito útil do CodeProject:

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

Aqui está a minha implementação do 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; } } 

Algum tempo atrás eu tinha escrito class simples para CSV leitura / gravação com base na biblioteca Microsoft.VisualBasic . Usando esta class simples, você poderá trabalhar com o CSV como com o array de 2 dimensões. Você pode encontrar minha class pelo seguinte link: https://github.com/ukushu/DataExporter

Exemplo simples 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 leitura de header só você precisa é ler csv.Rows[0] células 🙂

Baseado no post do unlimit em Como dividir corretamente um CSV usando a function C # split ()? :

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

OBSERVAÇÃO: isso não trata de vírgulas com escape / aninhadas, etc. e, portanto, é adequado apenas para determinadas listas de CSV simples.

Este código lê csv para 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; } 

Outro para essa lista, o Cinchoo ETL – uma biblioteca de código aberto para ler e gravar vários formatos de arquivo (CSV, arquivo simples, Xml, JSON etc)

A amostra abaixo mostra como ler o arquivo CSV rapidamente (não é necessário um object 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); } } } 

A amostra abaixo mostra como ler o arquivo CSV usando o object 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); } } } 

Por favor, confira artigos no CodeProject sobre como usá-lo.

Solução de arquivo de origem única para necessidades simples de análise, útil. Lida com todos os casos desagradáveis. Como nova normalização de linha e manipulação de novas linhas em literais de string entre aspas. Não há de quê!

Se o arquivo CSV tiver um header, você apenas lerá os nomes das colunas (e calculará os índices das colunas) da primeira linha. Simples assim.

Observe que Dump é um método LINQPad, você pode querer removê-lo se não estiver usando o 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; } }