golang:併發的 Web 爬蟲

《GO程序設計語言》設計中案例,僅作爲筆記進行收藏。Web 爬蟲 只是簡單獲取頁面屬性href中鏈接。

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"golang.org/x/net/html"
)

func main() {
	// 可能有重複的 URL 列表
	worklist := make(chan []string)
	// 去重後的 URL 列表
	unseenLinks := make(chan string)

	// 向任務列表中添加命令行參數
	go func() { worklist <- os.Args[1:] }()

	// 創建20個爬蟲 goroutine 來獲取每個不可見鏈接
	for i := 0; i < 20; i++ {
		go func() {
			for link := range unseenLinks {
				foundLinks := crawl(link)
				go func() { worklist <- foundLinks }()
			}
		}()
	}

	// 主 goroutine 對 URL 列表進行去重
	// 並把沒有爬過的條目發送給爬蟲程序
	seen := make(map[string]bool)
	for list := range worklist {
		for _, link := range list {
			if !seen[link] {
				seen[link] = true
				unseenLinks <- link
			}
		}
	}
}

func crawl(url string) []string {
	fmt.Println(url)
	list, err := Extract(url)
	if err != nil {
		log.Print(err)
	}
	return list
}

// Extract 函數向給定 URL 發起 HTTP GET 請求
// 解析 HTML 並返回 HTML 文檔中存在的鏈接
// 網頁爬蟲的核心是解決圖的遍歷
func Extract(url string) ([]string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("getting %s:%s", url, resp.Status)
	}

	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("parsing %s as HTML:%v", url, err)
	}

	var links []string
	visitNode := func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "a" {
			for _, a := range n.Attr {
				if a.Key != "href" {
					continue
				}

				link, err := resp.Request.URL.Parse(a.Val)
				if err != nil {
					continue // 忽略不合法的 URL
				}

				fmt.Println(link)
				links = append(links, link.String())
			}
		}
	}

	forEachNode(doc, visitNode, nil)
	return links, nil
}

// forEachNode 調用 pre(x) 和  post(x) 遍歷以n爲根的樹種的每個節點x
// 兩個函數是可選的
// pre 在子節點被訪問前(前序)調用
// post 在訪問後(後序)調用
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
	if pre != nil {
		pre(n)
	}

	for c := n.FirstChild; c != nil; c = c.NextSibling {
		forEachNode(c, pre, post)
	}

	if post != nil {
		post(n)
	}
}

編譯後,就可以使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章