Creación de un rastreador web con Go para detectar títulos duplicados

En este artículo escribiré un pequeño rastreador web. No estaba seguro de si mi sitio web tenía buenos títulos de página en todo el sitio y si tenía títulos duplicados, así que escribí esta pequeña utilidad para averiguarlo.

Comenzaré escribiendo un comando que acepte una página de inicio desde la línea de comando, ysigue cualquier enlace que tenga la URL original como base.

Más tarde, agregaré una marca opcional para detectar si el sitio tienetítulos duplicados, algo que podría ser útil para fines de SEO.

Presentandogolang.org/x/net/html

losgolang.org/xLos paquetes son paquetes mantenidos por el equipo de Go, pero no forman parte de la biblioteca estándar por varias razones.

Tal vez sean demasiado específicos y no serán utilizados por la mayoría de los desarrolladores de Go. Tal vez todavía estén en desarrollo o sean experimentales, por lo que no se pueden incluir en stdlib, que debe cumplir con la promesa de Go 1.0 de que no hay cambios incompatibles con versiones anteriores: cuando algo entra en stdlib, es "final".

Uno de estos paquetes esgolang.org/x/net/html.

Para instalarlo, ejecute

go get golang.org/x/net...

En este artículo usaré en particularhtml.Parse()función, y lahtml.Nodeestructura:

package html

type Node struct { Type NodeType Data string Attr []Attribute FirstChild, NextSibling *node }

type NodeType int32

const ( ErrorNode NodeType = iota TextNode DocumentNode ElementNode CommentNode DoctypeNode )

type Attribute struct { Key, Val string }

func Parse(r io.Reader) (*Node, error)

El primer programa aquí abajo acepta una URL y calcula los enlaces únicos que encuentra, dando un resultado como este:

http://localhost:1313/go-filesystem-structure/ -> Filesystem Structure of a Go project
http://localhost:1313/golang-measure-time/ -> Measuring execution time in a Go program
http://localhost:1313/go-tutorial-fortune/ -> Go CLI tutorial: fortune clone
http://localhost:1313/go-tutorial-lolcat/ -> Build a Command Line app with Go: lolcat

Empecemos pormain(), ya que muestra una descripción general de alto nivel de lo que hace el programa.

  1. obtiene elurlde los argumentos de CLI usando `os.Args [1]
  2. instanciavisited, un mapa con cadenas clave y cadena de valor, donde almacenaremos la URL y el título de las páginas del sitio.
  3. llamadasanalyze().urlse pasa 2 veces, ya que la función es recursiva y el segundo parámetro sirve como la URL base para las llamadas recursivas
  4. itera sobre elvisitedmapa, que se pasó por referencia aanalyze()y ahora tiene todos los valores llenos, por lo que podemos imprimirlos

    package main
    

    import ( “fmt” “net/http” “os” “strings”

    <span style="color:#e6db74">"golang.org/x/net/html"</span>
    

    )

    func main() { url := os.Args[1] if url == “” { fmt.Println(“Usage: webcrawler &lt;url&gt;) os.Exit(1) } visited := map[string]string{} analyze(url, url, &visited) for k, v := range visited { fmt.Printf(”%s -> %s\n”, k, v) } }

¿Suficientemente simple? Vamos a entraranalyze(). Lo primero que llamaparse(), que, dada una cadena que apunta a una URL, la buscará y analizará devolviendo un puntero html.Node y un error.

func parse (cadena de URL) (* html.Node, error)

Después de verificar el éxito,analyze()obtiene el título de la página usandopageTitle(), que da una referencia a un html.Node, lo escanea hasta que encuentra la etiqueta del título y luego devuelve su valor.

func pageTitle (n * html.Node) cadena

Una vez que tenemos el título de la página, podemos agregarlo alvisitedmapa.

A continuación, obtenemos todos los enlaces de la página llamandopageLinks(), que dado el nodo de la página de inicio, escaneará recursivamente todos los nodos de la página y devolverá una lista de enlaces únicos encontrados (sin duplicados).

func pageLinks (enlaces [] cadena, n * html.Node) [] cadena

Una vez que obtenemos el segmento de enlaces, iteramos sobre ellos y hacemos una pequeña verificación: sivisitedaún no contiene la página, significa que aún no la visitamos y el enlace debe tenerbaseurlcomo prefijo. Si se confirman esas 2 afirmaciones, podemos llamaranalyze()con la URL del enlace.

// analyze given a url and a basurl, recoursively scans the page
// following all the links and fills the `visited` map
func analyze(url, baseurl string, visited *map[string]string) {
	page, err := parse(url)
	if err != nil {
		fmt.Printf("Error getting page %s %s\n", url, err)
		return
	}
	title := pageTitle(page)
	(*visited)[url] = title
<span style="color:#75715e">//recursively find links

links := pageLinks(nil, page) for _, link := range links { if (*visited)[link] == “” && strings.HasPrefix(link, baseurl) { analyze(link, baseurl, visited) } } }

pageTitle()usa elgolang.org/x/net/htmlAPI que presentamos anteriormente. En la primera iteración,nes el<html>nodo. Buscamos la etiqueta del título. La primera iteración nunca satisface esto, así que vamos y recorremos el primer hijo de<html>primero, y sus hermanos después, y llamamospageTitle()pasando de forma recursiva el nuevo nodo.

Eventualmente llegaremos al<title>etiqueta: unhtml.Nodeinstancia conTypeigual ahtml.ElementNode(ver arriba) yDataigual atitle, y devolvemos su contenido accediendo a suFirstChild.Datapropiedad

// pageTitle given a reference to a html.Node, scans it until it
// finds the title tag, and returns its value
func pageTitle(n *html.Node) string {
	var title string
	if n.Type == html.ElementNode && n.Data == "title" {
		return n.FirstChild.Data
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		title = pageTitle(c)
		if title != "" {
			break
		}
	}
	return title
}

pageLinks()no es muy diferente apageTitle(), excepto que no se detiene cuando encuentra el primer elemento, sino que busca todos los enlaces, por lo que debemos pasar ellinksslice como parámetro para esta función recursiva. Los enlaces se descubren marcando elhtml.Nodeposeehtml.ElementNode Type,Datadebe seray también deben tener unAttrconKey href, ya que de lo contrario podría ser un ancla.

// pageLinks will recursively scan a `html.Node` and will return
// a list of links found, with no duplicates
func pageLinks(links []string, n *html.Node) []string {
	if n.Type == html.ElementNode && n.Data == "a" {
		for _, a := range n.Attr {
			if a.Key == "href" {
				if !sliceContains(links, a.Val) {
					links = append(links, a.Val)
				}
			}
		}
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		links = pageLinks(links, c)
	}
	return links
}

sliceContains()es una función de utilidad llamada porpageLinks()para comprobar la unicidad en la rebanada.

// sliceContains returns true if `slice` contains `value`
func sliceContains(slice []string, value string) bool {
	for _, v := range slice {
		if v == value {
			return true
		}
	}
	return false
}

La última función esparse(). Usa elhttpfuncionalidad stdlib para obtener el contenido de una URL (http.Get()) y luego usa elgolang.org/x/net/html html.Parse()API para analizar el cuerpo de la respuesta de la solicitud HTTP, devolviendo unhtml.Nodereferencia.

// parse given a string pointing to a URL will fetch and parse it
// returning an html.Node pointer
func parse(url string) (*html.Node, error) {
	r, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("Cannot get page")
	}
	b, err := html.Parse(r.Body)
	if err != nil {
		return nil, fmt.Errorf("Cannot parse page")
	}
	return b, err
}

Detectar títulos duplicados

Ya que quierousar una bandera de línea de comandopara verificar si hay duplicados, voy a cambiar ligeramente la forma en que se pasa la URL al programa: en lugar de usaros.Args, También pasaré la URL con una bandera.

Este es el modificadomain()función, con banderas analizando antes de hacer el trabajo habitual de preparar elanalyze()ejecución e impresión de valores. Además, al final hay un cheque para eldupbandera booleana, y si es verdadera se ejecutacheckDuplicates().

import (
	"flag"
//...
)

func main() { var url string var dup bool flag.StringVar(&url, “url”, “”, “the url to parse”) flag.BoolVar(&dup, “dup”, false, “if set, check for duplicates”) flag.Parse()

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">""</span> {
	<span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">PrintDefaults</span>()
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
}

<span style="color:#a6e22e">visited</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{}
<span style="color:#a6e22e">analyze</span>(<span style="color:#a6e22e">url</span>, <span style="color:#a6e22e">url</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">visited</span>)
<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">visited</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">"%s -&gt; %s\n"</span>, <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span>)
}

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">dup</span> {
	<span style="color:#a6e22e">checkDuplicates</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">visited</span>)
}

}

checkDuplicatestoma el mapa de url -> títulos y lo itera para construir su propiouniquesmap, que esta vez tiene el título de la página como clave, por lo que podemos verificaruniques[title] == ""para determinar si un título ya está allí, y podemos acceder a la primera página que se ingresó con ese título imprimiendouniques[title].

// checkDuplicates scans the visited map for pages with duplicate titles
// and writes a report
func checkDuplicates(visited *map[string]string) {
	found := false
	uniques := map[string]string{}
	fmt.Printf("\nChecking duplicates..\n")
	for link, title := range *visited {
		if uniques[title] == "" {
			uniques[title] = link
		} else {
			found = true
			fmt.Printf("Duplicate title \"%s\" in %s but already found in %s\n", title, link, uniques[title])
		}
	}
<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">found</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"No duplicates were found 😇"</span>)
}

}

Créditos

El lenguaje de programación GoEl libro de Donovan y Kernighan usa un rastreador web como ejemplo a lo largo del libro, cambiándolo en diferentes capítulos para introducir nuevos conceptos. El código proporcionado en este artículo se inspira en el libro.


Más tutoriales de go: