Importar archivo CSV a una estructura de datos fuertemente tipada en .Net [cerrado]

106

¿Cuál es la mejor manera de importar un archivo CSV en una estructura de datos fuertemente tipada?

MattH
fuente
Este es un duplicado de stackoverflow.com/questions/1103495/…
Mark Meuer
7
Teniendo en cuenta que esto fue creado un año antes que 1103495, creo que esa pregunta es un duplicado de esta.
MattH
2
Gracias, Matt. Solo estaba tratando de vincularlos, no indicar cuál vino primero. Verá que tengo exactamente el mismo texto en la otra pregunta que apunta a esta. ¿Hay una mejor manera de unir dos preguntas?
Mark Meuer

Respuestas:

74

TextFieldParser de Microsoft es estable y sigue RFC 4180 para archivos CSV. No se deje intimidar por el Microsoft.VisualBasicespacio de nombres; es un componente estándar en .NET Framework, solo agregue una referencia al Microsoft.VisualBasicensamblado global .

Si está compilando para Windows (a diferencia de Mono) y no anticipa tener que analizar archivos CSV "rotos" (no compatibles con RFC), entonces esta sería la opción obvia, ya que es gratis, sin restricciones, estable, y con soporte activo, la mayoría de los cuales no se pueden decir de FileHelpers.

Consulte también: Cómo: Leer archivos de texto delimitados por comas en Visual Basic para ver un ejemplo de código VB.

MarkJ
fuente
2
En realidad, no hay nada específico de VB sobre esta clase que no sea su espacio de nombres desafortunadamente llamado. Definitivamente elegiría esta biblioteca si solo necesitara un analizador CSV "simple", porque no hay nada que descargar, distribuir o de qué preocuparse en general. Con ese fin, he editado la redacción centrada en VB de esta respuesta.
Aaronaught
@Aaronaught Creo que tus ediciones son principalmente una mejora. Aunque ese RFC no es necesariamente autorizado, ya que muchos escritores de CSV no lo cumplen, por ejemplo, Excel no siempre usa una coma en los archivos "CSV". Además, ¿mi respuesta anterior ya no decía que la clase podría usarse desde C #?
MarkJ
El TextFieldParsertambién funcionará para archivos cruft delimitados por tabuladores y otros extraños generados por Excel. Me doy cuenta de que su respuesta anterior no afirmaba que la biblioteca fuera específica de VB, simplemente me pareció que implicaba que realmente estaba destinada a VB y no estaba destinada a ser utilizada desde C #, lo que no creo que sea el caso - hay algunas clases realmente útiles en MSVB.
Aaronaught
21

Utilice una conexión 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
fuente
Esto requiere acceso al sistema de archivos. Hasta donde yo sé, no hay forma de hacer que OLEDB funcione con flujos en memoria :(
UserControl
3
@UserControl, por supuesto, requiere acceso al sistema de archivos. Preguntó sobre la importación de un archivo CSV
Kevin
1
No me estoy quejando. De hecho, preferiría la solución OLEDB sobre el resto, pero me sentí frustrado tantas veces cuando necesité analizar CSV en aplicaciones ASP.NET, así que quería notarlo.
UserControl
12

Si espera escenarios bastante complejos para el análisis de CSV, ni siquiera piense en ejecutar nuestro propio analizador . Hay muchas herramientas excelentes, como FileHelpers o incluso las de CodeProject .

El punto es que este es un problema bastante común y podría apostar a que muchos desarrolladores de software ya han pensado y resuelto este problema.

Jon Limjap
fuente
Si bien este enlace puede responder a la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si cambia la página enlazada. - De la crítica
techspider
Gracias @techspider. -antiguos ciclos de evolución de la tecnología
Jon Limjap
9

Brian ofrece una buena solución para convertirlo en una colección fuertemente tipada.

La mayoría de los métodos de análisis de CSV proporcionados no tienen en cuenta los campos de escape o algunas de las otras sutilezas de los archivos CSV (como los campos de recorte). Aquí está el código que utilizo personalmente. Es un poco tosco en los bordes y prácticamente no tiene informes de errores.

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

Tenga en cuenta que esto no maneja el caso límite de los campos que no están delimitados por comillas dobles, sino que simplemente tienen una cadena entre comillas dentro. Vea esta publicación para una mejor expansión, así como algunos enlaces a algunas bibliotecas adecuadas.

ICR
fuente
9

Estoy de acuerdo con @ NotMyself . FileHelpers está bien probado y maneja todo tipo de casos que eventualmente tendrá que lidiar si lo hace usted mismo. Eche un vistazo a lo que hace FileHelpers y solo escriba el suyo propio si está absolutamente seguro de que (1) nunca necesitará manejar los casos extremos que FileHelpers hace, o (2) le encanta escribir este tipo de cosas y va a regocíjese cuando tenga que analizar cosas como esta:

1, "Bill", "Smith", "Supervisor", "Sin comentarios"

2, 'Drake', 'O'Malley', "Conserje,

¡Vaya, no estoy cotizado y estoy en una nueva línea!

Jon Galloway
fuente
6

Estaba aburrido, así que modifiqué algunas cosas que escribí. Intenta encapsular el análisis sintáctico de manera orientada a objetos mientras reduce la cantidad de iteraciones a través del archivo, solo itera una vez en la parte superior de cada uno.

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
fuente
2

Una buena forma sencilla de hacerlo es abrir el archivo y leer cada línea en una matriz, lista vinculada, estructura de datos de su elección. Sin embargo, tenga cuidado con el manejo de la primera línea.

Esto puede estar por encima de su cabeza, pero parece haber una forma directa de acceder a ellos también usando una cadena de conexión .

¿Por qué no intentar usar Python en lugar de C # o VB? Tiene un buen módulo CSV para importar que hace todo el trabajo pesado por usted.

holaandre
fuente
1
No salte a Python desde VB por el bien de un analizador CSV. Hay uno en VB. Aunque extrañamente parece haber sido ignorado en las respuestas a esta pregunta. msdn.microsoft.com/en-us/library/…
MarkJ
1

Tuve que usar un analizador CSV en .NET para un proyecto este verano y me decidí por Microsoft Jet Text Driver. Usted especifica una carpeta usando una cadena de conexión, luego consulta un archivo usando una instrucción SQL Select. Puede especificar tipos seguros mediante un archivo schema.ini. No hice esto al principio, pero luego estaba obteniendo malos resultados donde el tipo de datos no era evidente de inmediato, como números de IP o una entrada como "XYQ 3.9 SP1".

Una limitación con la que me encontré es que no puede manejar nombres de columna de más de 64 caracteres; se trunca. Esto no debería ser un problema, excepto que estaba tratando con datos de entrada muy mal diseñados. Devuelve un conjunto de datos ADO.NET.

Esta fue la mejor solución que encontré. Sería cauteloso al usar mi propio analizador CSV, ya que probablemente perdería algunos de los casos finales, y no encontré ningún otro paquete de análisis CSV gratuito para .NET por ahí.

EDITAR: Además, solo puede haber un archivo schema.ini por directorio, por lo que lo agregué dinámicamente para escribir firmemente las columnas necesarias. Solo escribirá fuertemente las columnas especificadas e inferirá para cualquier campo no especificado. Realmente aprecié esto, ya que estaba tratando con la importación de un CSV fluido de 70+ columnas y no quería especificar cada columna, solo las que se comportaban mal.

pbh101
fuente
¿Por qué no el analizador de CSV integrado en VB.NET? msdn.microsoft.com/en-us/library/…
MarkJ
1

Escribí un código. El resultado en el visor de cuadrícula de datos se veía bien. Analiza una sola línea de texto en una lista de matrices 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
fuente
0

Si puede garantizar que no haya comas en los datos, probablemente la forma más sencilla sería utilizar String.split .

Por ejemplo:

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

Es posible que haya bibliotecas que pueda utilizar para ayudar, pero probablemente sea lo más simple que puede conseguir. Solo asegúrese de no tener comas en los datos, de lo contrario tendrá que analizarlos mejor.

Mike Stone
fuente
esta no es una solución óptima
crisis redonda
muy malo en el uso de la memoria y mucha sobrecarga. Lo pequeño debería ser menos gracias a unos pocos kilobytes. ¡Definitivamente no es bueno para un csv de 10 MB!
Piotr Kula
Depende del tamaño de su memoria y del archivo.
tonymiao