Eslody

生活在树上-始终热爱大地-升入天空

  • 主页
  • 随笔
所有文章 关于我

Eslody

生活在树上-始终热爱大地-升入天空

  • 主页
  • 随笔

go指南——练习:web爬虫

2021-03-02

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

初始

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
73
74
75
76
77
78
79
80
81
82
83
84
package main

import (
"fmt"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
Crawl(u, depth-1, fetcher)
}
return
}

func main() {
Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
body string
urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

要求实现并行抓取和不重复抓取

 

实现不重复抓取

make一个map用以实现url爬取记录即可

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
73
74
75
76
77
78
79
80
81
82
83
84
85
package main

import (
"fmt"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
urlRecord[url]++
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
if _,exists := urlRecord[u];!exists{
Crawl(u, depth-1, fetcher)
}
}
}

func main() {
Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
//函数外初始化尽量用var
var urlRecord = make(map[string]int)

type fakeResult struct {
body string
urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

 

实现并行抓取+不重复抓取

一.使用Mutex+WaitGroup
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package main

import (
"fmt"
"sync"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
defer ur.wg.Done()
if depth <= 0 {
return
}
//setUrl
ur.mu.Lock()
ur.mp[url]++
ur.mu.Unlock()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
//getUrl
ur.mu.Lock()
_,exists := ur.mp[u]
ur.mu.Unlock()
if !exists{
ur.wg.Add(1)
go Crawl(u, depth-1, fetcher)
}
}

}

func main() {
ur.wg.Add(1)
Crawl("https://golang.org/", 4, fetcher)
ur.wg.Wait()
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
body string
urls []string
}

type urlRecord struct {
mp map[string]int
mu sync.Mutex //实现互斥访问map
wg sync.WaitGroup //保证所有goroutine都结束
}

var ur = urlRecord {mp:make(map[string]int)}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

二.使用通道channel

通道是go语言特性之一,coding时尽量利用好这个特性。笔者采用一个接收者多个发送者的方式,接收者阻塞在通道,发送者协程发送url,而直到所有发送者协程关闭,监测协程关闭通道,程序结束。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package main

import (
"fmt"
"sync"
"time"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

func worker(url string, c chan string, fetcher Fetcher) {
wg.Add(1)
defer wg.Done()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
//将urls写入通道
for _, u := range urls {
c <- u
}
}

// Crawl 对传入的通道内所有url进行递归爬取
func Crawl(c chan string, fetcher Fetcher) {
urlMap := make(map[string]int) //用bool作值也可
//开辟一个协程用于关闭通道
go func(chan string) {
time.Sleep(100 * time.Millisecond) //等待wg.Add
wg.Wait()
close(c)
}(c)
//持续读取通道内待爬取的url,直到通道关闭
for url := range c {
if _, exists := urlMap[url];!exists {
urlMap[url]++
go worker(url, c, fetcher) //处理显示url和body
}
}
}

func main() {
c := make(chan string)
//这里注意:由于开辟的通道是一种无缓冲通道,所以当对这个缓冲通道写的时候,会一直阻塞等到某个协程对这个缓冲通道读,所以这里需要开辟个goroutine对其进行写入
go func() {
c <- "https://golang.org/"
}()
Crawl(c, fetcher)
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
body string
urls []string
}

var wg sync.WaitGroup

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

 

注:本文仅为个人练习,不保证结果的准确及代码的简洁

赏

谢谢你请我吃糖果

  • go

扫一扫,分享到微信

微信分享二维码
正则表达式(Regular Expression)
printf执行原理
  1. 1. 初始
  2. 2. 实现不重复抓取
  3. 3. 实现并行抓取+不重复抓取
    1. 3.1. 一.使用Mutex+WaitGroup
    2. 3.2. 二.使用通道channel
© 2021 eslody
To be or not to be.
  • 所有文章
  • 关于我

tag:

  • 数据结构与算法
  • java
  • Linux
  • 操作系统
  • 计算机网络
  • 区块链
  • go
  • 工具
  • 分布式
  • 实习记录
  • 随笔
  • http
  • RPC
  • Gin

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • QUIC协议原理浅析

    2021-07-11

    #计算机网络#http

  • 基数树(Radix tree)和前缀树(Trie tree)

    2021-07-10

    #Gin

  • 转发://http2.0多路复用

    2021-07-08

    #http

  • bufio包原理解析

    2021-07-08

    #go

  • 为什么response.Body.Close()是必要的

    2021-06-29

    #go

  • 记一次代码优化的经历

    2021-06-25

    #实习记录#随笔

  • syn/singleflight源码阅读

    2021-06-21

    #go

  • 转发://深入理解Golang

    2021-06-12

    #go

  • go unsafe.Pointer和uintptr解析

    2021-06-09

    #go

  • net/http源码阅读

    2021-06-04

    #go

  • MapReduce的一些改进

    2021-05-25

    #go#分布式

  • 转发://推荐几个巨强的大佬博客(持续更新)

    2021-05-15

  • RPC项目的流程图

    2021-05-14

    #RPC

  • 关于快速选择的尾递归

    2021-03-25

    #数据结构与算法

  • 实现一个MapReduce

    2021-03-22

    #go#分布式

  • 1~n整数中1出现的次数

    2021-03-09

    #数据结构与算法

  • 一些有关快排的小问题

    2021-03-06

    #数据结构与算法

  • IO多路复用详解

    2021-03-05

    #Linux#操作系统#计算机网络

  • 聊聊虚拟内存

    2021-03-04

    #操作系统

  • 正则表达式(Regular Expression)

    2021-03-03

    #工具

  • go指南——练习:web爬虫

    2021-03-02

    #go

  • printf执行原理

    2021-02-24

    #操作系统

  • SYN洪泛攻击原理及防御

    2021-02-23

    #计算机网络

  • Arrays.asList基本用法

    2020-12-12

    #java

  • 从暴力递归到动态规划(二)

    2020-12-11

    #数据结构与算法

  • 从暴力递归到动态规划(一)

    2020-12-11

    #数据结构与算法

  • 线段树(区间修改树)

    2020-12-08

    #数据结构与算法

  • 理解https

    2020-11-18

    #计算机网络

  • Solidity实现以太坊秘密竞拍智能合约

    2020-10-03

    #区块链

热爱跳舞

永远是学徒