# Go快速入门(四)- 实战训练营
# 本章学习目标
在前几章节夯实基础的下,原本兴趣十足的小伙伴们都跃(mei)跃(you)欲(xing)试(qu)了。那今天这个章节我们就抛开语言的条条款款,来实战一番。
# Web 爬虫实战
上一个章节末尾留下一道练习题目,不知道大家完成得怎么样了呢?
嗯?题目?什么题目?我怎么没有看到呢?
好吧,我知道大家根本没有看啦。那本章节我们来看看这到题目怎么完成。
# 1、需求分析
用爬虫进行Web数据挖掘已经越来越普遍,最常见的是对于网站内容的爬取,包括文本、图片和文件等;其次对于网站结构的爬取,包括网站目录、链接之间的相互跳转关系、二级域名等;还有一种爬虫是对于Web应用数据的挖掘。
既然爬虫如此重要还如此好玩,那我们来看看如何使用go
语言实现爬虫吧。
# 2、实现思路
爬虫,就是获取从Web页面内容的数据和URL,然后再从获取URL相对应页面内容的数据和URL。从一个Web页面出发像蜘蛛网一样发散开来。
因此,如何实现一个爬虫程序,关键点是如何从一个Web页面获取该页面下的URL。另一个关键点则是如何并发地抓取Web页面内容。使用go
语言http
获取网页内容(暂时不涉及),使用goroutine
并发抓取网页,so easy!
# 3、习题分析
点开题目链接 Go 的并发特性来实现一个 Web 爬虫。能看到题目中已经写了很多代码啦。那我们来分析一下这些代码目的是什么。
// 7~10行
type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}
// 44-49行
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)
}
这个不就是第二章节中的interface
接口嘛,而且fakeFetcher
实现了接口Fetcher
的Fetch
方法。那我们再看看结构体fakeFetcher
。等等fake fetcher?假的抓取者?
// 36-42行
// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
结构体fakeResult
,就是爬虫抓取URL网页的内容body和body中的很多URL。但是为什么是又是fake呢?
type fakeFetcher map[string]*fakeResult
这个好像看不懂啊,type
是定义类型的关键字,难道是定义了一个map[string]*fakeResult
的fakeFetcher
类型?
为什么这么多问题啊,再看看题目吧。
// 51-83行
// 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/",
},
},
}
var fetcher = fakeFetcher{…}
,定义一个变量类型为fakeFetcher
,果然fakeFetcher
是自定义的类型。再看赋值内容,fakeResult
结构体系属性body、urls都相应地被赋值了。想到Fetcher
被fakeFetcher
实现的方法直接返回的是fakeFetcher
下fakeResult
的body、urls。
提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!
看到题目提示map 本身并不是并发安全
(至于为什么不是并发安全,大家可以了解一下Map的实现原理),那么如何才能使用map并发完全呢?大家都知道,在多线程访问共享内存时也不是并发安全的。那么map也可以使用互斥锁的方式让其并发安全。
# 4、sync包
互斥锁 Mutex
一个互斥锁只能同时被一个
goroutine
锁定,其它goroutine
将阻塞直到互斥锁被解锁。func (m *Mutex) Lock() // 上锁 func (m *Mutex) Unlock() // 解锁
WaitGroup
WaitGroup
用于等待一组goroutine
结束。func (wg *WaitGroup) Add(delta int) // 添加deltaw个goroutine func (wg *WaitGroup) Done() // 添加减少1个goroutine func (wg *WaitGroup) Wait() // 等待goroutine执行完成
sync包下的
Mutex
与WaitGroup
将会在下面爬虫的实现中用到。
# 5、解题
从上面的分析可以知道,这道题只用写并发逻辑不用真实抓取网页内容。fakeResult
表示假的抓取网页内容,fakeFetcher
表示假的抓取的网页结构。只用实现并发情况下抓取网页,并且使用map记录下已经抓取过的URL。
首先,我们定义一个已经抓取过网页的结构。
type Fetched struct {
m map[string]error
sync.Mutex // 互斥锁、匿名结构体
}
var fetched = Fetched{m: make(map[string]error)}
然后,修改Crawl函数。
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
// 判断url是否被抓取过
fetched.Lock()
if _, ok := fetched.m[url]; ok {
fetched.Unlock()
return
}
fetched.Unlock()
body, urls, err := fetcher.Fetch(url)
// 记录抓取当前url结果
fetched.Lock()
fetched.m[url] = err
fetched.Unlock()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 使用goroutine循环抓取urls,使用sync.WaitGroup来等待当抓取到的urls被抓取完
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(wg *sync.WaitGroup, url string) {
Crawl(url, depth-1, fetcher)
wg.Done()
}(&wg, u)
}
wg.Wait()
return
}
最后,在main函数中打印已经被抓取的结果。
package main
import (
"fmt"
"sync"
)
type Fetched struct {
m map[string]error
sync.Mutex // 互斥锁、匿名结构体
}
var fetched = Fetched{m: make(map[string]error)}
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
}
// 判断url是否被抓取过
fetched.Lock()
if _, ok := fetched.m[url]; ok {
fetched.Unlock()
return
}
fetched.Unlock()
body, urls, err := fetcher.Fetch(url)
// 记录抓取当前url结果
fetched.Lock()
fetched.m[url] = err
fetched.Unlock()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 使用goroutine循环抓取urls,使用sync.WaitGroup来等待当抓取到的urls被抓取完
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func(wg *sync.WaitGroup, url string) {
Crawl(url, depth-1, fetcher)
wg.Done()
}(&wg, u)
}
wg.Wait()
return
}
func main() {
Crawl("https://golang.org/", 4, fetcher)
fmt.Println("Fetching stats\n--------------")
for url, err := range fetched.m {
if err != nil {
fmt.Printf("%v failed: %v\n", url, err)
} else {
fmt.Printf("%v was fetched\n", url)
}
}
}
// 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/",
},
},
}
那么这道题目就正式完成啦,其实上面的实现方法中使用WaitGroup
会等待一组goroutine
执行完成后才能结束。我们能不能让goroutine
执行完成后不让调用函数等待呢?当然可以,这就是下一章节channel
的内容,敬请期待。