使用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撰写的这本书在整本书中均以网络爬虫为例,并在不同的章节中对其进行了更改,以引入新的概念。本文提供的代码从本书中汲取了灵感。


更多教程: