Importa file CSV in una struttura dati fortemente tipizzata in .Net [chiuso]


106

Qual è il modo migliore per importare un file CSV in una struttura di dati fortemente tipizzata?




7
Considerando che è stato creato un anno prima del 1103495, penso che la domanda sia un duplicato di questa.
MattH

2
Grazie, Matt. Stavo solo cercando di collegarli insieme, non di indicare quale è venuto prima. Vedrai che ho esattamente lo stesso testo sull'altra domanda che punta a questa. C'è un modo migliore per legare insieme due domande?
Mark Meuer

Risposte:


74

TextFieldParser di Microsoft è stabile e segue RFC 4180 per i file CSV. Non lasciarti scoraggiare dallo Microsoft.VisualBasicspazio dei nomi; è un componente standard in .NET Framework, basta aggiungere un riferimento Microsoft.VisualBasicall'assembly globale .

Se stai compilando per Windows (al contrario di Mono) e non prevedi di dover analizzare file CSV "danneggiati" (non conformi a RFC), questa sarebbe la scelta più ovvia, poiché è gratuito, senza restrizioni, stabile, e attivamente supportato, la maggior parte del quale non si può dire per FileHelpers.

Vedere anche: Procedura: leggere da file di testo delimitati da virgole in Visual Basic per un esempio di codice VB.


2
In realtà non c'è nulla di specifico di VB su questa classe oltre al suo spazio dei nomi sfortunatamente chiamato. Sceglierei sicuramente questa libreria se avessi solo bisogno di un "semplice" parser CSV, perché non c'è nulla da scaricare, distribuire o preoccuparsi in generale. A tal fine ho modificato il fraseggio incentrato su VB da questa risposta.
Aaronaught

@Aaronaught Penso che le tue modifiche siano principalmente un miglioramento. Sebbene tale RFC non sia necessariamente autorevole, poiché molti scrittori CSV non lo rispettano, ad esempio Excel non usa sempre una virgola nei file "CSV". Inoltre, la mia risposta precedente non diceva già che la classe potrebbe essere utilizzata da C #?
MarkJ

Il TextFieldParserlavoro di volontà di strano cruft Excel-generated delimitato da tabulazioni e altri troppo. Mi rendo conto che la tua risposta precedente non affermava che la libreria fosse specifica per VB, mi è sembrato semplicemente di suggerire che fosse davvero pensata per VB e non intesa per essere utilizzata da C #, che non credo sia il caso - ci sono alcune classi davvero utili in MSVB.
Aaronaught

21

Usa una connessione 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();

Ciò richiede l'accesso al file system. Per quanto ne so, non c'è modo di far funzionare OLEDB con flussi in memoria :(
UserControl

3
@UserControl, ovviamente richiede l'accesso al file system. Ha chiesto informazioni sull'importazione di un file CSV
Kevin

1
Non mi sto lamentando. In effetti preferirei la soluzione OLEDB rispetto al resto, ma sono stato frustrato così tante volte quando era necessario analizzare CSV nelle applicazioni ASP.NET, quindi volevo prenderne nota.
UserControl

12

Se ti aspetti scenari abbastanza complessi per l'analisi CSV, non pensare nemmeno a rotolare il nostro parser . Ci sono molti ottimi strumenti là fuori, come FileHelpers , o anche quelli di CodeProject .

Il punto è che questo è un problema abbastanza comune e potresti scommettere che molti sviluppatori di software hanno già pensato e risolto questo problema.


Sebbene questo collegamento possa rispondere alla domanda, è meglio includere le parti essenziali della risposta qui e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia. - Dalla recensione
techspider

Grazie @techspider Spero che tu abbia notato che questo post era del periodo beta di StackOverflow: D Detto questo, oggigiorno gli strumenti CSV sono meglio forniti dai pacchetti Nuget, quindi non sono sicuro che anche le risposte ai link siano immuni da 8 anni -vecchi cicli evolutivi della tecnologia
Jon Limjap

9

Brian offre una buona soluzione per convertirlo in una raccolta fortemente tipizzata.

La maggior parte dei metodi di analisi CSV forniti non tiene conto dei campi di escape o di alcune altre sottigliezze dei file CSV (come i campi di ritaglio). Ecco il codice che uso personalmente. È un po 'approssimativo e non ha praticamente alcun rapporto di errore.

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

Si noti che questo non gestisce il caso limite dei campi che non vengono delimitati da virgolette doppie, ma che contengono una stringa tra virgolette. Vedi questo post per un'espansione un po 'migliore e alcuni link ad alcune librerie appropriate.


9

Sono d'accordo con @ NotMyself . FileHelpers è ben testato e gestisce tutti i tipi di casi limite che alla fine dovrai affrontare se lo fai da solo. Dai un'occhiata a ciò che fa FileHelpers e scrivi il tuo solo se sei assolutamente sicuro che (1) non avrai mai bisogno di gestire i casi limite di FileHelpers, o (2) ami scrivere questo tipo di cose e lo farai essere felicissimo quando devi analizzare cose come questa:

1, "Bill", "Smith", "Supervisor", "No Comment"

2, "Drake", "O'Malley", "Janitor,

Oops, non sono citato e sono su una nuova riga!


6

Ero annoiato, quindi ho modificato alcune cose che avevo scritto. Cerca di incapsulare l'analisi in un modo OO riducendo la quantità di iterazioni attraverso il file, itera solo una volta all'inizio per ogni.

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


2

Un buon modo semplice per farlo è aprire il file e leggere ogni riga in un array, in un elenco collegato, in una struttura dati a scelta. Fai attenzione però a gestire la prima riga.

Questo potrebbe essere sopra la tua testa, ma sembra esserci un modo diretto per accedervi anche utilizzando una stringa di connessione .

Perché non provare a utilizzare Python invece di C # o VB? Ha un bel modulo CSV da importare che fa tutto il lavoro pesante per te.


1
Non saltare a Python da VB per il bene di un parser CSV. Ce n'è uno in VB. Anche se stranamente sembra essere stato ignorato nelle risposte a questa domanda. msdn.microsoft.com/en-us/library/…
MarkJ

1

Ho dovuto utilizzare un parser CSV in .NET per un progetto quest'estate e ho optato per il driver Microsoft Jet Text. Specificare una cartella utilizzando una stringa di connessione, quindi interrogare un file utilizzando un'istruzione SQL Select. È possibile specificare tipi forti utilizzando un file schema.ini. All'inizio non l'ho fatto, ma poi ho ottenuto cattivi risultati in cui il tipo di dati non era immediatamente evidente, come i numeri IP o una voce come "XYQ 3.9 SP1".

Una limitazione in cui mi sono imbattuto è che non può gestire nomi di colonne superiori a 64 caratteri; tronca. Questo non dovrebbe essere un problema, tranne per il fatto che avevo a che fare con dati di input progettati molto male. Restituisce un DataSet ADO.NET.

Questa è stata la migliore soluzione che ho trovato. Sarei cauto nel lanciare il mio parser CSV, poiché probabilmente mi perderei alcuni dei casi finali e non ho trovato altri pacchetti di analisi CSV gratuiti per .NET là fuori.

EDIT: Inoltre, può esserci solo un file schema.ini per directory, quindi l'ho aggiunto dinamicamente per digitare con forza le colonne necessarie. Digiterà solo fortemente le colonne specificate e dedurrà per qualsiasi campo non specificato. L'ho apprezzato molto, poiché avevo a che fare con l'importazione di un CSV fluido di oltre 70 colonne e non volevo specificare ogni colonna, solo quelle che si comportano male.


Perché non il VB.NET integrato nel parser CSV? msdn.microsoft.com/en-us/library/…
MarkJ

1

Ho digitato un codice. Il risultato nel datagridviewer sembrava buono. Analizza una singola riga di testo in un elenco di oggetti.

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

0

Se puoi garantire che non ci siano virgole nei dati, il modo più semplice sarebbe probabilmente usare String.split .

Per esempio:

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

Potrebbero esserci librerie che potresti usare per aiutare, ma probabilmente è il più semplice che puoi ottenere. Assicurati solo di non poter avere virgole nei dati, altrimenti dovrai analizzarli meglio.


questa non è una soluzione ottimale
roundcrisis

pessimo sull'utilizzo della memoria e molto sovraccarico. Piccolo dovrebbe essere meno grazie a pochi kilobyte. Sicuramente non va bene per un CSV da 10 MB!
Piotr Kula

Dipende dalle dimensioni della memoria e del file.
tonymiao
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.