perri.to: A mashup of things

Scrapeando informacion de una web

  2019-09-29


La idea detras de este tutorial es mas que nada un poco de pr谩ctica, intentaremos resolver un problema com煤n sin mucho esfuerzo pero en la realidad no tiene gran utilidad pr谩ctica mas all谩 de la pr谩ctica en si.

El ejercicio planteado intentar谩 obtener la cotizaci贸n del Dolar Estado-Unidense con respecto al Peso Argentino, al menos al d铆a de la fecha no hay a simple vista una API del Banco de la Nacion Argentina para obtener estos datos, as铆 que los vamos a leer desde su sitio web directamente.

Para lograr esto vamos a necesitar dos partes:

La petici贸n

Para el caso de GET que es relativamente simple (no debemos enviar un cuerpo es simplemente acceder a una URL y esperar una respuesta) podemos utilizar directamente un m茅todo provisto por la libreria net/http: http.Get que toma como par谩metro 煤nico la URL, entonces un ejemplo del c贸digo ser铆a.

	const bnaURL = "http://www.bna.com.ar/Personas"
	/* ... */
	res, err := http.Get(bnaURL)
	if err != nil {
		log.Fatalf("getting bna website: %v", err)
	}
	/* ... */

Ahora bi茅n, de esta interacci贸n obtenemos res que es un objeto http.Response que contiene un par de miembros que nos importan:

  • StatusCode un entero representando el c贸digo de estado HTTP que nos va a servir para saber si el pedido fue en efecto exitoso, esto puede ser enga帽oso ya que intuitivamente uno esperar铆a obtener eta informaci贸n de err pero esto solo nos da el resultado de realizar la petici贸n (errores obtenidos aqu铆 seran por ejemplo cuando hay problemas de red o no se puede resolver la direcci贸n) este miembro en vez contiene el etado de la petici贸n en si.
  • Body esto es un io.ReaderCloser que exporta la interface de un io.Reader y un ‘io.Closer’ y nos permite tratar a Body como si fuese una especie de archivo del cual podemos leer y nos agrega la potestad de cerrarlo para finalizar la petici贸n corretamente.

Nuestro siguiente paso ser谩 leer el Body que nos deveolvi贸 la petici贸n (asumiendo que fue exitoso y obtuvimos 200 en StatusCode) e interpretar el HTML para obtener las cotizaciones y a su vez interpretarlas.

Una buena pr谩ctica es asegurarse que Body se cerrar谩, para lo cual utilizaremos un defer que nos permitir谩 encolar la invocaci贸n a Close() sin importar el resultado (en rasgos generales, si la funci贸n retorna la invocaci贸n se ejecutar谩, en nuestro ejemplo utilizamos log.Fatalf por brevedad en los ejemplos, la funci贸n donde originalmente escrib铆 esto retorna).

En el ejemplo a continuaci贸n ademas veremos que se utiliza Response.Status que es un texto del estado de retorno (por ejemplo 200 OK).

	defer res.Body.Close()
	if res.StatusCode != 200 {
		log.Fatalf("c贸digo de estado de la petici贸n inesperado: %d %s", res.StatusCode, res.Status)
	}

El c贸digo HTML

Habiendonos ocupado de lo referente a control de errores, podemos ahora dedicarnos a interpretar el HTML y obtener las cotizaciones, para ello utilizaremos la librer铆a goquery que nos permite seleccionar nodos del DOM HTML como si fuese jQuery

Lo primero que haremos es instanciar lo que goquery llama un Document utilizando el constructor goquery.NewDocumentFromReader que podr谩 tomar directamente a Response.Body porque implementa la interfaz io.Reader (recordemos que antes dejamos deferido una invocaci贸n a Close() que se ejecutar谩 incluso si la lectura falla). Es importante tener en cuenta que la lectura es lineal, como Body no implementa Seeker no se puede “Rebobinar” as铆 que leerlo podr铆a definirse como “consumirlo”.

	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Fatalf("reading site body: %v", err)
	}

Luego de intaniarlo, necesitaremos crear una funci贸n an贸nima aunque bien podr铆amos utilizar una funci贸n com煤n, esto es solo por practicidad y por la nula reutilizabilidad de la misma.

	const USD = "Dolar U.S.A"

	/* ... */

	var buy, sell string
	var dollar bool

	// Una selecci贸n es el resultado de un filtro o b煤squeda dentro del DOM
	// en ete caso dicho filtro se har谩 mas adelante y el resultado se pasar谩
	// a esta funci贸n an贸nima.
	extractUSD := func(i int, innerS *goquery.Selection) {
		// Buscamos un elemento con la clase y cuyo texto tenga lo que buscamos, este criterio
		// lo obtuvimos de analizar el c贸digo HTML de la pagina detenidamente el la 
		// secci贸n que nos interesa.
		if innerS.HasClass("tit") && innerS.Text() == USD {
			// utilizamos el flag dollar para denotar que en efecto este nodo es el inicio
			// de los datos de cotizaci贸n, si es true significa que los valores a continuaci贸n son la cotizaci贸n
			dollar = true
			return
		}
		// i indica cual de los nodos de esta selecci贸n tenemos (de 0 a N)
		// en este caso 0 es el t铆tulo de la secci贸n, 1 la cotizaci贸n comprador
		// y 2 vendedor.
		if dollar && i == 1 {
			buy := innerS.Text()
		}
		if dollar && i == 2 {
			sell := innerS.Text()
			// finalmente reseteamos el contador, esto nos garantiza que ignoramos los siguientes
			// nodos si los hubiese, esto es un detalle de esta implementaci贸n en particular.
			dollar = false
		}
	}

Una vez que hemos definido nuestra funci贸n de busqueda dentro del HTML la invocaremos utilizando el m茅todo Find del Document, para este caso filtraremos “tags <tr> dentro de nodos con id billetes, dado que esto devuelve un set filtrado, podemos invocar el m茅todo Each que recibe una funci贸n con la misma firma que nuestra funci贸n an贸nima y la ejecuta una vez por cada nodo del resultado, pasando oportunamente la posici贸n del nodo en la lista de resultados y un objeto que lo representa.

Para nuestro ejemplo hicimos una funci贸n an贸nima intermedia que tomar谩 la selecci贸n pasada y dentro de la misma filtrar谩 por los nodos <td> antes de invocar extractUSD en cada uno de ellos.

	// Find the review items
	doc.Find("#billetes tr").Each(func(i int, s *goquery.Selection) {
		s.Find("td").Each(extractUSD)
	})

Para terminar, vamos a convertir el resultado obtenido a un formato num茅rico apto para manejar dinero, Decimal (En ingl茅s) qe es un formato que nos permite guardar valores que reprensentan dinero y operar con ellos sin tener las p茅rdidas de precisi贸n t铆picas del rendondeo de numeros con coma flotante

// El banco utiliza `,` como indica la localizaci贸n de Argentina, pero la computadora
// espera `.`
sell := strings.Replace(sell, ",", ".", -1)
// obtendremos entonces el decimal con un constructor que espera una representaci贸n textual
// del n煤mero a convertir.
numericSell, err := decimal.NewFromString(sell)
if err != nil {
	log.Fatalf("no se puede convertir el valor de venta a Decimal: %v", err)
}

Finalmente, podremos ver el programa entero en un formato simp谩tico para ejecutar con go run de la siguiente manera go run main.go

El archivo main.go cuya salida al ser ejecutado es (al d铆a de hoy) (Nacion) Compra: 55.50 , Venta: 59.00

package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/PuerkitoBio/goquery"
	"github.com/shopspring/decimal"
)

const bnaURL = "http://www.bna.com.ar/Personas"
const USD = "Dolar U.S.A"

func main() {
	res, err := http.Get(bnaURL)
	if err != nil {
		log.Fatalf("getting bna website: %v", err)
	}

	defer res.Body.Close()
	if res.StatusCode != 200 {
		log.Printf("c贸digo de estado de la petici贸n inesperado: %d %s", res.StatusCode, res.Status)
		return
	}

	var buy, sell string
	var dollar bool

	// Una selecci贸n es el resultado de un filtro o b煤squeda dentro del DOM
	// en ete caso dicho filtro se har谩 mas adelante y el resultado se pasar谩
	// a esta funci贸n an贸nima.
	extractUSD := func(i int, innerS *goquery.Selection) {
		// Buscamos un elemento con la clase y cuyo texto tenga lo que buscamos, este criterio
		// lo obtuvimos de analizar el c贸digo HTML de la pagina detenidamente el la
		// secci贸n que nos interesa.
		if innerS.HasClass("tit") && innerS.Text() == USD {
			// utilizamos el flag dollar para denotar que en efecto este nodo es el inicio
			// de los datos de cotizaci贸n, si es true significa que los valores a continuaci贸n son la cotizaci贸n
			dollar = true
			return
		}
		// i indica cual de los nodos de esta selecci贸n tenemos (de 0 a N)
		// en este caso 0 es el t铆tulo de la secci贸n, 1 la cotizaci贸n comprador
		// y 2 vendedor.
		if dollar && i == 1 {
			buy = innerS.Text()
		}
		if dollar && i == 2 {
			sell = innerS.Text()
			// finalmente reseteamos el contador, esto nos garantiza que ignoramos los siguientes
			// nodos si los hubiese, esto es un detalle de esta implementaci贸n en particular.
			dollar = false
		}
	}

	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Printf("reading site body: %v", err)
		return
	}

	// Find the review items
	doc.Find("#billetes tr").Each(func(i int, s *goquery.Selection) {
		s.Find("td").Each(extractUSD)
	})

	// El banco utiliza `,` como indica la localizaci贸n de Argentina, pero la computadora
	// espera `.`
	sell = strings.Replace(sell, ",", ".", -1)
	buy = strings.Replace(buy, ",", ".", -1)

	// obtendremos entonces el decimal con un constructor que espera una representaci贸n textual
	// del n煤mero a convertir.
	numericSell, err := decimal.NewFromString(sell)
	if err != nil {
		log.Printf("no se puede convertir el valor de venta a Decimal: %v", err)
		return
	}
	numericBuy, err := decimal.NewFromString(buy)
	if err != nil {
		log.Printf("no se puede convertir el valor de compra a Decimal: %v", err)
		return
	}

	fmt.Printf("(Nacion) Compra: %s , Venta: %s", numericBuy.StringFixedBank(2), numericSell.StringFixedBank(2))
}