前言

​ 学习了一段时间的go,觉得自己应该实际上手写一些东西,之前一直使用在三只师傅python写的jsfinder,就想着用go写一个类似jsfinder的爬虫。

steps1

首先实现简单访问并读取页面内容的功能,主要使用net/http模块。

1
2
3
4
5
6
7
8
9
10
11
12
func main(){
resp,err:=http.Get("https://www.lenovo.com.cn")
if err!=nil{
log.Fatal(err)
}
if resp.StatusCode!=200 {
log.Fatal(err)
}
doc,err:=ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s",doc)
}

ioutil.ReadAll 是一个常用的数据读取方法,经常用来读取http请求的response数据,或者读取文件数据。

可以看到正常输出了页面内容。

step2

​ 第二步我们需要对页面进行解析,并提取页面中的js链接。我们可以使用go自带的regexp来进行正则匹配(类似python),这里我使用很方便的第三方包goquery读取HTML代码。

​ goquery是一个使用go语言写成的HTML解析库,可以让你像jQuery那样的方式来操作DOM文档,使用起来非常的简便。

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
package main

import (
"fmt"
"net/http"

"github.com/PuerkitoBio/goquery"
)



func main(){
resp,err:=http.Get("https://www.lenovo.com.cn")
if err!=nil{
return
}
if resp.StatusCode!=200 {
return
}

doc,err:=goquery.NewDocumentFromReader(resp.Body)
// NewDocumentFromReader:读取字符串的HTML代码
if err!=nil{
return
}
resp.Body.Close()

//Find函数是查找HTML里面所有符合要求的标签。
doc.Find("a").Each(func(i int, s *goquery.Selection) {
href, ex := s.Attr("href")
// 使用Attr获取数据所在HTML代码的href属性
if ex {
fmt.Println(href)
}
})
}

可以看到这里成功爬取到了页面的js链接。

当然这里还有一个更好用的第三方包:github.com/jackdanger/collectlinks 可以提取网页中所有的链接。

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
package main

import (
"fmt"

"github.com/jackdanger/collectlinks"

"net/http"
)



func main() {
resp, err := http.Get("https://www.lenovo.com.cn")
if err != nil {
return
}
if resp.StatusCode != 200 {
return
}

links := collectlinks.All(resp.Body)
for _, link := range links {
fmt.Println(link)
}

resp.Body.Close()
}

使用下来感觉要比自己写规则爬的全很多。

step3

​ 第三步我们要对获取到的链接进行处理,首先转化为绝对链接,然后提取出其中的子域名经过去重之后存入新的数组队列中。

我们这里封装三个函数,分别用来转化绝对路径、判断子域名和去重:

1
2
3
4
5
6
7
8
9
10
11
func urlparse(href, base string) string {
uri, err := url.Parse(href)
if err != nil {
return " "
}
baseUrl, err := url.Parse(base)
if err != nil {
return " "
}
return baseUrl.ResolveReference(uri).String()
}

issubdomain

1
2
3
4
5
6
7
8
9
10
func isSubdomain(rawURL, domain string) bool {
reg,err:=regexp.MatchString(domain,rawURL)
if err!=nil{
log.Println(err)
}
if reg{
return true
}
return false
}

remove

去重这里参考了网上数组切片去重的方式。

1
2
3
4
5
6
7
8
9
10
11
func remove(languages []string) []string {
result := make([]string, 0, len(languages))
temp := map[string]struct{}{}
for _, item := range languages {
if _, ok := temp[item]; !ok {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}

​ 然后在主函数中再定义一个sublists数组,然后对收集到的js链接调用上述函数进行处理后,将结果保存到数组中输出。

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
64
65
66
67
68
69
70
71
package main

import (
"fmt"
"log"
"net/url"
"regexp"

"github.com/jackdanger/collectlinks"

"net/http"
)




func main() {
sublists := make([]string, 0)
first:="https://www.lenovo.com.cn"
resp, err := http.Get(first)
if err != nil {
log.Print(err)
}
if resp.StatusCode != 200 {
return
}

links := collectlinks.All(resp.Body)
resp.Body.Close()

for _, link := range links {
urls,err:=url.Parse(link)
if err!=nil {
log.Println(err)
continue
}
if isSubdomain(urls.Host,"lenovo.com"){
sublists=append(sublists,"http://"+urls.Host)
}
}
sublists=remove(sublists)
for _,sublist:=range sublists{
fmt.Println(sublist)
}

}


func isSubdomain(rawURL, domain string) bool {
reg,err:=regexp.MatchString(domain,rawURL)
if err!=nil{
log.Println(err)
}
if reg{
return true
}
return false
}


func remove(languages []string) []string {
result := make([]string, 0, len(languages))
temp := map[string]struct{}{}
for _, item := range languages {
if _, ok := temp[item]; !ok {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}

step4

第四步我们实现循环爬取的功能,首先我们将之前的主函数封装为crawl爬虫函数:

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
func crawl(uri string) []string {
fmt.Println(uri)
sublists := make([]string, 0)
resp, err := http.Get(uri)
if err != nil {
log.Print(err)
}
if resp.StatusCode != 200 {
log.Print(err)
}

links := collectlinks.All(resp.Body)
resp.Body.Close()

for _, link := range links {
urls,err:=url.Parse(link)
if err!=nil {
log.Println(err)
continue
}
if isSubdomain(urls.Host,"lenovo.com"){
sublists=append(sublists,"http://"+urls.Host)
}
}
sublists=remove(sublists)

return sublists
}

然后在主函数中进行循环爬取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
sublists:=make([]string,0)
seen:=make(map[string]bool)
sublists = append(sublists,"http://www.lenovo.com.cn")
for len(sublists) > 0 {
items := sublists
sublists = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
sublists = append(sublists, crawl(item)...)
}
}
}
}

这里爬了下jd,效果还是挺明显的:

​ 这里在爬取过程会遇到一个异常:dial tcp: lookup help.en.jd.com: no such host,

意思大概是获取到的域名不能解析,所以我们需要使用defer+recover来捕获异常。防止程序直接退出。

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
64
65
66
67
68
69
70
71
72
func crawl(uri string) []string {
fmt.Println(uri)
defer func(){
if err := recover(); err != nil{
return
}
}()
sublists := make([]string, 0)
resp, err1 := http.Get(uri)
if err1 != nil {
log.Print(err1)
}

links := collectlinks.All(resp.Body)
resp.Body.Close()

for _, link := range links {
urls,err:=url.Parse(link)
if err!=nil {
log.Println(err)
continue
}
if isSubdomain(urls.Host,"jd.com"){
sublists=append(sublists,"http://"+urls.Host)
}
}
sublists=remove(sublists)

return sublists
}


func main() {
sublists:=make([]string,0)
seen:=make(map[string]bool)
sublists = append(sublists,"https://www.jd.com")
for len(sublists) > 0 {
items := sublists
sublists = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
sublists=append(sublists,crawl(item)...)
}
}
}
}


func isSubdomain(rawURL, domain string) bool {
reg,err:=regexp.MatchString(domain,rawURL)
if err!=nil{
log.Println(err)
}
if reg{
return true
}
return false
}


func remove(languages []string) []string {
result := make([]string, 0, len(languages))
temp := map[string]struct{}{}
for _, item := range languages {
if _, ok := temp[item]; !ok {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}

step5

最后一步我们来实现并发功能:

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
package main

import (
"fmt"
"log"
"net/url"
"regexp"

"github.com/jackdanger/collectlinks"

"net/http"
)

func main() {
worklist := make(chan []string)
sublists := make([]string, 0)
seen := make(map[string]bool)
sublists = append(sublists, "https://www.jd.com")

go func() { worklist <- sublists }()

for list := range worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
go func(link string) {
worklist <- crawl(link)
}(link)
}
}
}
}

Step6

最后在主函数使用flag包获取命令行参数,传入函数执行即可。

1
2
3
flag.StringVar(&ur1, "u","","待爬url,如http://www.jd.com")
flag.StringVar(&domain, "s","","域名,如jd.com")
flag.Parse()

最后

完整代码:Shu1L/go_jsspider: 用go编写的简单爬取页面js的脚本 (github.com)