Importar arquivo CSV para estrutura de dados fortemente tipada em .Net [fechado]

106

Qual é a melhor maneira de importar um arquivo CSV para uma estrutura de dados fortemente tipada?

MattH
fonte
Esta é uma duplicata de stackoverflow.com/questions/1103495/…
Mark Meuer
7
Considerando que esta pergunta foi criada um ano antes de 1103495, acho que essa pergunta é uma duplicata desta.
MattH
2
Obrigado, Matt. Eu estava apenas tentando uni-los, não indicar qual veio primeiro. Você verá que tenho exatamente o mesmo texto na outra pergunta que aponta para esta. Existe uma maneira melhor de unir duas perguntas?
Mark Meuer de

Respostas:

74

O TextFieldParser da Microsoft é estável e segue a RFC 4180 para arquivos CSV. Não desanime pelo Microsoft.VisualBasicnamespace; é um componente padrão no .NET Framework, basta adicionar uma referência ao Microsoft.VisualBasicconjunto global .

Se você está compilando para Windows (em oposição ao Mono) e não prevê ter que analisar arquivos CSV "quebrados" (não compatíveis com RFC), então esta seria a escolha óbvia, pois é gratuito, irrestrito, estável, e ativamente apoiado, a maioria dos quais não pode ser dito para FileHelpers.

Consulte também: How to: Read From Comma-Delimited Text Files in Visual Basic for a VB code example.

MarkJ
fonte
2
Na verdade, não há nada específico do VB sobre essa classe além de seu namespace infelizmente nomeado. Definitivamente, eu escolheria esta biblioteca se só precisasse de um analisador CSV "simples", porque não há nada para baixar, distribuir ou se preocupar em geral. Para esse fim, editei a formulação com foco em VB dessa resposta.
Aaronaught,
@Aaronaught Acho que suas edições são principalmente uma melhoria. Embora esse RFC não seja necessariamente oficial, já que muitos escritores de CSV não o cumprem, por exemplo, o Excel nem sempre usa uma vírgula em arquivos "CSV". Além disso, minha resposta anterior já não dizia que a classe poderia ser usada em C #?
MarkJ de
O TextFieldParserfuncionará para delimitado por tabulação e outros dados estranhos gerados em Excel também. Eu percebo que a sua resposta anterior não estava afirmando que a biblioteca foi específico-VB, ele só veio até mim como implicando que foi realmente significou para VB, e não destina a ser usado em C #, que eu não acho que é o caso - existem algumas classes realmente úteis no MSVB.
Aaronaught,
21

Use uma conexão OleDB.

String sConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\InputDirectory\\;Extended Properties='text;HDR=Yes;FMT=Delimited'";
OleDbConnection objConn = new OleDbConnection(sConnectionString);
objConn.Open();
DataTable dt = new DataTable();
OleDbCommand objCmdSelect = new OleDbCommand("SELECT * FROM file.csv", objConn);
OleDbDataAdapter objAdapter1 = new OleDbDataAdapter();
objAdapter1.SelectCommand = objCmdSelect;
objAdapter1.Fill(dt);
objConn.Close();
Kevin
fonte
Isso requer acesso ao sistema de arquivos. Pelo que eu sei, não há como fazer o OLEDB funcionar com streams na memória :(
UserControl,
3
@UserControl, é claro que requer acesso ao sistema de arquivos. Ele perguntou sobre a importação de um arquivo CSV
Kevin,
1
Eu não estou reclamando. Na verdade, eu preferiria a solução OLEDB em vez do resto, mas fiquei frustrado muitas vezes quando precisei analisar CSV em aplicativos ASP.NET e quis anotar isso.
UserControl
12

Se você está esperando cenários bastante complexos para a análise CSV, nem pense em lançar nosso próprio analisador . Existem muitas ferramentas excelentes por aí, como FileHelpers , ou mesmo algumas do CodeProject .

A questão é que este é um problema bastante comum e você pode apostar que muitos desenvolvedores de software já pensaram e resolveram esse problema.

Jon Limjap
fonte
Embora este link possa responder à pergunta, é melhor incluir as partes essenciais da resposta aqui e fornecer o link para referência. As respostas somente com link podem se tornar inválidas se a página vinculada mudar. - Da avaliação
techspider
Obrigado @techspider, espero que você tenha notado que este post foi do período beta do StackOverflow: D Dito isso, hoje em dia as ferramentas CSV são melhor obtidas a partir de pacotes Nuget - então não tenho certeza se mesmo as respostas dos links são imunes a 8 anos - antigos ciclos de evolução da tecnologia
Jon Limjap,
9

Brian oferece uma boa solução para convertê-lo em uma coleção fortemente tipada.

A maioria dos métodos de análise CSV fornecidos não leva em conta os campos de escape ou algumas das outras sutilezas dos arquivos CSV (como campos de corte). Aqui está o código que eu uso pessoalmente. É um pouco áspero nas bordas e praticamente não tem relatórios de erros.

public static IList<IList<string>> Parse(string content)
{
    IList<IList<string>> records = new List<IList<string>>();

    StringReader stringReader = new StringReader(content);

    bool inQoutedString = false;
    IList<string> record = new List<string>();
    StringBuilder fieldBuilder = new StringBuilder();
    while (stringReader.Peek() != -1)
    {
        char readChar = (char)stringReader.Read();

        if (readChar == '\n' || (readChar == '\r' && stringReader.Peek() == '\n'))
        {
            // If it's a \r\n combo consume the \n part and throw it away.
            if (readChar == '\r')
            {
                stringReader.Read();
            }

            if (inQoutedString)
            {
                if (readChar == '\r')
                {
                    fieldBuilder.Append('\r');
                }
                fieldBuilder.Append('\n');
            }
            else
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();

                records.Add(record);
                record = new List<string>();

                inQoutedString = false;
            }
        }
        else if (fieldBuilder.Length == 0 && !inQoutedString)
        {
            if (char.IsWhiteSpace(readChar))
            {
                // Ignore leading whitespace
            }
            else if (readChar == '"')
            {
                inQoutedString = true;
            }
            else if (readChar == ',')
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();
            }
            else
            {
                fieldBuilder.Append(readChar);
            }
        }
        else if (readChar == ',')
        {
            if (inQoutedString)
            {
                fieldBuilder.Append(',');
            }
            else
            {
                record.Add(fieldBuilder.ToString().TrimEnd());
                fieldBuilder = new StringBuilder();
            }
        }
        else if (readChar == '"')
        {
            if (inQoutedString)
            {
                if (stringReader.Peek() == '"')
                {
                    stringReader.Read();
                    fieldBuilder.Append('"');
                }
                else
                {
                    inQoutedString = false;
                }
            }
            else
            {
                fieldBuilder.Append(readChar);
            }
        }
        else
        {
            fieldBuilder.Append(readChar);
        }
    }
    record.Add(fieldBuilder.ToString().TrimEnd());
    records.Add(record);

    return records;
}

Observe que isso não lida com o caso extremo de campos não serem delimitados por aspas duplas, mas apenas com uma string entre aspas. Veja este post para uma melhor expansão, bem como alguns links para algumas bibliotecas adequadas.

ICR
fonte
9

Eu concordo com @NotMyself . FileHelpers é bem testado e lida com todos os tipos de casos extremos com os quais você eventualmente terá que lidar se fizer isso sozinho. Dê uma olhada no que o FileHelpers faz e apenas escreva o seu próprio se você tiver certeza absoluta de que (1) você nunca precisará lidar com os casos extremos que o FileHelpers faz, ou (2) você adora escrever esse tipo de coisa e vai fique muito feliz quando tiver que analisar coisas como esta:

1, "Bill", "Smith", "Supervisor", "Sem comentários"

2, 'Drake,', 'O'Malley', "Zelador,

Ops, não fui citado e estou em uma nova linha!

Jon Galloway
fonte
6

Eu estava entediado, então modifiquei algumas coisas que escrevi. Ele tenta encapsular a análise de uma maneira OO enquanto corta a quantidade de iterações no arquivo, itera apenas uma vez no primeiro foreach.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.IO;

namespace ConsoleApplication1
{
    class Program
    {

        static void Main(string[] args)
        {

            // usage:

            // note this wont run as getting streams is not Implemented

            // but will get you started

            CSVFileParser fileParser = new CSVFileParser();

            // TO Do:  configure fileparser

            PersonParser personParser = new PersonParser(fileParser);

            List<Person> persons = new List<Person>();
            // if the file is large and there is a good way to limit
            // without having to reparse the whole file you can use a 
            // linq query if you desire
            foreach (Person person in personParser.GetPersons())
            {
                persons.Add(person);
            }

            // now we have a list of Person objects
        }
    }

    public abstract  class CSVParser 
    {

        protected String[] deliniators = { "," };

        protected internal IEnumerable<String[]> GetRecords()
        {

            Stream stream = GetStream();
            StreamReader reader = new StreamReader(stream);

            String[] aRecord;
            while (!reader.EndOfStream)
            {
                  aRecord = reader.ReadLine().Split(deliniators,
                   StringSplitOptions.None);

                yield return aRecord;
            }

        }

        protected abstract Stream GetStream(); 

    }

    public class CSVFileParser : CSVParser
    {
        // to do: add logic to get a stream from a file

        protected override Stream GetStream()
        {
            throw new NotImplementedException();
        } 
    }

    public class CSVWebParser : CSVParser
    {
        // to do: add logic to get a stream from a web request

        protected override Stream GetStream()
        {
            throw new NotImplementedException();
        }
    }

    public class Person
    {
        public String Name { get; set; }
        public String Address { get; set; }
        public DateTime DOB { get; set; }
    }

    public class PersonParser 
    {

        public PersonParser(CSVParser parser)
        {
            this.Parser = parser;
        }

        public CSVParser Parser { get; set; }

        public  IEnumerable<Person> GetPersons()
        {
            foreach (String[] record in this.Parser.GetRecords())
            {
                yield return new Person()
                {
                    Name = record[0],
                    Address = record[1],
                    DOB = DateTime.Parse(record[2]),
                };
            }
        }
    }
}
Brian Leahy
fonte
2

Uma boa maneira simples de fazer isso é abrir o arquivo e ler cada linha em um array, lista encadeada, estrutura de dados de sua escolha. No entanto, tenha cuidado ao manusear a primeira linha.

Isso pode passar por cima da sua cabeça, mas parece haver uma maneira direta de acessá-los usando uma string de conexão .

Por que não tentar usar Python em vez de C # ou VB? Tem um bom módulo CSV para importar que faz todo o trabalho pesado para você.

helloandre
fonte
1
Não pule para python do VB por causa de um analisador CSV. Há um no VB. Embora estranhamente pareça ter sido ignorado nas respostas a esta pergunta. msdn.microsoft.com/en-us/library/…
MarkJ
1

Tive que usar um analisador CSV em .NET para um projeto neste verão e me conformei com o driver de texto Microsoft Jet. Você especifica uma pasta usando uma string de conexão e consulta um arquivo usando uma instrução SQL Select. Você pode especificar tipos fortes usando um arquivo schema.ini. Não fiz isso no início, mas depois estava obtendo resultados ruins em que o tipo de dados não era imediatamente aparente, como números de IP ou uma entrada como "XYQ 3.9 SP1".

Uma limitação que encontrei é que ele não pode lidar com nomes de colunas com mais de 64 caracteres; ele trunca. Isso não deve ser um problema, exceto que eu estava lidando com dados de entrada muito mal projetados. Ele retorna um ADO.NET DataSet.

Essa foi a melhor solução que encontrei. Eu desconfiaria de lançar meu próprio analisador CSV, já que provavelmente perderia alguns dos casos finais e não encontrei nenhum outro pacote de análise CSV gratuito para .NET por aí.

EDIT: Além disso, só pode haver um arquivo schema.ini por diretório, então eu anexei dinamicamente a ele para digitar fortemente as colunas necessárias. Ele só digitará fortemente as colunas especificadas e fará a inferência para qualquer campo não especificado. Eu realmente gostei disso, pois estava lidando com a importação de um CSV de coluna 70+ de fluido e não queria especificar cada coluna, apenas as que se comportavam mal.

pbh101
fonte
Por que não o VB.NET embutido no analisador CSV? msdn.microsoft.com/en-us/library/…
MarkJ
1

Eu digitei algum código. O resultado no datagridviewer parecia bom. Ele analisa uma única linha de texto para uma lista de arraylist de objetos.

    enum quotestatus
    {
        none,
        firstquote,
        secondquote
    }
    public static System.Collections.ArrayList Parse(string line,string delimiter)
    {        
        System.Collections.ArrayList ar = new System.Collections.ArrayList();
        StringBuilder field = new StringBuilder();
        quotestatus status = quotestatus.none;
        foreach (char ch in line.ToCharArray())
        {                                
            string chOmsch = "char";
            if (ch == Convert.ToChar(delimiter))
            {
                if (status== quotestatus.firstquote)
                {
                    chOmsch = "char";
                }                         
                else
                {
                    chOmsch = "delimiter";                    
                }                    
            }

            if (ch == Convert.ToChar(34))
            {
                chOmsch = "quotes";           
                if (status == quotestatus.firstquote)
                {
                    status = quotestatus.secondquote;
                }
                if (status == quotestatus.none )
                {
                    status = quotestatus.firstquote;
                }
            }

            switch (chOmsch)
            {
                case "char":
                    field.Append(ch);
                    break;
                case "delimiter":                        
                    ar.Add(field.ToString());
                    field.Clear();
                    break;
                case "quotes":
                    if (status==quotestatus.firstquote)
                    {
                        field.Clear();                            
                    }
                    if (status== quotestatus.secondquote)
                    {                                                                           
                            status =quotestatus.none;                                
                    }                    
                    break;
            }
        }
        if (field.Length != 0)            
        {
            ar.Add(field.ToString());                
        }           
        return ar;
    }
Pieter
fonte
0

Se você pode garantir que não há vírgulas nos dados, a maneira mais simples provavelmente seria usar String.split .

Por exemplo:

String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);

Pode haver bibliotecas que você possa usar para ajudar, mas provavelmente é o mais simples possível. Apenas certifique-se de que não haja vírgulas nos dados, caso contrário, você precisará analisá-los melhor.

Mike Stone
fonte
esta não é uma solução ideal
roundcrisis
muito ruim no uso de memória e muita sobrecarga. Pequeno deve ser menos, graças a alguns kilobytes. Definitivamente não é bom para um csv de 10 MB!
Piotr Kula
Depende do tamanho da sua memória e do arquivo.
tonymiao