重複するタイトルを検出するためにGoを使用してWebクローラーを構築する

この記事では、小さなWebクローラーを作成します。私のウェブサイトにサイト全体で素敵なページタイトルがあるかどうか、そして重複したタイトルがあるかどうかわからなかったので、この小さなユーティリティを書いて調べました。

コマンドラインから開始ページを受け入れるコマンドを作成することから始めます。元のURLをベースとするリンクをたどる

後で、サイトにあるかどうかを検出するためのオプションのフラグを追加します重複するタイトル、SEOの目的に役立つかもしれない何か。

紹介golang.org/x/net/html

ザ・golang.org/xパッケージはGoチームによって管理されているパッケージですが、さまざまな理由から標準ライブラリの一部ではありません。

たぶん、それらはあまりにも具体的で、Go開発者の大多数によって使用されることはないでしょう。おそらくそれらはまだ開発中または実験中であるため、stdlibに含めることはできません。これは、後方互換性のない変更がないというGo 1.0の約束を果たす必要があります。stdlibに何かが入ると、それは「最終」です。

これらのパッケージの1つは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`os.Args [1]を使用してCLI引数から
  2. インスタンス化visited、キー文字列と値文字列を含むマップ。ここにURLとサイトページのタイトルを保存します
  3. 呼び出しanalyze()url関数は再帰的であり、2番目のパラメーターは再帰呼び出しのベースURLとして機能するため、は2回渡されます。
  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、error)

成功を確認した後、analyze()を使用してページタイトルを取得しますpageTitle()は、html.Nodeへの参照を指定し、titleタグが見つかるまでスキャンしてから、その値を返します。

func pageTitle(n * html.Node)文字列

ページタイトルを取得したら、それをに追加できますvisited地図。

次に、を呼び出してすべてのページリンクを取得しますpageLinks()、開始ページノードを指定すると、すべてのページノードを再帰的にスキャンし、見つかった一意のリンクのリストを返します(重複はありません)。

func pageLinks(links [] string、n * html.Node)[] string

リンクスライスを取得したら、それらを繰り返し処理し、少しチェックします。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/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()ただし、最初のアイテムが見つかっても停止せず、すべてのリンクを検索するため、linksこの再帰関数のパラメーターとしてのスライス。リンクはチェックすることによって発見されますhtml.Node持っているhtml.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()。それは使用しますhttpURLのコンテンツを取得するstdlib機能(http.Get())そして、golang.org/x/net/html html.Parse()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ブールフラグ。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>)
}

}

checkDuplicatesURL->タイトルのマップを取得し、それを繰り返して独自のマップを作成します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による本は、本全体の例としてWebクローラーを使用し、新しい概念を導入するためにさまざまな章で変更しています。この記事で提供されるコードは、本からインスピレーションを得ています。


その他のチュートリアル: