Haz tus queries a HTML con LINQ To HTML

Problema

Desde hace varios meses cada domingo envió más de 250 SMS vía Orange a distintos celulares de diferentes compañías telefónicas,  Orange tienen una aplicación web que me permite agregar contactos y a la vez con estos agregarlo a listas de contactos para poder enviarle lo mensajes, sucede que los números que no pertenecen a Orange no se pueden enviar y a la vez se quedan en los estados  “Numero Invalido”, “Pendiente”, “Expirado” y “Fallido”, hasta aquí vamos bien, la aplicación web me provee una opción para ver el histórico de mensajes enviado, el problema consiste en que el histórico no me provee una opción para filtrar los números que están en los estados “Numero Invalido”, “Pendiente”, “Expirado” y “Fallido”.

Solución   

Dada esta problemática decidí por mi cuenta poder hacer consultas al html que genera la aplicación

Aplicacion

Tome solamente del código html el table donde están los campos que necesito para poder hacerle más adelante consulta con Linq y lo guarde en el disco C: con el nombre geeksPhones.html

Entendiendo el esquema del archivo guardado

El archivo guardado tiene el siguiente esquema para la primera fila:

 esquemafirstrow

Esta primera fila realmente no nos interesa la que si nos va a interesar es a partir de la segunda fila en adelante que tendrá el mismo código repetitivo de los 1546 mensajes enviados:

secondrow

Diseño de nuestra aplicacion

Como todos sabemos HTML se deriva del GML (Standard Generalized Markup Language – Lenguaje de Marcación Estándar Generalizada), que tambien xml se deriva del GML, entonces  decidi convertir el HTML en XML y esto se hizo posible usando el componente htmlagilitypack y la vez parsiando ese html con mi Extension Methods ToXMLDocument();

  public static XDocument ToXMLDocument(this HtmlDocument doc)
        {
            using (StringWriter strWriter = new StringWriter())
            {
                doc.OptionOutputAsXml = true;
                doc.Save(strWriter);
                return XDocument.Parse(strWriter.GetStringBuilder().ToString());
            }
        }

Tenemos tambien la clase Phone:

public class Phones
    {
        public string SendDate { get; set; }
        public string Phone { get; set; }
        public string EndDate { get; set; }
        public Status Status { get; set; }
    }

Y finalmente el Enum Status:

public enum Status
    {
        None = 0, //Siempre uso este como valor Default
        Recibido = 1,
        Pendiente = 2,
        NumeroInvalido = 3,
        Fallido = 4,
        Expirado = 5
    }

 

Definido ya estos elementos entonces use el siguiente Linq Query para poder leer el html y convertirlo en una Lista genérica de tipo Phone:

HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
            
            doc.Load(@"C:geeksphones.html");
            XDocument xmlDoc = doc.ToXMLDocument();
            List<Phones> query = (from d in xmlDoc.Descendants("table")
                         let tbody = d.Element("tbody")
                         let tr = tbody.Element("tr")
                         select new
                         {

                             SendDate = (from s in tbody.Descendants("td")
                                         select s
                                             into tempsenddate
                                             where tempsenddate.Attribute("width").Value == "5%" && 
tempsenddate.HasElements ==
false
&& tempsenddate.Value.Length > 18
select tempsenddate.Value ).ToArray(), Status = (from status in tbody.Descendants("td") select status into tempstatus where tempstatus.Attribute("width").Value == "3%" && tempstatus.HasElements == false
&& tempstatus.Value.Length > 3 && tempstatus.Value.Length <= 15 select tempstatus.Value).ToArray(), PhoneNumber = (from p in tbody.Descendants("td") select p into tempPhoneNumber where tempPhoneNumber.Attribute("width").Value == "5%" &&
tempPhoneNumber.HasElements ==
false && tempPhoneNumber.Value.Length == 10 select tempPhoneNumber.Value).ToArray(), ReceiveDate = (from r in tbody.Descendants("td") select r into tempReceive where tempReceive.Attribute("width").Value == "3%" &&
tempReceive.HasElements ==
false && tempReceive.Value.Length == 19
|| tempReceive.Value.Length == 3
select tempReceive.Value).ToArray() } into tempPhone from i in Enumerable.Range(0, tempPhone.PhoneNumber.Count()) select new Phones() { Phone = tempPhone.PhoneNumber[i], EndDate = tempPhone.ReceiveDate[i], SendDate = tempPhone.SendDate[i], Status = (Status)Enum.Parse(typeof(Status), tempPhone.Status[i].Replace(" ", "")) } ).ToList();

Explicación del Query

Básicamente lo que queremos de nuestro HTML es saber la fecha de envió, el número de teléfono, el estatus del mensaje y la fecha de recepción, entonces para obtenerlo correctamente tenemos que:

SendDate: solamente se llenara cuando el elemento (td) tenga el atributo width en 5%, no tenga más elementos hijos y la longitud del valor del elemento sea igual a 19 caracteres.

Status: para que pueda tener los datos correctos se debe cumplir la condición de que el elemento (td) tenga el atributo width = 3%, el elemento no tenga más elementos hijos y la longitud del valor del elemento este en un rango de 3 hasta 15 caracteres.

PhoneNumber: para obtener los números correctamente se debe cumplir las condiciones de que el elemento (td) tenga el atributo width = 5%, el elemento no tenga más elementos hijos y la longitud del valor del elemento sea igual a 10 caracteres debido a que los números en mi país tiene un total de 10 caracteres.

ReceiveDate: para obtener la fecha de recibido correctamente se debe cumplir las condiciones de que el elemento (td) tenga el atributo width=3%, el elemento no tenga más elemento hijos y la longitud del elemento sea igual a 18 caracteres o a 3 caracteres ya que cuando el mensaje se queda en el estatus expirado la fecha sale con los caracteres “—“.

 linqToHtml

Descargas

Codigo Linq To HTML aqui.
Archivo geekPhones.html aqui. (este archivo es necesario ponerlo en el disco C:)

Plus

Dentro de la aplicación también encontraran un Tab llamado leer imágenes el cual tiene la funcionalidad de leer las imágenes de una dirección web dada y mostrar el largo y el ancho de la imagen.

LinqToHTMLImg

y el Codigo para esto es:

 HtmlAgilityPack.HtmlWeb hweb = new HtmlAgilityPack.HtmlWeb();
            string strUrl = @textBox1.Text;
            Uri url = new Uri(strUrl);
            HtmlAgilityPack.HtmlDocument doc = hweb.Load(strUrl);
            


            var xdoc = doc.ToXMLDocument();
            var imgs = from x in xdoc.Descendants()
                       let width = int.Parse((x.Attribute("width") ?? new XAttribute(XName.Get("width"), "0")).Value.TrimEnd('%'))
                       let height = int.Parse((x.Attribute("height") ?? new XAttribute(XName.Get("height"), "0")).Value.TrimEnd('%'))
                       let metrica = Math.Sqrt(width * height)
                       where x.Name.LocalName == "img"
                       orderby metrica descending
                       select new
                       {
                           Src = x.Attribute(XName.Get("src")).Value,
                           Width = width,
                           Height = height
                       };

 

Espero que le haya sido de utilidad.