Создание веб-краулера с Go для обнаружения повторяющихся заголовков

В этой статье я напишу небольшой веб-сканер. Я не был уверен, есть ли у моего веб-сайта хорошие заголовки страниц по всему сайту, и были ли у меня повторяющиеся заголовки, поэтому я написал эту небольшую утилиту, чтобы узнать.

Я начну с написания команды, которая принимает стартовую страницу из командной строки, иследует по любой ссылке, имеющей исходный URL-адрес в качестве основы.

Позже я добавлю необязательный флаг, чтобы определить, есть ли на сайтеповторяющиеся заголовки, что-то, что может быть полезно для целей SEO.

Представляемgolang.org/x/net/html

Вgolang.org/xПакеты - это пакеты, поддерживаемые командой Go, но они не являются частью стандартной библиотеки по разным причинам.

Возможно, они слишком специфичны и не будут использоваться большинством разработчиков Go. Возможно, они все еще находятся в стадии разработки или экспериментальной разработки, поэтому их нельзя включить в stdlib, который должен соответствовать обещанию Go 1.0 об отсутствии обратно несовместимых изменений - когда что-то попадает в stdlib, это «окончательно».

Один из этих пакетовgolang.org/x/net/html.

Чтобы установить его, выполните

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

В этой статье я, в частности, буду использоватьhtml.Parse()функция, аhtml.Nodeструктура:

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)

Первая программа, представленная ниже, принимает URL-адрес и вычисляет уникальные ссылки, которые она находит, давая следующий результат:

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

Начнем сmain(), поскольку он показывает общий обзор того, что делает программа.

  1. получаетurlиз аргументов CLI с помощью `os.Args [1]
  2. создает экземплярыvisited, карта с ключевыми строками и строкой значений, где мы будем хранить URL-адрес и заголовок страниц сайта.
  3. звонкиanalyze().urlпередается 2 раза, так как функция является рекурсивной, а второй параметр служит базовым URL-адресом для рекурсивных вызовов
  4. повторяетvisitedкарта, которая была передана по ссылке наanalyze()и теперь все значения заполнены, поэтому мы можем их распечатать

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

Достаточно просто? Давай внутрьanalyze(). Во-первых, он звонитparse(), который с учетом строки, указывающей на URL-адрес, будет извлекать и анализировать его, возвращая указатель html.Node и ошибку.

func parse (url string) (* html.Node, ошибка)

После проверки на успех,analyze()извлекает заголовок страницы, используяpageTitle(), который дает ссылку на html.Node, сканирует его, пока не найдет тег заголовка, а затем возвращает его значение.

func pageTitle (n * html.Node) строка

Когда у нас есть заголовок страницы, мы можем добавить его вvisitedкарта.

Затем мы получаем все ссылки на страницы, вызываяpageLinks(), для которого задан узел начальной страницы, он рекурсивно просканирует все узлы страницы и вернет список найденных уникальных ссылок (без дубликатов).

func pageLinks (links [] строка, n * html.Node) [] строка

Получив фрагмент ссылок, мы перебираем их и делаем небольшую проверку: еслиvisitedеще не содержит страницу, значит, мы ее еще не посещали, а ссылка должна иметьbaseurlкак префикс. Если эти 2 утверждения подтвердятся, мы можем позвонитьanalyze()с URL-адресом ссылки.

// 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()используетgolang.org/x/net/htmlAPI, которые мы представили выше. На первой итерацииnэто<html>узел. Ищем тег заголовка. Первая итерация этого никогда не удовлетворяет, поэтому мы перебираем в цикле первого дочернего элемента<html>сначала, а потом его братья и сестры, и мы называемpageTitle()рекурсивно передать новый узел.

В конце концов мы перейдем к<title>tag: anhtml.Nodeэкземпляр сTypeравноhtml.ElementNode(см. выше) иDataравноtitle, и мы возвращаем его содержимое, обращаясь к егоFirstChild.Dataсвойство

// 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()не сильно отличается отpageTitle(), за исключением того, что он не останавливается, когда находит первый элемент, а просматривает каждую ссылку, поэтому мы должны передатьlinksslice как параметр для этой рекурсивной функции. Ссылки обнаруживаются путем проверкиhtml.Nodeимеетhtml.ElementNode Type,Dataдолжно бытьaа также они должны иметьAttrсKey href, иначе это мог бы быть якорь.

// 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()это служебная функция, вызываемаяpageLinks()проверить уникальность в срезе.

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

Последняя функцияparse(). Он используетhttpstdlib для получения содержимого URL-адреса (http.Get()), а затем используетgolang.org/x/net/html html.Parse()API для синтаксического анализа тела ответа из HTTP-запроса, возвращающегоhtml.Nodeссылка.

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

Обнаруживать повторяющиеся заголовки

Так как я хочуиспользовать флаг командной строкичтобы проверить наличие дубликатов, я собираюсь немного изменить способ передачи URL в программу: вместо использованияos.Args, Я тоже передам URL с помощью флага.

Это модифицированныйmain()функция с синтаксическим анализом флагов перед выполнением обычной работы по подготовкеanalyze()оформление и печать ценностей. Кроме того, в конце есть чек наdupлогический флаг, и если истина, он запускаетсяcheckDuplicates().

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

}

checkDuplicatesберет карту URL -> заголовков и перебирает ее, чтобы построить свою собственнуюuniquesmap, что на этот раз в качестве ключа используется заголовок страницы, поэтому мы можем проверитьuniques[title] == ""чтобы определить, есть ли уже заголовок, и мы можем получить доступ к первой странице, которая была введена с этим заголовком, распечатавuniques[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>)
}

}

Кредиты

Язык программирования GoКнига Донована и Кернигана использует поискового робота в качестве примера на протяжении всей книги, изменяя его в разных главах, чтобы представить новые концепции. Код, представленный в этой статье, вдохновлен книгой.


Больше руководств по go: