Création d'un robot d'exploration Web avec Go pour détecter les titres en double

Dans cet article, j'écrirai un petit robot d'exploration Web. Je n'étais pas sûr si mon site Web avait de beaux titres de page à l'échelle du site, et si j'avais des titres en double, j'ai donc écrit ce petit utilitaire pour le savoir.

Je vais commencer par écrire une commande qui accepte une page de départ à partir de la ligne de commande, etsuit tout lien qui a l'url d'origine comme base.

Plus tard, j'ajouterai un indicateur facultatif pour détecter si le site atitres en double, quelque chose qui pourrait être utile à des fins de référencement.

Présentationgolang.org/x/net/html

Legolang.org/xLes packages sont des packages gérés par l'équipe Go, mais ils ne font pas partie de la bibliothèque standard pour diverses raisons.

Peut-être qu'ils sont trop spécifiques et ne seront pas utilisés par la majorité des développeurs Go. Peut-être qu'ils sont encore en cours de développement ou expérimentaux, donc ils ne peuvent pas être inclus dans la stdlib, qui doit tenir la promesse de Go 1.0 de ne pas faire de modifications rétrocompatibles - quand quelque chose entre dans la stdlib, c'est «final».

L'un de ces packages estgolang.org/x/net/html.

Pour l'installer, exécutez

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

Dans cet article, je vais utiliser en particulierhtml.Parse()fonction, et lehtml.Nodestruct:

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)

Le premier programme ci-dessous accepte une URL et calcule les liens uniques qu'il trouve, donnant une sortie comme celle-ci:

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

Commençons parmain(), car il montre un aperçu de haut niveau de ce que fait le programme.

  1. obtient leurlà partir des arguments CLI en utilisant `os.Args [1]
  2. instancievisited, une carte avec des chaînes de clé et une chaîne de valeur, où nous stockons l'URL et le titre des pages du site
  3. appelsanalyze().urlest passé 2 fois, car la fonction est récursive et le second paramètre sert d'URL de base pour les appels récursifs
  4. itère sur levisitedmap, qui a été transmise par référence àanalyze()et a maintenant toutes les valeurs remplies, afin que nous puissions les imprimer

    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) } }

Assez simple? Rentrons à l'intérieuranalyze(). Première chose, ça appelleparse(), qui, étant donné une chaîne pointant vers une URL, la récupérera et l'analysera en retournant un pointeur html.Node et une erreur.

func parse (chaîne d'url) (* html.Node, erreur)

Après avoir vérifié le succès,analyze()récupère le titre de la page en utilisantpageTitle(), qui a donné une référence à un html.Node, le scanne jusqu'à ce qu'il trouve la balise de titre, puis il renvoie sa valeur.

func pageTitle (n * html.Node) chaîne

Une fois que nous avons le titre de la page, nous pouvons l'ajouter auvisitedcarte.

Ensuite, nous obtenons tous les liens de page en appelantpageLinks(), qui étant donné le nœud de la page de départ, il analysera récursivement tous les nœuds de la page et retournera une liste de liens uniques trouvés (pas de doublons).

func pageLinks (links [] chaîne, n * html.Node) [] chaîne

Une fois que nous avons obtenu la tranche de liens, nous les parcourons, et nous faisons un petit contrôle: sivisitedne contient pas encore la page, cela signifie que nous ne l'avons pas encore visitée, et le lien doit avoirbaseurlcomme préfixe. Si ces 2 affirmations sont confirmées, nous pouvons appeleranalyze()avec l'url du lien.

// 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()utilise legolang.org/x/net/htmlAPI que nous avons présentées ci-dessus. À la première itération,nest le<html>nœud. Nous recherchons la balise de titre. La première itération ne satisfait jamais cela, nous allons donc faire une boucle sur le premier enfant de<html>d'abord, et ses frères et sœurs plus tard, et nous appelonspageTitle()en passant récursivement le nouveau nœud.

Finalement, nous arriverons à la<title>étiquette: unhtml.Nodeinstance avecTypeégal àhtml.ElementNode(voir ci-dessus) etDataégal àtitle, et nous retournons son contenu en accédant à sonFirstChild.Databiens

// 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()n'est pas très différent depageTitle(), sauf qu'il ne s'arrête pas lorsqu'il trouve le premier élément, mais recherche chaque lien, il faut donc passer lelinksslice comme paramètre de cette fonction récursive. Les liens sont découverts en vérifiant lehtml.Nodeahtml.ElementNode Type,Datadoit êtreaet aussi ils doivent avoir unAttravecKey href, sinon cela pourrait être une ancre.

// 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()est une fonction utilitaire appelée parpageLinks()pour vérifier l'unicité de la tranche.

// 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 dernière fonction estparse(). Il utilise lehttpfonctionnalité stdlib pour obtenir le contenu d'une URL (http.Get()) puis utilise legolang.org/x/net/html html.Parse()API pour analyser le corps de la réponse à partir de la requête HTTP, renvoyant unhtml.Noderéférence.

// 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
}

Détecter les titres en double

Puisque je veuxutiliser un indicateur de ligne de commandepour vérifier les doublons, je vais modifier légèrement la façon dont l'URL est transmise au programme: au lieu d'utiliseros.Args, Je transmettrai également l'URL en utilisant un indicateur.

Ceci est le modifiémain()fonction, avec des indicateurs d'analyse avant de faire le travail habituel de préparation duanalyze()exécution et impression des valeurs. De plus, à la fin, il y a un chèque pour ledupindicateur booléen, et s'il est vrai, il s'exécutecheckDuplicates().

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>)
}

}

checkDuplicatesprend la carte des url -> titres et itère dessus pour construire la sienneuniquesmap, que cette fois a le titre de la page comme clé, afin que nous puissions vérifieruniques[title] == ""pour déterminer si un titre est déjà là, et nous pouvons accéder à la première page qui a été saisie avec ce titre en imprimantuniques[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édits

Le langage de programmation Golivre de Donovan et Kernighan utilise un robot d'exploration comme exemple tout au long du livre, le changeant dans différents chapitres pour introduire de nouveaux concepts. Le code fourni dans cet article s'inspire du livre.


Plus de tutoriels go: