jueves, 11 de agosto de 2011

CSV Parser para C#

Antes que nada, hay que definir que es el formato CSV que muchos archivos manejan:

Los ficheros CSV (del inglés comma-separated values) son un tipo de documento en formato abierto sencillo para representar datos en forma de tabla, en las que las columnas se separan por comas (o punto y coma en donde la coma es el separador decimal: España, Francia, Italia...) y las filas por saltos de línea. Los campos que contengan una coma, un salto de línea o una comilla doble deben ser encerrados entre comillas dobles.

El formato CSV es muy sencillo y no indica un juego de caracteres concreto, ni cómo van situados los bytes , ni el formato para el salto de línea. Estos puntos deben indicarse muchas veces al abrir el fichero, por ejemplo, con una hoja de cálculo .

Fuente: Wikipedia


Para el desarrollo de esta libreria se tomaron como base archivos .csv exportados de programas de calculo, como Excel. Así que si lo que deseas es importar a tu programa en C# algun archivo en .csv entregado por Excel para despues meterlo a algun control, como por ejemplo un DataGridView o un ListView, puer aqui tengo la solucion.

Ejemplo:
987,juan,87345,10 norte 342
876,pedro,43649,8 oriente 342
123,jorge,03342,av. libertad 23
69,vicente,61560,valencia nº183
18,lorenzo,06490,sol nº 18
19,lucía,06480,luna nº 8



Definiremos a continuacion las reglas que se deben seguir para la creacion y lectura de un archivo en formato CSV:


/// CSV files have a very simple structure:
    ///Each record is one line (with exceptions)
    ///Fields are separated with commas
    ///Leading and trailing space-characters adjacent to comma field separators are ignored
    ///Fields with embedded commas must be delimited with double-quote characters
    ///Fields that contain double quote characters must be surounded by double-quotes, and the embedded double-quotes must each be represented by a pair of consecutive double quotes.
    ///A field that contains embedded line-breaks must be surounded by double-quotes
    ///Fields with leading or trailing spaces must be delimited with double-quote characters
    ///Fields may always be delimited with double quotes
    ///The first record in a CSV file may be a header record containing column (field) names


En español tenemos:

  • Cada registro debe ir en una linea independiente
  • Cada uno de los campos es separado de otro campo por medio de una coma
  • El espacio en blanco o caracteres separadores entre el contenido de un campo y la coma delimitadora son ignorados
  • Campos que contienen comas en su interior deben ser encerrados entre comillas ("campo")
    • ejemplo: si un campo contiene : cosa1,cosa2
    • seria: "cosa1,cosa2",
  •  Campos que contengan comillas en su interior deben ser encerrados entre comillas, y las comillas dentro del campo mismo son cambiadas por dos comillas consecutivas
    • ejemplo; si un campo contiene: "cosa1"
    • seria:"""cosa1""",
  • Y asi tambien si contiene comillas y comas a la vez (el campo se encierra entre comillas y las comillas dentro del campo se cambian por dos comillas consecutivas):
    • ejemplo, si un campo contiene: "cosa1","cosa2",cosa3
    • seria:"""cosa1"",""cosa2"",cosa3"
  • Dice algo acerca de que la primera linea, o primer registro debe ser el que contenga las cabeceras con los titulos de cada campo, pero para este caso omitiremos esta regla


Por tanto vemos que es muy sencillo la creacion y lectura de los mismos, es una manera de organizar informacion que no requiere de tanta complejidad, pero a la vez se pierde eficacia, en especial en funciones de busqueda.

Por lo general se usan archivos .csv como un recurso simple a la hora de manejar bases de datos, haciendo que cada uno de los archivos .csv que se manejan sea una tabla de una base de datos mas extensa.

Para poder manipular archivos CSV es necesario parsearlos, es decir, obtener una linea del archivo, y separar esa linea en cada uno de los campos que compondran un registro, el conjunto de esos registros compondran la tabla, que es nuestro archivo .csv.

Por tanto, aqui proveo una libreria que te ayudara a manejar de manera sencilla cadenas y registros CSV, puedes descargar el .dll de aqui:

CSVParser.dll

Esta libreria se compone de funciones sencillas, son dos Metodos Estaticos:

//El primer Metodo es:
public static string CSV_WriteLinea(string[] registro)
//Por medio de este metodo puedes convertir un array string[] pasado de parametro a una linea string e formato CSV.
//Despues esa misma linea puedes escribirla en un archivo o usarla de la la manera que mas te convenga


//Y el segundo:        
public static string[] CSV_ParseLinea(string linea)
//Por medio de este metodo puedes parsear una linea string en formato CSV y pasarl cada uno de los campos dentro de la misma a un arrat de strings (string[]).
//Esto es muy util cuando lees una linea de un archivo .csv y deseas separar cada uno de los campos 







La manera mas sencilla de usar esta libreria, es agregandola a tu proyecto y despues mandar a llamar al metodo que vayas a usar, recordando que la clase CSVParser no posee constructor debido a que sus unicos dos metodos son estaticos

Aqui coloco un Tuto simple de como agregar la referencia de un archivo .dll a tu proyecto, RECUERDA QUE PARA PODER USAR LA LIBRERIA PRIMERO DEBES AGREGAR LA REFERENCIA DE LA MISMA A TU PROYECTO:

Para agregar la referencia en VS 2010:
  1. Menu Project / Add Reference
  2. Pestaña Browse
  3. Busca el archivo CSVParser.dll que acabas de descargar
  4. Listo, ahora solo falta usarla
Estos son algunos ejemplos de como usar la libreria:

Para convertir un array de strings donde cada elemento es un campo del registro a una sola linea string:

//Inicia un nuevo proyecto de WindowsForms, agrega la referrncia a la libreria y añade en el evento Form_Load el siguiente codigo:

string[] registro = new string[3];
registro[0] = "coco";
registro[1] = "chalo,io";
registro[2] = "\"feria\"carro\"chalada\"";

foreach (string campo in registro)
{
MessageBox.Show(campo);
}

MessageBox.Show(Parsers.CSVParsers.CSV_WriteLinea(registro));

//Este ejemplo mostrara 4 MessageBox consecutivos, los primeros 3 contendran los campos del registro
//el ultiomo contendra una linea en formato CVS,
//Despues de parsear el registro a una string en formato CSV puede usarse para colocarla dentro un archivo
//que colocandolo con extension .csv puede ser reconocido por Excel o casi cualquier procesador de Hojas de Calculo


Para convertir una linea en formato CSV a un array de strings:

//Inicia un nuevo proyecto de WindowsForms, agrega la referrncia a la libreria y añade en el evento Form_Load el siguiente codigo:
string linea = "bola1,bola,bola2,\"\"\"b\"\"ola\"\"7\"\"\",";
string[] registro = Parsers.CSVParsers.CSV_ParseLinea(linea);

foreach (string campo in registro)
{
MessageBox.Show(campo);
}
//Esto mostrara una serie de MessageBox consecutivos donde cada uno contendra un campo del registro


Aqui va un ejemplo donde se usan los dos al mismo tiempo:


//Inicia un nuevo proyecto de WindowsForms, añade la referencia a la libreria, y en el evento Form_Load agrega el siguiente codigo:

            string[] registro = new string[7];
            registro[0] = "gato";
            registro[1] = "\"perro\"";
            registro[2] = "cosa1,cosa3";
            registro[3] = "\"bola1\",\"boa2\"";
            registro[4] = "\"bolas,bolasitas\"";
            registro[5] = ",";
            registro[6] = "";

            string linea = Parsers.CSVParsers.CSV_WriteLinea(registro);

            string[] registro2 = Parsers.CSVParsers.CSV_ParseLinea(linea);

            foreach (string campos in registro2)
            {
                MessageBox.Show(campos);
            }

/*
El ejemplo anterior desplegará una seria de MessageBox donde cada una contendrá un campo del registro, el cual es el arreglo string[] que se creó al inicio.

*/






Coloco el codigo completo de la clase por si encuentran algun error que quisieran ayudar a solucionar

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;

/*
 * Creado por Hazael Fernando Mojica Garcia
 * San Nicolas de los Garza, Nuevo Leon, Mexico
 * 11/Agosto/2011
*/
namespace Parsers
{
    /// 
    /// Clase CSVParser con esta clase puedes convertir un array de string (string[]) donde cada elemento es un campo, en una sola cadena string en formato CSV
    /// Así como también una cadena string en formato CSV puedes pasarla a un array string[] donde cada elemento del arreglo es un campo
    /// 
    /// 
    /// CSV files have a very simple structure:
    ///Each record is one line (with exceptions)
    ///Fields are separated with commas
    ///Leading and trailing space-characters adjacent to comma field separators are ignored
    ///Fields with embedded commas must be delimited with double-quote characters
    ///Fields that contain double quote characters must be surounded by double-quotes, and the embedded double-quotes must each be represented by a pair of consecutive double quotes.
    ///A field that contains embedded line-breaks must be surounded by double-quotes
    ///Fields with leading or trailing spaces must be delimited with double-quote characters
    ///Fields may always be delimited with double quotes
    ///The first record in a CSV file may be a header record containing column (field) names
    /// 
    /// 
    public class CSVParsers
    {
        /*Metodo Publico que convierte un array de strings en una sola linea en formato CSV
 * Ejemplo:
 * Convierte el siguiente (arreglo string[] registro) a una linea en formato CSV (string linea)
 * string[] registro = new string[6];
 * registro[0] = "hola";                            -> hola
 * registro[1] = "mundo";                           -> mundo
 * registro[2] = "como estas";                      -> como, estas
 * registro[3] = "espero, que, bien";               -> espero, que, bien
 * registro[4] = "\"ES\" \"PERFECTO\"";             -> "Es" "PERFECTO"
 * registro[5] = "\"cosa1\",\"cosa2\",\"cosa3\"";   -> "cosa1","cosa2","cosa3"
 * string linea = CSV_WriteLinea(registro);
 * MessageBox.Show(linea);
 * --El mensaje mostraria: hola,mundo,"como, estas","espero, que, bien","""Es"" ""PERFECTO""","""cosa1"",""cosa2"",""cosa3"""
*/

        /// 
        /// Conviete el array de strings pasado de parametro a una sola linea en formato CSV
        /// 
        /// 
Array de strings a convertir/// 
        /// Ejemplo:
        /// 
        /// Convierte el siguiente (arreglo string[] registro) a una linea en formato CSV (string linea)
        /// string[] registro = new string[6];
        /// registro[0] = "hola";                            -> hola
        /// registro[1] = "mundo";                           -> mundo
        /// registro[2] = "como estas";                      -> como, estas
        /// registro[3] = "espero, que, bien";               -> espero, que, bien
        /// registro[4] = "\"ES\" \"PERFECTO\"";             -> "Es" "PERFECTO"
        /// registro[5] = "\"cosa1\",\"cosa2\",\"cosa3\"";   -> "cosa1","cosa2","cosa3"
        /// string linea = CSV_WriteLinea(registro); 
        /// 
        /// --El mensaje mostraria: hola,mundo,"como, estas","espero, que, bien","""Es"" ""PERFECTO""","""cosa1"",""cosa2"",""cosa3"""
        /// 
        /// Regresa una sola linea con formato CSV
        public static string CSV_WriteLinea(string[] registro)
        {
            string linea = "";
            string campo = "";

            for (int i = 0; i < registro.Length; i++)
            {
                campo = registro[i];
                if (VerificRegex(campo, ",") > 0 || VerificRegex(campo, "\"") > 0)
                {//si existe alguna coma o comilla dentro del campo, el campo debe llevar comillas
                    campo = Regex.Replace(campo, "\"", "\"\"");//Reemplazamos las comilas sencillas por dobles comillas
                    linea += "\"" + campo + "\",";//añadimos el campo a la linea colocando comas
                }
                else
                {//si no, el campo pasa igual solo que con una coma al final del mismo
                    linea += campo + ",";
                }
            }

            return linea;
        }

        /*Metodo publico estatico que parsea una linea CSV que posee comas y comillas dentro de sus campos
         * El procedimiento a seguir para parsearlas es: 
         * 1.- encontrar las dobles comillas y cambiarlas por tabulaciones
         *  *Se recorre de uno en uno todos los elementos de la linea identificando cuando comienza un campo con comillas
         *  *De encontrarse un campo que comienze con comillas, todos los elementos en medio de las comillas pasaran iguales al ArrayList auxiliar, con excepcion de las dobles comillas las cuales se cambiaran por comillas normales
         * 2.- Una vez teniendo dividida la linea en campos dentro del ArrayList, se convierte a un array string[]
        */

        //ejemplo (linea con formato .csv): cosa1,"""cosa2""",cosa3,"""cosa4""cosa5""cosa6""",cosa7
        //regresa una string[] con:
        //[0]= cosa1
        //[1]= "cosa2"
        //[2]= cosa3
        //[3]= "cosa4"cosa5"cosa6"
        //[4]= cosa7

        //ejemplo (linea con formato .csv): cosa1,"cosa2,cosa3",cosa4,"c,e,d",hola
        //regresa una string[] con:
        //[0]= cosa1
        //[1]= cosa2,cosa3
        //[2]= cosa4
        //[3]= c,e,d
        //[4]= hola


        /// 
        /// Regresa un array de strings en el cual cada elemento del arreglo es un campo del registro pasado como cadena string
        /// 
        /// 
Es una string que posee el formato CSV///     /// 
        /// Esto es un ejemplo, para pasar una linea leida de un archivo .csv a un arreglo string[]
        /// 
        /// string linea = archivo.ReadLine();
        /// string[] registro = CSVParser.CSV_ParseLinea(linea);
        /// 
        /// 
        /// Otro Ejemplo:
        /// 
        /// string linea="bola1,bola2,\"bola3,bolar4\",bola5,\"bola7 ,,,,\",,";
        /// string[] registro = CSV_ParseLinea(linea);   
        /// foreach (string campos in registro)
        /// {
        ///    MessageBox.Show(campos);
        /// }
        /// 
        /// Producira que se muestren de manera consecutiva varias MessageBox conteniendo cada una:
        /// bola1
        /// bola2
        /// bola3,bolar4
        /// bola5
        /// bola7 ,,,,
        /// en blanco
        /// en blanco
        /// 
        /// Array de strings el cual contiene en cada elemento del array el campo correspondiente de la linea CSV parseada
        public static string[] CSV_ParseLinea(string linea)
        {
            ArrayList lista = new ArrayList();//Lista dinamica que acumulará los registros
            bool concomilla = false;//indica si se encuentra en un campo que posee comillas (lo cual siginifica que tiene comillas como contenido de campo)
            string campo = "";//contendra el campo actual
            string caracter = "";
            int i = 0;
            char tab = Convert.ToChar(9);//Obtenemos una tabulacion Horizontal (codigo Ascii 9)
            string[] registro;

            //Si la linea contiene alguna comilla esto quiere decir que posee alguna coma o comilla embebida si no
            // se puede tratar como un CSV tipico del mas sencillo y se parsea por otro metodo mas rapidamente
            if (VerificRegex(linea, "\"") > 0)
            {
                //se hace una iteracion en cada uno de los elementos de la linea
                for (i = 0; i < linea.Length; i++)
                {
                    caracter = linea.Substring(i, 1);//se extrae un caracter

                    if (caracter == "\"")
                    {//si el caracter es una comilla
                        if (concomilla)
                        {//Estoy en medio de un campo que posee comillas y comas en su interior, por lo tanto si el caracter siguiente es otra comilla (juntas son una doble comilla) se cambia por una tabulacion
                            if (i == linea.Length - 1)
                            {//si es el ultimo caracter y estoy un concomillas quiere decir que es una comilla de fin de campo
                                lista.Add(campo);
                                concomilla = false;
                            }
                            else
                            {//si no es el ultimo caracter
                                if (linea.Substring(i + 1, 1) == "\"")
                                {
                                    campo += "\"";
                                    i++;//saltamos una iteracion del for, para que se brinque el siguiente caracter que es una comilla
                                }
                                else
                                {//si el siguiente caracter no es una comilla significa que termino el campo concomillas o es un caracter mas dentro de campo
                                    if (linea.Substring(i + 1, 1) == ",")
                                    {//si es coma, es porque termino el campo con comillas
                                        concomilla = false;
                                    }
                                    else
                                    {//si no es coma es un caracter normal que se debe agregar
                                        campo += caracter;
                                    }
                                }
                            }
                        }
                        else
                        {//si no estoy dentro de un campo que posee comillas, entonces es una comilla de inicio de campo
                            concomilla = true;
                        }
                    }
                    else
                    {//si no es comilla
                        if (caracter == ",")
                        {//si es una coma
                            if (concomilla)
                            {//si estoy dentro de un campo que posee comas y comillas
                                campo += caracter;
                            }
                            else
                            {//si no, es porque es una coma delimitadora de campos, agregamos el campo a la lista y blanqueamos el campo
                                lista.Add(campo);
                                campo = "";
                                if (i == linea.Length - 1)
                                {//si es la ultima iteracion, agregamos el campo a la lista
                                    lista.Add(campo);
                                }
                            }
                        }
                        else
                        {//si no es comilla ni coma, es un caracter normal, que debe ser agregado al campo actual, no se comprueba si esta o no dentro de concomillas debido a que siempre debe estar, y de no estarlo es error de sintaxis pero aun asi se agrega
                            campo += caracter;
                            if (i == linea.Length - 1)
                            {//si es la ultima iteracion, agregamos el campo a la lista
                                lista.Add(campo);
                            }
                        }
                    }

                }
                //convertimos la lista a un array de string
                registro = (string[])lista.ToArray(typeof(string));
            }
            else
            {
                registro = CSV_ParseLineaSencilla(linea);
            }

            return registro;
        }


        //Metodo estatico que verifica si en una string existe una Expresion Regular
        //Es decir, otra string patron que se repite una o varias veces dentro de la primera
        //regresa un int que representa el numero de veces que se repite el patron de la linea string
        //regresa 0 si no encuentra ninguna coincidencia
        private static int VerificRegex(string linea, string patron)
        {
            int rep = 0;
            int i = 0;

            for (i = 0; i <= (linea.Length - patron.Length); i++)
            {
                if (linea.Substring(i, patron.Length) == patron)
                    rep++;
            }
            return rep;
        }


        //Metodo privado que parsea una linea considerando que es CSV y que solo contiene comas de separacion
        //Es decir, no posee comas embebidas dentro de campos ni comillas
        private static string[] CSV_ParseLineaSencilla(string linea)
        {
            string[] registro = Regex.Split(linea, ",");
            return registro;
        }

    }
} 

Creado por Hazael FMG

No hay comentarios:

Publicar un comentario