Importer un fichier CSV dans une structure de données fortement typée dans .Net [fermé]

106

Quelle est la meilleure façon d'importer un fichier CSV dans une structure de données fortement typée?

MattH
la source
Consultez la bibliothèque Open Source FileHelpers .
NotMyself
Ceci est un double de stackoverflow.com/questions/1103495/…
Mark Meuer
7
Étant donné que cela a été créé un an plus tôt que 1103495, je pense que cette question est un double de celle-ci.
MattH
2
Merci, Matt. J'essayais juste de les relier, pas d'indiquer lequel est venu en premier. Vous verrez que j'ai exactement le même texte sur l'autre question qui pointe vers celle-ci. Y a-t-il une meilleure façon de lier deux questions ensemble?
Mark Meuer

Réponses:

74

TextFieldParser de Microsoft est stable et suit la RFC 4180 pour les fichiers CSV. Ne soyez pas rebuté par l' Microsoft.VisualBasicespace de noms; c'est un composant standard dans le .NET Framework, ajoutez simplement une référence à l' Microsoft.VisualBasicassembly global .

Si vous compilez pour Windows (par opposition à Mono) et que vous ne prévoyez pas d'analyser des fichiers CSV «cassés» (non conformes à la RFC), alors ce serait le choix évident, car il est gratuit, sans restriction, stable, et activement pris en charge, dont la plupart ne peut pas être dit pour FileHelpers.

Voir aussi: Comment: lire des fichiers texte délimités par des virgules dans Visual Basic pour un exemple de code VB.

MarkJ
la source
2
Il n'y a en fait rien de spécifique à VB à propos de cette classe autre que son espace de noms malheureusement nommé. Je choisirais certainement cette bibliothèque si je n'avais besoin que d'un analyseur CSV «simple», car il n'y a rien à télécharger, distribuer ou à craindre en général. À cette fin, j'ai édité la formulation axée sur le VB de cette réponse.
Aaronaught
@Aaronaught Je pense que vos modifications sont principalement une amélioration. Bien que cette RFC ne fasse pas nécessairement autorité, comme de nombreux rédacteurs CSV ne la respectent pas, par exemple, Excel n'utilise pas toujours une virgule dans les fichiers "CSV". Ma réponse précédente ne disait-elle pas non plus que la classe pouvait être utilisée à partir de C #?
MarkJ
La TextFieldParservolonté de travail délimité par des tabulations et d' autres étranges cruft généré Excel aussi. Je me rends compte que votre réponse précédente ne prétendait pas que la bibliothèque était spécifique à VB, elle m'est juste apparue comme impliquant qu'elle était vraiment destinée à VB, et non destinée à être utilisée à partir de C #, ce que je ne pense pas le cas - il existe des classes vraiment utiles dans MSVB.
Aaronaught
21

Utilisez une connexion 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
la source
Cela nécessite un accès au système de fichiers. Pour autant que je sache, il n'y a aucun moyen de faire fonctionner OLEDB avec des flux en mémoire :(
UserControl
3
@UserControl, bien sûr, il nécessite un accès au système de fichiers. Il a posé une question sur l'importation d'un fichier CSV
Kevin
1
Je ne me plains pas. En fait, je préférerais la solution OLEDB au reste, mais j'étais frustré tant de fois lorsque je devais analyser le CSV dans les applications ASP.NET, je voulais donc le noter.
UserControl
12

Si vous vous attendez à des scénarios assez complexes pour l'analyse CSV, ne pensez même pas à lancer notre propre analyseur . Il existe de nombreux excellents outils, comme FileHelpers , ou même ceux de CodeProject .

Le fait est que c'est un problème assez courant et vous pouvez parier que de nombreux développeurs de logiciels ont déjà pensé et résolu ce problème.

Jon Limjap
la source
Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien pour référence. Les réponses aux liens uniquement peuvent devenir invalides si la page liée change. - De l'avis
techspider
Merci @techspider J'espère que vous avez noté que cet article provenait de la période bêta de StackOverflow: D Cela étant dit, de nos jours, les outils CSV proviennent mieux des packages Nuget - donc je ne suis pas sûr que même les réponses de lien soient à l'abri de 8 ans -anciens cycles d'évolution de la technologie
Jon Limjap
9

Brian donne une belle solution pour le convertir en une collection fortement typée.

La plupart des méthodes d'analyse CSV données ne prennent pas en compte les champs d'échappement ou certaines des autres subtilités des fichiers CSV (comme le découpage des champs). Voici le code que j'utilise personnellement. C'est un peu rugueux sur les bords et il n'y a pratiquement pas de rapport d'erreur.

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

Notez que cela ne gère pas la casse de bord des champs qui ne sont pas délimités par des guillemets doubles, mais meerley ayant une chaîne entre guillemets à l'intérieur. Voir cet article pour une meilleure explication ainsi que des liens vers certaines bibliothèques appropriées.

ICR
la source
9

Je suis d'accord avec @ NotMyself . FileHelpers est bien testé et gère toutes sortes de cas que vous devrez éventuellement gérer si vous le faites vous-même. Jetez un œil à ce que fait FileHelpers et n'écrivez les vôtres que si vous êtes absolument sûr que (1) vous n'aurez jamais besoin de gérer les cas extrêmes que FileHelpers fait, ou (2) vous aimez écrire ce genre de choses et allez soyez ravi lorsque vous devez analyser des choses comme celle-ci:

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

2, «Drake», «O'Malley», «Concierge,

Oups, je ne suis pas cité et je suis sur une nouvelle ligne!

Jon Galloway
la source
6

Je m'ennuyais donc j'ai modifié certaines choses que j'ai écrites. Il essaie d'encapsuler l'analyse d'une manière OO tout en réduisant le nombre d'itérations dans le fichier, il n'itère qu'une fois en haut pour chacun.

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
la source
2

Un bon moyen simple de le faire est d'ouvrir le fichier et de lire chaque ligne dans un tableau, une liste chaînée, une structure de données de votre choix. Soyez prudent lorsque vous manipulez la première ligne.

C'est peut-être au-dessus de votre tête, mais il semble y avoir un moyen direct d'y accéder également en utilisant une chaîne de connexion .

Pourquoi ne pas essayer d'utiliser Python au lieu de C # ou VB? Il a un joli module CSV à importer qui fait tout le gros du travail pour vous.

bonjourandre
la source
1
Ne passez pas à python depuis VB pour le plaisir d'un analyseur CSV. Il y en a un dans VB. Bien que bizarrement, il semble avoir été ignoré dans les réponses à cette question. msdn.microsoft.com/en-us/library/...
MarkJ
1

J'ai dû utiliser un analyseur CSV en .NET pour un projet cet été et me suis installé sur le pilote Microsoft Jet Text. Vous spécifiez un dossier à l'aide d'une chaîne de connexion, puis interrogez un fichier à l'aide d'une instruction SQL Select. Vous pouvez spécifier des types forts à l'aide d'un fichier schema.ini. Je ne l'ai pas fait au début, mais j'obtenais de mauvais résultats là où le type de données n'était pas immédiatement apparent, comme les numéros IP ou une entrée comme "XYQ 3.9 SP1".

Une limitation que j'ai rencontrée est qu'il ne peut pas gérer les noms de colonnes au-dessus de 64 caractères; il tronque. Cela ne devrait pas être un problème, sauf que j'avais affaire à des données d'entrée très mal conçues. Il renvoie un DataSet ADO.NET.

C'était la meilleure solution que j'ai trouvée. Je me méfierais de lancer mon propre analyseur CSV, car je manquerais probablement certains des cas de fin, et je n'ai trouvé aucun autre package d'analyse CSV gratuit pour .NET.

EDIT: De plus, il ne peut y avoir qu'un seul fichier schema.ini par répertoire, je l'ai donc ajouté dynamiquement pour taper fortement les colonnes nécessaires. Il ne tapera fortement que les colonnes spécifiées et en déduira pour tout champ non spécifié. J'ai vraiment apprécié cela, car je travaillais à importer un fichier CSV fluide de plus de 70 colonnes et je ne voulais pas spécifier chaque colonne, uniquement celles qui se comportaient mal.

pbh101
la source
Pourquoi pas l'analyseur CSV intégré à VB.NET? msdn.microsoft.com/en-us/library/...
MarkJ
1

J'ai tapé du code. Le résultat dans le datagridviewer semblait bon. Il analyse une seule ligne de texte en une liste d'objets.

    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
la source
0

Si vous pouvez garantir qu'il n'y a pas de virgule dans les données, le moyen le plus simple serait probablement d'utiliser String.split .

Par exemple:

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

Il existe peut-être des bibliothèques que vous pourriez utiliser pour vous aider, mais c'est probablement aussi simple que possible. Assurez-vous simplement que vous ne pouvez pas avoir de virgules dans les données, sinon vous devrez mieux les analyser.

Mike Stone
la source
ce n'est pas une solution optimale
roundcrisis
très mauvais sur l'utilisation de la mémoire et beaucoup de frais généraux. Petit devrait être moins grâce à quelques kilo-octets. Certainement pas bon pour un csv de 10 Mo!
Piotr Kula
Cela dépend de la taille de votre mémoire et du fichier.
tonymiao