Posts Tagged ‘aprende’

Parsing de HTML con HtmlAgilityPack

February 5th, 2009

La web esta llena de información, todo tipo de información, desde el vídeo divertido mas irrelevante hasta noticias o documentos de verdadera importancia. Hay un mundo de información a la cual tu computadora esta unida por el cordón umbilical de tu cable de red, dispuesta a ser accesada y mostrada en tu pantalla a la mas mínima orden tuya.

Pero tiene un problema.

Toda esta valiosa información se encuentra ofuscada en HTML, un lenguaje el cual a sido seriamente abusado durante el transcurso de los años, debido a que su especificación indica que los navegadores deben hacer lo mejor posible para corregir errores en el formateo de un documento, por lo que los usuarios terminaron en volverse descuidados, olvidando una etiqueta de cierre aquí y halla, al fin y al cabos el resultado es el mismo, ¿no?, los navegadores hacen todo lo posible por mostrar incluso los documentos mas mal formados.

Eso hace que hacer un programa para extraer información de un documento HTML pueda llegar a ser un poco complicado en ocasiones.

HtmlAgilityPack to the rescue!

HtmlAgilityPack es una librería para .NET que nos brinda un parser de HTML que nos permite leer, escribir y manipular documentos HTML y soporta Xpath, el cual discutí hace tiempo. Y lo que lo hace realmente especial es que es increíblemente permisivo, soportando los documentos mas perversamente malformados que te puedes encontrar en tu navegación día a día.

HtmlAgilityPack combinado con Firebug nos permite extraer hasta la ultima gota de información requerida, y para no hacérselas larga pondré un par de ejemplos en el resto del post.

Quiero obtener la lista de títulos del blog del AlbertEin

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
    var client = new System.Net.WebClient();
    client.Encoding = System.Text.Encoding.UTF8;
    var document = new HtmlAgilityPack.HtmlDocument();
    document.LoadHtml(client.DownloadString("http://albertein.gamersla.net"));
 
    foreach (HtmlAgilityPack.HtmlNode node in 
        document.DocumentNode.SelectNodes("//a[@class='title']"))
            Console.WriteLine(node.InnerHtml);
}

Quiero obtener la lista de palabras listada en una web de un diccionario

1
2
3
4
5
6
7
8
9
10
11
12
static void Main(string[] args)
{
    var url = "http://diccionario.babylon.com/Humanidades/Mitolog%C3%ADa/A/1/";
    var client = new System.Net.WebClient();
    client.Encoding = System.Text.Encoding.UTF8;
    var document = new HtmlAgilityPack.HtmlDocument();
    document.LoadHtml(client.DownloadString(url));
 
    foreach (HtmlAgilityPack.HtmlNode node in
        document.DocumentNode.SelectNodes("//a[@class='words']"))
            Console.WriteLine(node.InnerHtml);
}

Quiero bajar todos los malditos comics de Penny Arcade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void Main(string[] args)
{
    var url = "http://www.penny-arcade.com/comic/1998/11/18/";
    var baseUrl = "http://www.penny-arcade.com";
    bool lastStrip = false;
    var client = new System.Net.WebClient();
    client.Encoding = System.Text.Encoding.UTF8;
 
    while (!lastStrip)
    {
        var document = new HtmlAgilityPack.HtmlDocument();
        document.LoadHtml(client.DownloadString(url));
 
        var imageQuery = "/html/body/div[2]/div[3]/div/div[2]/img";
        var nextButtonQuery = "/html/body/div[2]/div[3]/div/div[3]/a[4]";
 
        var imageUrl = document.DocumentNode.
            SelectSingleNode(imageQuery).Attributes["src"].Value;
 
        //Aquí iria el codigo para descargar la imagen que se encuentra en
        //baseUrl + imageUrl
 
        //Obtenemos la dirección del próximo comic
 
        url = baseUrl + document.DocumentNode.
            SelectSingleNode (nextButtonQuery).Attributes ["href"].Value; 
 
        lastStrip = url == baseUrl; // Llegamos al ultimo comic?
    }
}

Podría seguir así todo el día, pero supongo que con eso es suficiente para que capten la idea.

¿Y tu, como lo has usado?

Aprendiendo Regex – El imperio contrataca.

February 21st, 2008

Gnoblis posteo hacerca de un pequeño programa que hizo para convertir bbcode a html en su web: http://gnoblis.gamersla.net/?p=67. El codigo esta aca http://gamersla.net/gnoblis/descargas/CCast.cs

Su solucion si bien funciona, no es de lo mas elegante. El problema es facilmente resuelto con expresiones regulares, y se lo hizo saber en un comentario en su post.

Claro, los que me conocen sabran que no es suficiente haberle dicho como se podria resolver mejor, ¡hace falta hacerlo uno mismo!

Asi que como demostracion del poder de las expresiones regulares y que no soy un hablador les muestro el codigo:

Nota, el programa no lo probe a excepcion de las expreciones regulares, por lo que pueden encontrarse con sorpresas ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
 
class Program
{
 
    public static void Main(string[] args)
    {
 
        StreamReader inputReader;
        StreamWriter outputWriter;
        string actualLine;
        StringBuilder inputFileContent = new StringBuilder ();
 
        foreach (string actualFile in args) //Agrego la capacidad de convertir varios archivos a la ves
        {
            if (!File.Exists(actualFile))
            {
                Console.WriteLine("File not found, skipped: {0}", actualFile);
                continue;                   //Ignoramos el archivo, pero seguimos con los demas \o/
            }
 
            inputReader = new StreamReader(File.OpenRead(actualFile));
 
 
            inputFileContent.Remove(0, inputFileContent.Length);
            while ((actualLine = inputReader.ReadLine()) != null)
                inputFileContent.AppendLine(string.Format("{0}{1}", actualLine, "<br />"));
 
            inputReader.Close ();
 
            if (inputFileContent.Length == 0)
            {
                Console.WriteLine("Empty file, skipped: {0}", actualFile);
                continue;
            }
 
            string bbcodeData = inputFileContent.ToString();
 
            //Quitamos todos los espacios antes y despues de un igual, no solo uno
            bbcodeData = Regex.Replace(bbcodeData, @"\ {0,}=\ {0,}", "=", RegexOptions.IgnoreCase | 
                                                                          RegexOptions.Multiline);
 
            //Convertimos [b], [i], [u], [s] y sus respectivos cierres
            bbcodeData = Regex.Replace(bbcodeData, @"\[(?<tag>\/{0,1}[b|i|u|s])\]", @"<${tag}>", 
                RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
            //imagenes
            bbcodeData = Regex.Replace(bbcodeData, @"\[img\](?<url>.*?)\[\/img\]",
                "<img src=\"${url}\" />", RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
            //Links
            bbcodeData = Regex.Replace(bbcodeData, @"\[url=(?<url>.*?)\](?<data>.*?)\[/url\]",
                "<a href=\"${url}\">${data}</a>", RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
            //Colores
            bbcodeData = Regex.Replace(bbcodeData, @"\[color=(?<color>.*?)\](?<data>.*?)\[/color\]", 
                "<font color=\"${color}\">${data}</font>", RegexOptions.IgnoreCase | 
                                                           RegexOptions.Multiline);
 
            outputWriter = new StreamWriter (File.Create(actualFile + ".html"));
 
            outputWriter.Write(bbcodeData);
 
            outputWriter.Close();
        }
    }
}

¿No sabes XPath? ¡Aprende!

December 11th, 2007

Otra de las herramientas de un desarrollador que se vuelven completamente imprescindibles una ves que las conoces es XPath.

XPath es un estándar del W3C para realizar querys a documentos Xml, haciendo fácil filtrar los nodos de un documento de acuerdo a la estructura y atributos de los nodos.

Si no usamos XPath, tendríamos que iterar a través de todos los nodos de un documento Xml para obtener la información que buscamos.

Para no hacérsela mas larga voy a dejar que el código hable por si mismo.

Imaginen que tienen el siguiente Xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8" ?> 
<Sucursales>
	<Sucursal name="Matriz">
		<Articulo name="Monitor" precio="1500"/>
		<Articulo name="Impresora" precio="400"/>
	</Sucursal>
	<Sucursal name="Mayoreo">
		<Articulo name="Monitor" precio="1600"/>
		<Articulo name="Impresora" precio="500"/>
	</Sucursal>
	<Sucursal name="Menudeo">
		<Articulo name="Monitor" precio="1800"/>
		<Articulo name="Impresora" precio="700"/>
	</Sucursal>
</Sucursales>

Y ustedes quieren saber tres cosas:

  1. ¿Que artículos con que precios se encuentran en la sucursal Matriz?
  2. ¿Que sucursales tienen monitores?
  3. ¿Que precio tiene una impresora en la sucursal menudeo?

Sin XPath estas tres simples preguntas se convierte en esta monstruosidad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Xml;
using System.Collections.Generic;
 
class Program
{
    public static void Main()
    {
        XmlDocument document = new XmlDocument();
        document.Load("Test.xml");
        XmlNode sucursales = document.ChildNodes[1];
 
        //Obtener los articulos de la sucursal matriz
        XmlNodeList articulosMatriz;
        foreach (XmlNode sucursal in sucursales.ChildNodes)
            if (sucursal.Name == "Sucursal")
                if (sucursal.Attributes["name"].Value == "Menudeo")
                    articulosMatriz = sucursal.ChildNodes;
 
        //Obtener todas las sucursales que tienen monitores
        List<XmlNode> sucursalesConMonitor = new List<XmlNode>();
        foreach (XmlNode sucursal in sucursales.ChildNodes)
        {
            if (sucursal.Name == "Sucursal")
            {
                foreach (XmlNode articulo in sucursal.ChildNodes)
                {
                    if (articulo.Name == "Articulo")
                    {
                        if (articulo.Attributes["name"].Value == "Monitor")
                        {
                            sucursalesConMonitor.Add(sucursal);
                            break; //Siguiente sucursal
                        }
                    }
                }
            }
        }
 
        //Obtener el precio de una impresora en la sucursal menudeo
        XmlNode impresoraSucursalMenudeo = null;
        foreach (XmlNode sucursal in sucursales.ChildNodes)
        {
            if (sucursal.Name == "Sucursal")
            {
                if (sucursal.Attributes["name"].Value == "Menudeo")
                {
                    foreach (XmlNode articulo in sucursal.ChildNodes)
                    {
                        if (articulo.Name == "Articulo")
                        {
                            if (articulo.Attributes["name"].Value == "Impresora")
                            {
                                impresoraSucursalMenudeo = articulo;
                                break; 
                            }
                        }
                    }
 
                    if (impresoraSucursalMenudeo != null)
                        break; //Se encontro el articulo, terminamos!
                }
            }
        }
    }
}

¡HORRIBLE!

Claro que si hicieron la tarea y saben XPath, ¡las cosas cambian dramáticamente!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Xml;
 
class Program
{
    public static void Main()
    {
        string xPathQuery;
        XmlDocument document = new XmlDocument();
        document.Load("Test.xml");
 
        //Obtener los articulos de la sucursal matriz
        xPathQuery = @"/Sucursales/Sucursal[@name='Matriz']/Articulo";
        XmlNodeList nodes = document.SelectNodes(xPathQuery);
 
        //Obtener todas las sucursales que tienen monitores
        xPathQuery = @"/Sucursales/Sucursal[./Articulo[@name='Monitor']]";
        nodes = document.SelectNodes(xPathQuery);
 
        //Obtener el precio de una impresora en la sucursal menudeo
        xPathQuery = @"/Sucursales/Sucursal[@name='Menudeo']/Articulo[@name='Impresora']";
        XmlNode node = document.SelectSingleNode(xPathQuery);
    }
}

SIMPLE, SENCILLO Y HERMOSO

Si no saben nada al respecto lean por lo menos este tutorial de w3schools, no se arrepentirán.

P.S XPath es un estándar, por lo que no depende de .NET, puedes usarlo en Java, Python o lo que te de tu gana

¿No sabes Regex? ¡Aprende!

December 7th, 2007

Algo de lo que me e dado cuenta cuando e dado platicas/talleres/cursos es que la gran mayoría de los desarrolladores, al menos de la región, no tienen ni puta idea acerca de que demonios son las expresiones regulares (Regex para los amigos).

Muchos recuerdan haber escuchado algo al respecto en su clase de autómatas en la universidad, pero como a todos, les paso de noche :P .

Es difícil de entender como la gente puede pasarse sentado en su vida profesional sin saber Regex, escribiendo cientos de lineas de código resolviendo problemas que con unos cuantos caracteres de Regex queda solucionado.

Si aun no sabes Regex, ¡deja de leer esto inmediatamente y ponte a buscar información al respecto!

Para los escépticos, un pequeño ejemplo:

Imagina que quieres validar que un teléfono siga el formato (900) 9-00-00-00, la forma común de hacerlo seria algo cómo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    bool isValidPhoneNumber(string input)
    {
        //Validate (900) 9-00-00-00
        if (input.Length != 16)
            return false;
        if (!input.StartsWith("("))
            return false;
        if (input[4] != ')')
            return false;
        int number;
        if (!int.TryParse(input.Substring(1, 3), out number))
            return false;
        if (input[1] == '0' || !char.IsNumber(input[1]))
            return false;
        if (input[5] != ' ')
            return false;
        string[] numbers = input.Substring(6, 10).Split ('-');
        if (numbers.Length != 4)
            return false;
        foreach (string subpart in numbers)
            if (!int.TryParse(subpart, out number))
                return false;
        if (numbers[0][0] == '0')
            return false;
        return true;
    }

Es tedioso de hacer, resulta fácil dejar errores en el, sin mencionar que es completamente horrible y resulta difícil de entender, y precisamente este es uno de los momentos en que Regex es útil:

1
2
3
4
5
6
7
8
   bool isValidPhoneNumber(string input)
    {
        //Validate (900) 9-00-00-00
        string expression = @"^\([1-9]\d{2}\)\ [1-9](\-\d{2}){3}$";
        System.Text.RegularExpressions.Regex regex = 
            new System.Text.RegularExpressions.Regex(expression);
        return regex.IsMatch(input);
    }

Si bien la sintaxis puede resultar confusa al inicio, basta un poco de practica para acostumbrarse a ella y puede llegar a ahorrarte toneladas de trabajo, especialmente si la usas para buscar patrones de texto en ves de solamente validación.

Confíen en mi, si no las conocen, ¡Aprendan!.

P.S: No me hago responsable de cualquier mal uso que puedan hacer de ellas ;)