使用Go構建Web爬網程序以檢測重複的標題

在本文中,我將編寫一個小型Web搜尋器。我不確定我的網站是否在整個站點範圍內都有漂亮的頁面標題,以及是否有重複的標題,因此我編寫了這個小實用程序來查找。

我將從編寫一個從命令行接受起始頁的命令開始,然後跟隨任何以原始網址為基礎的鏈接

稍後,我將添加一個可選標誌以檢測該網站是否具有重複標題,可能對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被傳遞兩次,因為函數是遞歸的,第二個參數用作遞歸調用的基本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字符串)(* html.Node,錯誤)

檢查成功後,analyze()使用獲取頁面標題pageTitle(),它給出了對html.Node的引用,對其進行掃描,直到找到title標記,然後返回其值。

func pageTitle(n * html.Node)字符串

有了頁面標題後,我們可以將其添加到visited地圖。

接下來,我們通過調用獲得所有頁面鏈接pageLinks(),它具有給定的起始頁面節點,它將遞歸掃描所有頁面節點,並返回找到的唯一鏈接的列表(無重複)。

func pageLinks(鏈接[] string,n * html.Node)[] string

一旦獲得鏈接切片,我們將對其進行迭代,然後進行一些檢查:visited尚未包含該頁面,這意味著我們尚未訪問該頁面,並且鏈接必須包含baseurl作為前綴。如果這兩個斷言都得到確認,我們可以致電analyze()與鏈接網址。

// 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/html我們在上面介紹的API。在第一次迭代中,n是個<html>節點。我們正在尋找標題標籤。第一次迭代永遠不會滿足此要求,因此我們遍歷了<html>首先,然後是它的兄弟姐妹,我們稱pageTitle()遞歸地傳遞新節點。

最終,我們將到達<title>標籤:html.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.Nodehtml.ElementNode TypeData必須是a而且他們必須有一個AttrKey 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()解析來自HTTP請求的響應主體的API,返回一個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布爾標誌,如果為true,則運行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-> title的地圖並對其進行迭代以構建自己的地圖uniques地圖,這次以頁面標題為鍵,因此我們可以檢查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編程語言Donovan和Kernighan撰寫的這本書在整本書中均以網絡爬蟲為例,在不同的章節中對其進行了更改,以引入新的概念。本文提供的代碼從本書中汲取了靈感。


更多教程: