Xây dựng Trình thu thập thông tin web với Go để phát hiện các tiêu đề trùng lặp

Trong bài viết này, tôi sẽ viết một trình thu thập thông tin web nhỏ. Tôi không chắc liệu trang web của mình có tiêu đề trang đẹp trên toàn bộ trang web hay không và nếu tôi có tiêu đề trùng lặp, vì vậy tôi đã viết tiện ích nhỏ này để tìm hiểu.

Tôi sẽ bắt đầu bằng cách viết một lệnh chấp nhận một trang bắt đầu từ dòng lệnh vàtheo bất kỳ liên kết nào có url gốc làm cơ sở.

Sau đó, tôi sẽ thêm một cờ tùy chọn để phát hiện xem trang web cótrùng lặp tiêu đề, một cái gì đó có thể hữu ích cho mục đích SEO.

Giới thiệugolang.org/x/net/html

Cácgolang.org/xcác gói là các gói được duy trì bởi đội cờ vây, nhưng chúng không phải là một phần của thư viện chuẩn vì nhiều lý do khác nhau.

Có thể chúng quá cụ thể, sẽ không được đa số các nhà phát triển cờ vây sử dụng. Có thể chúng vẫn đang trong quá trình phát triển hoặc thử nghiệm, vì vậy chúng không thể được đưa vào stdlib, vốn phải tuân theo lời hứa của Go 1.0 là không có những thay đổi không tương thích ngược - khi một cái gì đó đi vào stdlib, đó là "cuối cùng".

Một trong những gói này làgolang.org/x/net/html.

Để cài đặt nó, hãy thực thi

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

Trong bài viết này, tôi sẽ đặc biệt sử dụnghtml.Parse()chức năng vàhtml.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)

Chương trình đầu tiên ở đây bên dưới chấp nhận một URL và tính toán các liên kết duy nhất mà nó tìm thấy, đưa ra kết quả như sau:

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

Hãy bắt đầu từmain(), vì nó cho thấy tổng quan cấp cao về những gì chương trình thực hiện.

  1. nhận đượcurltừ các args CLI bằng cách sử dụng `os.Args [1]
  2. khởi tạovisited, một bản đồ với các chuỗi khóa và chuỗi giá trị, nơi chúng tôi sẽ lưu trữ URL và tiêu đề của các trang web
  3. cuộc gọianalyze().urlđược truyền 2 lần, vì hàm là đệ quy và tham số thứ hai đóng vai trò là URL cơ sở cho các lệnh gọi đệ quy
  4. lặp lạivisitedbản đồ, được chuyển qua tham chiếu tớianalyze()và hiện đã điền tất cả các giá trị, vì vậy chúng tôi có thể in chúng

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

Đủ đơn giản? Vào trong đianalyze(). Điều đầu tiên, nó gọiparse(), được cung cấp một chuỗi trỏ đến một URL sẽ tìm nạp và phân tích cú pháp nó trả về một con trỏ html.Node và một lỗi.

phân tích cú pháp func (chuỗi url) (* html.Node, lỗi)

Sau khi kiểm tra thành công,analyze()tìm nạp tiêu đề trang bằng cách sử dụngpageTitle(), cung cấp một tham chiếu đến một mã html.Node, quét nó cho đến khi nó tìm thấy thẻ tiêu đề và sau đó nó trả về giá trị của nó.

chuỗi func pageTitle (n * html.Node)

Khi chúng tôi có tiêu đề trang, chúng tôi có thể thêm nó vàovisitedbản đồ.

Tiếp theo, chúng tôi nhận được tất cả các liên kết trang bằng cách gọipageLinks(), được cung cấp cho nút trang bắt đầu, nó sẽ quét đệ quy tất cả các nút trang và sẽ trả về danh sách các liên kết duy nhất được tìm thấy (không có bản sao).

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

Khi chúng tôi có lát liên kết, chúng tôi sẽ lặp lại chúng và kiểm tra một chút: nếuvisitedchưa chứa trang có nghĩa là chúng tôi chưa truy cập trang đó và liên kết phải cóbaseurldưới dạng tiền tố. Nếu 2 xác nhận đó được xác nhận, chúng tôi có thể gọianalyze()với url liên kết.

// 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()sử dụnggolang.org/x/net/htmlCác API mà chúng tôi đã giới thiệu ở trên. Ở lần lặp đầu tiên,n<html>nút. Chúng tôi đang tìm thẻ tiêu đề. Lần lặp đầu tiên không bao giờ thỏa mãn điều này, vì vậy chúng tôi đi và lặp lại phần con đầu tiên của<html>đầu tiên và anh chị em của nó sau đó, và chúng tôi gọipageTitle()đệ quy đi qua nút mới.

Cuối cùng, chúng tôi sẽ đến<title>tag: anhtml.Nodeví dụ vớiTypetương đương vớihtml.ElementNode(xem ở trên) vàDatatương đương vớititlevà chúng tôi trả lại nội dung của nó bằng cách truy cập vàoFirstChild.Databất động sản

// 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()không khác nhiều so vớipageTitle(), ngoại trừ việc nó không dừng lại khi tìm thấy mục đầu tiên, nhưng tìm kiếm mọi liên kết, vì vậy chúng ta phải chuyểnlinkslát cắt làm tham số cho hàm đệ quy này. Các liên kết được phát hiện bằng cách kiểm trahtml.Nodehtml.ElementNode Type,Datacần phảiavà họ cũng phải có mộtAttrvớiKey href, nếu không nó có thể là một mỏ neo.

// 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()là một hàm tiện ích được gọi bởipageLinks()để kiểm tra tính duy nhất trong lát cắt.

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

Chức năng cuối cùng làparse(). Nó sử dụnghttpchức năng stdlib để lấy nội dung của một URL (http.Get()) và sau đó sử dụnggolang.org/x/net/html html.Parse()API để phân tích cú pháp nội dung phản hồi từ yêu cầu HTTP, trả vềhtml.Nodetài liệu tham khảo.

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

Phát hiện các tiêu đề trùng lặp

Vì tôi muốnsử dụng cờ dòng lệnhđể kiểm tra các bản sao, tôi sẽ thay đổi một chút cách URL được chuyển đến chương trình: thay vì sử dụngos.Args, Tôi cũng sẽ chuyển URL bằng cách sử dụng cờ.

Đây là bản sửa đổimain(), với các cờ phân tích cú pháp trước khi thực hiện công việc thông thường là chuẩn bịanalyze()thực thi và in các giá trị. Ngoài ra, ở phần cuối, có một kiểm tra chodupcờ boolean và nếu đúng thì nó sẽ chạycheckDuplicates().

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

}

checkDuplicateslấy bản đồ url -> tiêu đề và lặp lại trên đó để tạouniquesbản đồ, lần này có tiêu đề trang là khóa, vì vậy chúng tôi có thể kiểm trauniques[title] == ""để xác định xem một tiêu đề đã có ở đó hay chưa và chúng tôi có thể truy cập trang đầu tiên đã được nhập với tiêu đề đó bằng cách inuniques[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>)
}

}

Tín dụng

Ngôn ngữ lập trình Gocuốn sách của Donovan và Kernighan sử dụng trình thu thập thông tin web làm ví dụ xuyên suốt cuốn sách, thay đổi nó trong các chương khác nhau để giới thiệu các khái niệm mới. Mã được cung cấp trong bài viết này lấy cảm hứng từ cuốn sách.


Các hướng dẫn về go khác: