go语言实现爬虫(一)

本项目适合在快速学习完go语言后的一个简单练手,主要达成以下功能:

  • 能够递归的抓取网页链接,通过递归深度进行控制停止
  • 支持爬取的并发控制,超时处理,以及其执行过程的暂停与恢复
  • 基于gin提供restful的api接口,包括启动,停止,暂停,恢复,查看状态,获取结果等
  • 提供grpc远程调用功能,并调研grpc-gateway,简单做一个学习案例

    注:本文仅总结了重要的部分,还需不断进行完善,具体项目点击链接查看。

页面解析

依赖模块golang.org/x/net/htmlExtract函数向给定URL发起HTTP GET请求,解析HTML并返回HTML文档中存在的链接,如果要在此部分添加或删除规则,那么可以修改此函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
func Extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}

doc, err := html.Parse(resp.Body)
defer 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 // ignore bad URLs
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
//forEachNode(doc, startElement, endElement)
return links, nil
}

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

var depth int

func startElement(n *html.Node) {
if n.Type == html.ElementNode {
fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
depth++
}
}

func endElement(n *html.Node) {
if n.Type == html.ElementNode {
depth--
fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
}
}

然后再根据广度优先遍历的思想新建一个函数breadthFirst,breadthFirst对每个worklist元素调用f,并将返回的内容添加到worklist中,对每一个元素,最多调用一次f。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func breadthFirst(f func(item string) []string, worklist []string) {
seen := make(map[string]bool)
for len(worklist) > 0 {
items := worklist
worklist = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
worklist = append(worklist, f(item)...)
}
}
}
}

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

//// extract same domain
//var filterList []string
//u, _ := url.Parse(urlStr)
//for _, link := range list {
// l, _ := url.Parse(link)
// if u.Hostname() == l.Hostname() {
// filterList = append(filterList, link)
// }
//}

return list
}

func main() {
urls := []string{"http://www.xxxx.com"}
breadthFirst(crawl, urls)
}

并发控制

在go语言中实现并发较为简单,只需要在函数定义前加上关键字go即可。对于爬虫而言,过高的并行度也不是一个好的做法,如何合理的控制并发速率也成为了当前需要控制的重点,下面介绍两种并发控制方式。

方式一

利用有缓冲的channel控制并发,并灵活的通过一个计数器n来控制程序自动结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// tokens is a counting semaphore used to
// enforce a limit of 20 concurrent requests.
var tokens = make(chan struct{}, 20)

func crawl(url string) []string {
fmt.Println(url)
tokens <- struct{}{} // acquire a token
list, err := links.Extract(url)
<-tokens // release the token
if err != nil {
log.Print(err)
}
return list
}

func main() {
worklist := make(chan []string)
var n int
url := "http://www.xxxx.com"

// start
n++
go func() { worklist <- []string{url} }()

// crawl the web concurrently
visited := make(map[string]bool)
for ; n > 0; n-- {
list := <-worklist
for _, link := range list {
if !visited[link] {
visited[link] = true
n++
go func(link string) {
worklist <- crawl(link)
}(link)
}
}
}
}

方式二

使用一定数量常驻goroutine控制并发,并在channel中没有数据后一定时间内超时退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func main() {
worklist := make(chan []string) // lists of URLs, may have duplicates
unseenLinks := make(chan string) // de-duplicated URLs
// Add command-line arguments to worklist.
go func() { worklist <- os.Args[1:] }()
// Create 20 crawler goroutines to fetch each unseen link.
for i := 0; i < 20; i++ {
go func() {
for link := range unseenLinks {
foundLinks := crawl(link)
go func() { worklist <- foundLinks }()
}
}()
}
// The main goroutine de-duplicates worklist items
// and sends the unseen ones to the crawlers.
seen := make(map[string]bool)
for {
select {
case list := <-worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
unseenLinks <- link
}
}
case <- time.After(3 * time.Second)
fmt.Println("Exit, timeout")
return
}
}
}

递归深度

要控制抓取的递归深度可以从crawl函数入手,将url进行封装,添加一个depth属性,当获取这个url的下一层链接时则对depth进行加一。另外还需添加一个信号控制channel,当递归层数满足要求时,发送信号控制程序退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type work struct {
url string
depth int
}

func crawl(w work, quit chan struct{}) []work {
fmt.Printf("depth: %d, url: %s\n", w.depth, w.url)
if w.depth > 3 {
quit <- struct{}{}
return nil
}
urls, err := links.Extract(w.url)
if err != nil {
log.Print(err)
}

var works []work
for _, url := range urls {
works = append(works, work{url, w.depth + 1})
}

return works
}