miércoles, 20 de julio de 2016

El Demo del Día: Leer Archivos de Excel 2007 usando Archivos XML

Leer Archivos de Excel 2007 usando Archivos XML

Este Demo es muy interesante ya que nos permite leer archivos Excel 2007 sin necesidad de tener instalado ni Office (para Automatización COM) ni los Componentes de Acceso a Datos de Office (para OLEDB), ya que solo debemos tener instalado .NET Framework para ejecutar la aplicación (el cual ya viene instalado en la mayoría de Windows).

En el Demo al Descomprimir el archivo de Excel No se crean archivos en disco sino se guardan en memoria usando una lista de objetos XmlDocument ya que se van a leer varios nodos de cada archivo XML. Es por eso que la velocidad es superior a las otras 2 técnicas, sobre todo con gran cantidad de datos.

1. Formatos Office Open XML

Quizás la mayoría no se ha dado cuenta que los archivos de Office a partir de la versión 2007 cumplen con el estándar Open XML que quiere decir que cada archivo es un comprimido (zip) que contiene archivos XML con Metadatos y datos.

Antes los archivos de Office (versión 2003 e inferiores) eran archivos binarios definidos por Microsoft, pero a partir de la versión 2007 son archivos XML comprimidos en una extensión que termina en X (Xml) o M (Macros), por ejemplo:

- En Word: Documentos (docx), Documentos con Macros (docm), Plantillas (dotx), Plantillas con Macros (dotm).

- En Excel: Libros (xlsx), Libros con Macros (xlsm), Plantillas (xltx), Plantillas con Macros (xltm).

- En PowerPoint: Presentaciones (pptx), Presentaciones con Macros (pptm), Plantillas (potx), Plantillas con Macros (potm).

- Otros: XML Paper Specification (XPS), Open XPS (OXPS), etc.

Como experimento solo basta cambiar la extensión de cualquiera de estos archivos a rar y descomprimirlos para ver que contienen archivox XML, por ejemplo, en el caso de uno de mis archivos de Excel 2007:

- Carpeta _rels: archivo .rels

- Carpeta docProps: archivos app.xml y core.xml

- Carpeta xl:
  -) SubCarpeta _rels: archivo workbook.xml.rels
  -) SubCarpeta theme: archivo theme1.xml
  -) SubCarpeta worksheets: sheet1.xml, sheet2.xml, ...
  -) Archivo sharedStrings.xml
  -) Archivo styles.xml
  -) Archivo workbook.xml

- Archivo [Content_Types].xml

2. Crear una Aplicación Windows Forms en C#

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "Excel_Leer_XML", cambiarle de nombre al formulario por "frmVisorExcel" y realizar el diseño similar a la figura mostrada:

Diseño del Formulario "frmVisorExcel"

En el diseño hay una etiqueta (lblArchivo), un cuadro de texto (txtArchivo), 2 botones (btnAbrir y btnNuevo) y un control Tab (tabExcel) sin ninguna página ya que estas se crean en tiempo de ejecución en función al número de hojas.

3. Escribir el Código de la Aplicación en C#

Abrir el formulario "frmVisorExcel" y escribir el siguiente código:
using System;
using System.Text;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Xml;
using System.Windows.Forms;
using System.Collections.Generic;

namespace Excel_Leer_XML
{
    public partial class frmVisorExcel : Form
    {
        public frmVisorExcel()
        {
            InitializeComponent();
        }

        private void abrirExcel(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Title = "Selecciona el archivo de Excel a abrir";
            ofd.Filter = "Archivo de Excel 2007|*.xlsx";
            ofd.InitialDirectory = @"C:\Data\NET\Practicas\Archivos\Excel";
            if (ofd.ShowDialog().Equals(DialogResult.OK))
            {
                string archivo = ofd.FileName;
                txtArchivo.Text = archivo;
                tabExcel.Controls.Clear();
                List<XmlDocument> docs = new List<XmlDocument>();
                List<string> nombres = new List<string>();
                List<string> valores = new List<string>();
                //Descomprimir el archivo Excel y guardar solo los archivos xml necesarios
                //en una Lista de objetos de tipo XmlDocument
                using (var archivoExcel = File.OpenRead(archivo))
                using (var zip = new ZipArchive(archivoExcel, ZipArchiveMode.Read))
                {                  
                    XmlDocument doc;
                    foreach (var archivoXml in zip.Entries)
                    {
                        if (archivoXml.Name == "workbook.xml" ||
                             archivoXml.Name == "sharedStrings.xml" ||
                             archivoXml.Name.StartsWith("sheet") &&
                             Path.GetExtension(archivoXml.Name)==".xml")
                        {
                            using (Stream stream = archivoXml.Open())
                            {
                                doc = new XmlDocument();
                                doc.Load(stream);
                                docs.Add(doc);
                                nombres.Add(archivoXml.Name);
                            }
                        }
                    }
                }
                if (docs.Count > 0)
                {
                    //Leer el archivo xml donde se guardan los datos de tipo cadena
                    int pos = nombres.IndexOf("sharedStrings.xml");
                    if (pos > -1)
                    {
                        XmlDocument xdStrings = docs[pos];
                        XmlElement nodoRaizStrings = xdStrings.DocumentElement;
                        XmlNodeList nodosValores = nodoRaizStrings.ChildNodes;
                        if (nodosValores != null)
                        {
                            foreach (XmlNode nodoValor in nodosValores)
                            {
                                valores.Add(nodoValor.FirstChild.FirstChild.Value);
                            }
                        }
                    }
                    //Leer el archivo xml workbook donde se guardan la lista de hojas
                    pos = nombres.IndexOf("workbook.xml");
                    if (pos > -1)
                    {
                        XmlDocument xdLibro = docs[pos];
                        XmlElement nodoRaizHojas = xdLibro.DocumentElement;
                        XmlNodeList nodosHojas = nodoRaizHojas.GetElementsByTagName("sheet");
                        string id, hoja;
                        if (nodosHojas != null)
                        {
                            foreach (XmlNode nodoHoja in nodosHojas)
                            {
                                id = nodoHoja.Attributes["sheetId"].Value;
                                hoja = nodoHoja.Attributes["name"].Value;
                                //Leer cada archivo xml con la hoja
                                pos = nombres.IndexOf("sheet" + id + ".xml");
                                if (pos > -1)
                                {
                                    XmlDocument xdHoja = docs[pos];
                                    XmlElement nodoRaizHoja = xdHoja.DocumentElement;
                                    XmlNodeList nodosFilas =
                                    nodoRaizHoja.GetElementsByTagName("row");
                                    DataTable tabla = new DataTable();
                                    DataRow fila = null;
                                    int indice;
                                    string celda, valor;
                                    XmlAttribute tipoString;
                                    int cf = 0; //contador de filas
                                    int cc = 0; //contador de columnas
                                    string nombreCol; //nombre de la columna: A, B, C, ...
                                    if (nodosFilas != null)
                                    {
                                        foreach (XmlNode nodoFila in nodosFilas)
                                        {
                                            XmlNodeList nodoCeldas = nodoFila.ChildNodes;
                                            if (nodoCeldas != null)
                                            {
                                                cc = 0;
                                                foreach (XmlNode nodoCelda in nodoCeldas)
                                                {                              
                                                    celda = nodoCelda.Attributes["r"].Value;
                                                    tipoString = nodoCelda.Attributes["t"];                              
                                                    valor = "";
                                                    if (tipoString != null)
                                                    {
                                                        if (valores != null && valores.Count > 0)
                                                        {
                                                            indice = int.Parse(nodoCelda.FirstChild.
                                                            FirstChild.Value);
                                                            valor = valores[indice];
                                                        }
                                                    }
                                                    else
                                                    {
                                                        if(nodoCelda.FirstChild!=null &&  nodoCelda.FirstChild.
                                                        FirstChild!=null)
                                                          valor = nodoCelda.FirstChild.FirstChild.Value;
                                                    }
                                                    if (cf == 0)
                                                    {
                                                        nombreCol = ((char)(65+cc)).ToString();
                                                        tabla.Columns.Add(nombreCol);
                                                    }
                                                    if (cc == 0)
                                                    {
                                                        fila = tabla.NewRow();
                                                    }
                                                    if (cc < tabla.Columns.Count)
                                                    {
                                                        fila[cc] = valor;
                                                    }
                                                    cc++;
                                                }
                                            }
                                            tabla.Rows.Add(fila);                      
                                            cf++;
                                        }
                                        DataGridView dgv = new DataGridView();
                                        dgv.Dock = DockStyle.Fill;
                                        dgv.ReadOnly = true;
                                        dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
                                        dgv.AllowUserToAddRows = false;
                                        dgv.DataSource = tabla;
                                        dgv.CellFormatting += new
                                        DataGridViewCellFormattingEventHandler(cambiarCabeceraFilas);
                                        TabPage pagina = new TabPage();
                                        pagina.Text = hoja;
                                        pagina.Controls.Add(dgv);
                                        tabExcel.Controls.Add(pagina);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        private void cambiarCabeceraFilas(object sender, DataGridViewCellFormattingEventArgs e)
        {
            DataGridView dgvHoja = (DataGridView)sender;
            if (e.RowIndex > -1 && e.ColumnIndex == 0)
            dgvHoja.Rows[e.RowIndex].HeaderCell.Value = (e.RowIndex + 1).ToString();
        }

        private void nuevaLectura(object sender, EventArgs e)
        {
            txtArchivo.Clear();
            tabExcel.Controls.Clear();
        }
    }
}

Nota: El método "abrirExcel" estaasociado al evento click del botón "btnAbrir" y el método "nuevaLectura" esta asociado al evento click del botón "btnNuevo".

4. Ejecutar y Probar la Aplicación Windows Forms

Grabar la aplicación y pulsar F5 para ejecutarla, luego clic al botón "..." y se mostrará el diálogo de abrir mostrando los archivos de Excel 2007 (xlsx), tal como se muestra en la siguiente figura:

Dialogo de Abrir archivos de Excel

Seleccionar un archivo del diálogo y se cargarán todas las hojas del libro, mostrándose en una página del control Tab una grilla, tal como se muestra en la siguiente figura:

Primer Tab con la primera Hoja

Nota: Observar que la grilla muestra en las cabeceras de columnas las letras: A, B, C, D, etc. y en la cabecera de las filas los números: 1, 2, 3, 4, etc. Además en este Demo el orden de las hojas si es el verdadero (en el Demo anterior de OLEDB era como se leía primero).

Como en mi caso abrí un archivo con 2 hojas, la primera que tiene Frameworks de JavaScript y la segunda con algunos Videos de JavaScript, tal como se muestra en la siguiente figura:

Segundo Tab con la segunda Hoja

Si se desea se puede abrir cualquier otro archivo dando click nuevamente al botón "...", tal como se muestra en la siguiente figura:

Archivo Excel con solo una Hoja

En este último caso se abrió un archivo con solo una Hoja de Productos.

5. Comentario Final

En este Demo hemos visto como Leer todas las hojas de un libro de Excel 2007 usando archivos XML, para lo cual primero descomprimimos en memoria el archivo usando la clase "ZipArchive" y usando la colección de "Entries" obtuvimos solo los archivos xml necesarios: workbook.xml, sharedStrings.xml y todos los sheets.xml.

Primero se procedió a leer el archivo "sharedStrings.xml" donde se guardan los datos de tipo cadena y luego se procedió a leer el archivo "workbook.xml" donde se guardan la lista de hojas, finalmente para cada hoja se procedió a leer su archivo xml con los datos de la hoja, los cuales se almacenaron en un DataTable y se mostró en una grilla dentro de un tabPage en el tabControl.

Esta aplicación solo es una demostración y no incluye el formato, fórmulas, gráficos y demás objetos que también pueden leerse del archivo Excel xlsx, pero es un buen punto de partida para todos aquéllos que quieran entender "como funcionan las cosas", en este caso saber el contenido del archivo excel.

El que desea saber todo sobre la especificación de Excel 2007 les comparto el documento que lo encuentran también en la web de Microsoft Protocols junto con los demás formatos:
Especificación de Excel Open XML (XLSX)

Espero que les guste el Demo que es una primicia ya que no creo que lo encuentren en ningún lugar, sobre todo explicado, además si "entendieron" la técnica también la pueden usar para Aplicaciones Web y solo en el Cliente sin ir al Servidor, podemos usar JavaScript para descomprimir el archivo Excel y leer los archivos XML para mostrar su contenido. Esto lo haré en otro post, siempre y cuando valoren los demos, les den Like y los compartan en sus redes sociales (claro esta incluyendo al autor: Luis Dueñas).

6. Descarga la Aplicación Windows

2016_07_20_Demo_Excel_Leer_XML