Blog de Oxxigeno


C#, .NET 2.0 y procesamiento de CSV

Escrito por Israel Viñuales en .NET, Desarrollo

Los ficheros csv son ficheros de texto que contienen valores separados por alguna caracter (en nuestro caso punto y coma) y agrupados en líneas. Es decir, cada línea podemos considerarla un conjunto de valores completo y podemos identificar a este conjunto de valores como una entidad.


Lo primero que debemos hacer es identificar esa entidad y crear una clase para guardar esos datos. Después debemos tener algun mecanismo para rellenar esta clase con los valores de cada una de las filas (cada fila del fichero supondrá un objeto de ésta clase).

Pongamos un ejemplo.  Supongamos un fichero csv como el siguiente:

Edad; Nombre; Apellido
15; Pepito; Perez
57; Ernesto; Román
RowCount = 2

En ocasiones los csv traen una cabecera y/o un pie de fichero y en otras no, por lo que no podemos tratar de procesarlas siempre.

Vamos a empezar creando una clase entidad como la siguiente:

public class Edades
{
    private int _edad;
    private string _nombre;
    private string _apellido;
    public int Edad
    {
        get { return _edad; }
        set { _edad = value; }
    }
    public string Nombre
    {
        get { return _nombre; }
        set { _nombre = value; }
    }
    public string Apellido
    {
        get { return _apellido; }
        set { _apellido = value; }
    }
}

Como vemos, se trata de una clase tonta con 3 atributos y sus 3 correspondientes propiedades publicas. En este ejemplo sería muy sencillo por cada fila crearnos un objeto y asignar a Edad el primer valor de la fila, a Nombre la segunda y a Apellido la tercera, serían 3 simples líneas. Sin embargo, a medida que el csv (y la clase) contengan más campos esto se haría más complejo, y sería muy complejo de modificar si algún día deciden introducir un campo nuevo en mitad del csv.

Claro este punto, debemos tener alguna forma de relacionar cada propiedad de la clase con la posicion que ocupa ese valor en el csv. Para esto tenemos dos opciones:

  • Si la posición de cada campo es susceptible de ser modificada periódicamente deberíamos determinar esta relación en el web.config (si es una aplicación web) o en app.config (si es una aplicación de escritorio)
  • Si la posición de cada campo no es susceptible de ser modificada, es preferible agregar atributos a la clase que indique donde debemos buscar el valor de cada propiedad.

Voy a centrarme en este segundo supuesto, el primero posiblemente lo trataremos en otro artículo. Vamos a generar una clase que herede de Attribute para poder indicar en la clase que acabamos de crear donde buscaremos cada valor.

public class CSVFieldAttribute : Attribute
{
    private int _order;
    public int Order
    {
        get { return _order; }
        set { _order = value; }
    }
    public CSVFieldAttribute(int order)
    {
        _order = order;
    }
}

Ahora modificamos la clase de la entidad, añadiéndole a cada propiedad un atributo indicando su posición en el csv. La clase quedaría como sigue:

public class Edades
{
    private int _edad;
    private string _nombre;
    private string _apellido;
 
    [CSVField(0)]
    public int Edad
    {
        get { return _edad; }
        set { _edad = value; }
    }
    [CSVField(1)]
    public string Nombre
    {
        get { return _nombre; }
        set { _nombre = value; }
    }
    [CSVField(2)]
    public string Apellido
    {
        get { return _apellido; }
        set { _apellido = value; }
    }
}

En este punto podemos elegir partir de una base en 0 o una base en 1. Yo he preferido partir de base 0 porque es la utilizada en los arrays por C#.
Ya tenemos relacionada la clase de entidad con los valores del csv, ahora falta buscarnos la vida para rellenar un objeto a partir de una fila del csv. Propongo utilizar Reflexion y las Templated classes para generalizar este proceso, creando una clase que trasforme un array de valores en un objeto del tipo que le indiquemos. A las propiedades de ese objeto les asignaremos el valor (mediante reflexion) que haya en el array de valores en la posición indicada. Vayamos al tema:

public class CSVConverter<T>
{
    public T Convert(string[] values)
    {
        Type type = typeof(T);
        T obj = Activator.CreateInstance<T>();
        try
        {
            CSVFieldAttribute att = null;
            object[] attributes = null;
            foreach (PropertyInfo prop in type.GetProperties())
            {
                attributes = prop.GetCustomAttributes(typeof(CSVFieldAttribute), false);
                if (attributes.Length &gt; 0)
                {
                    att = attributes[0] as CSVFieldAttribute;
                    /* Si hemos optado por usar base 1 debemos poner:
                        values[att.Order - 1]
                    */
                    if (!(values[att.Order].Trim().Equals(string.Empty)))
                    {
                        prop.SetValue(obj,
                           Convert.ChangeType(values[att.Order], prop.PropertyType),
                           null
                        );
                    }
                }
            }
        }
        catch (Exception ex)
        {
            throw new Exception("Error al intentar hacer la conversión de tipos.\r\n" + ex.Message, ex);
        }
        return obj;
    }
}

Ya solo queda procesar el fichero para ir creando los objetos y hacer lo que tengamos que hacer con ellos. Para ello, nos crearemos una clase base a partir de la cual heredaremos las clases de negocio que creamos oportunas para procesar distintos modelos de csv.

public class ProcessCSV<T>
{
    protected StreamReader _streamReader;
    protected string _line;
    public void Process(string filename)
    {
        try
        {
            _streamReader = new StreamReader(filename);
            this.ProcessHeader();
            this.ProcessBody();
            this.ProcessFooter();
        }
        catch (Exception ex)
        {
            throw ex;
        }
        finally
        {
            if (_streamReader != null)
                _streamReader.Close();
        }
    }
    protected virtual void ProcessHeader()
    {
        throw new Exception("No implementado");
    }
    protected virtual void ProcessFooter()
    {
        throw new Exception("No implementado");
    }
    protected virtual void ProcessOneObject(T obj)
    {
        throw new Exception("No implementado");
    }
    protected void ProcessBody()
    {
        T obj = Activator.CreateInstance<T>();
        _line = _streamReader.ReadLine();
        while (!_streamReader.EndOfStream && _line != null && _line.Trim() != string.Empty)
        {
            try
            {
                string[] values = _line.Split(';');
                CSVConverter<T> converter = new CSVConverter<T>();
                obj = converter.Convert(values);
                this.ProcessOneObject(obj);
            }
            catch (Exception ex)
            {
                // Aquí escribiríamos algo en el log indicando qué fila ha fallado
            }
        }
    }
}

Ya tenemos nuestra clase genérica. Pero claro, con esto no hacemos nada, si intentas parsear algo llamando a esta clase te van a saltar excepciones porque no está implementados algunos métodos necesarios. Generemos la clase que realmente usaremos.

Partimos de varios supuestos: el csv es el que he puesto arriba (con cabecera y pie), no vamos a usar para nada ni la cabecera ni el pie y queremos agregar a una lista para luego procesarlos (esto no lo voy a contemplar en el articulo).
Al tema:

public class EdadesBL : ProcessCSV<Edades>
{
    private List<Edades> _list;
    public EdadesBL()
    {
        _list = new List<Edades>();
    }
    protected override void ProcessHeader()
    {
        // Solo leemos porque no queremos conservar la cabecera
        _streamReader.ReadLine();
    }
    protected override void ProcessFooter()
    {
        // Solo leemos porque no queremos conservar la cabecera
        _streamReader.ReadLine();
    }
    protected override void ProcessOneObject(Edades obj)
    {
        // Solo queremos agregarlo a la lista.
        _list.Add(obj);
    }
    public List<Edades> Edades
    {
        get { return _list; }
    }
}

Ahora solo tenemos que procesar el fichero con dos simples líneas (en nuestro pequeño ejemplo 3 para recoger tambien la lista de Edades):

EdadesBL business = new EdadesBL();
business.Process(filename);
List todos = business.Edades;

3 comentarios

  1. oscar dijo el 04/05/2010:

    EdadesBL business = new EdadesBL();
    business.Process(filename);
    List todos = business.Edades;

    estas lineas donde se ponen

  2. oscar dijo el 04/05/2010:

    esta parte me marca error ne > 0) en toda esta parte ok
    attributes = prop.GetCustomAttributes(typeof(CSVFieldAttribute), false);
    if (attributes.Length > 0)

    esta parte donde esta el “Convert” me marca error

    Convert—-.ChangeType(values[att.Order], prop.PropertyType),
    null

    te nesecito ayuda se los agradeceria
    gracias!!!

  3. oscar dijo el 04/05/2010:

    un gran favor podrias mandarme todo el codigo
    es que unas partes me marcar error

    y lo que queremos hacer nosotros es que
    aparescan mas registros
    como son
    docente,departemento,materia,grupo,curso,cuenta,nombredelalumno,prog

Deja un comentario


Nuestras oficinas:

Oxxigeno España
C/Luchana, 23. 28010 - Madrid
Tel.: 91 144 12 00
Fax: 91 144 12 01
E-mail:

Oxxigeno France
77, rue La Boëtie. 75008 - Paris
Tel.: +33 (0)1 45 61 68 99
Fax: +33 (0)1 45 61 51 05
E-mail: