在学习《Go语言之旅(练习:网络爬虫)》时只通过完成课程进行回答
达到的目的只有一个。
(Note: The given phrase “目的” means “purpose” or “objective” in English. The provided paraphrase in Chinese conveys the same meaning.)
《Go之旅》是一条所有刚开始学习Go语言的人都会经历的路线,但是练习题还是挺费脑力的。
即使无法解决,因为网络上有很多答案,我们可以理解并继续前进。
不过,只有最后一个问题,无论怎么找,都找不到只使用在教程中学到的知识就能解答的。
这可能只是我找不对方法吧。。。
这次,我只使用了在教程中学到的知识来解答(可能不太聪明),但还是留存下来。
請提供問題的內容。
Golang之旅 | 练习:网络爬虫
请修改Crawl函数,使其能够同时获取多个URL,而不需获取相同的URL两次。
补充说明:您可以在Crawl函数内部进行适当改进来实现,但不必非要将其都放在Crawl函数内部。
简而言之:map可以用来缓存已获取的URL,但仅靠map无法确保并发执行的安全性!
package main
import (
“fmt”
)
type Fetcher interface {
// Fetch返回URL的内容和找到的URL列表。
Fetch(url string) (body string, urls []string, err error)
}
// Crawl使用fetcher递归爬取页面,从给定的url开始,爬取到指定的深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行获取URL。
// TODO: 不重复获取相同的URL。
// 这个实现都没有做到:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf(“发现: %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(“未找到: %s”, url)
}
// fetcher是一个填充了假数据的fakeFetcher。
var fetcher = fakeFetcher{
“https://golang.org/”: &fakeResult{
“Go编程语言”,
[]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{
“fmt包”,
[]string{
“https://golang.org/”,
“https://golang.org/pkg/”,
},
},
“https://golang.org/pkg/os/”: &fakeResult{
“os包”,
[]string{
“https://golang.org/”,
“https://golang.org/pkg/”,
},
},
}
这段代码在做什么?
首先,在mail()函数中调用了Crawl(“https://golang.org/”, 4, fetcher)。
另外,可以看出对于通过fetcher.Fetch(url)获取的多个URL,也调用了Crawl(u, depth-1, fetcher)。
查看Crawl()函数,可以看到在depth <= 0时返回,并且每次调用Crawl()函数时都会对depth进行减1操作。
也就是说,对于每个URL最多调用了3次Crawl()函数。
然后,我们来对照一下fetcher的形状和值。
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
当看到第一个选项时,可以看出fetcher的形式是map[key]{ body, urls[XXX, YYY] }。
当调用 fetcher[“https://golang.org/”].body 时,将会返回 “The Go Programming Language”。
当调用 fetcher[“https://golang.org/”].urls 时,将会返回 [https://golang.org/pkg/ https://golang.org/cmd/]。
通过以上的了解,可以得出结论 fetcher.Fetch(url) 实际上只是返回了上述内容。
然后,对于每个URL,最多重复3次相同的操作或者在map的键(即URL)不存在(即未找到)之前一直重复。
答案之路
根据问题的内容,需要做的事情大致有两个方面。
-
- 并行获取URL
- 只调用一次URL
1. 通过使用goroutine,可以实现此功能。另外,为了进行同步,可以使用chanel来实现。
2. 关于第二点,根据所写,使用map应该可以实现。然而,由于涉及并行执行,似乎需要进行互斥控制,可以使用互斥锁mutex(Lock(), Unlock())来实现。
首先,我们看一下在main()函数中调用的Crawl()函数。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
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
}
虽然它是递归调用,但如果使用通道,似乎只需要在函数内部再创建一个函数就可以了。
我将原来的处理逻辑完整地放入了crawlChild()中。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
var crawlChild func(string, int)
crawlChild = func(url string, depth int) {
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 {
go crawlChild(u, depth-1)
}
}
crawlChild(url, depth)
return
}
如果使用这样运行,只会输出一个。它会在不等待其他goroutine的情况下结束。
found: https://golang.org/ "The Go Programming Language"
尝试修改以使用 Chanel。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
// 追加
ch := make(chan string, depth*depth)
var prev int
var crawlChild func(string, int)
crawlChild = func(url string, depth int) {
if depth <= 0 {
return
}
// 追加
ch <- url
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 {
go crawlChild(u, depth-1)
}
}
crawlChild(url, depth)
// 追加
for {
if len(ch) != prev {
prev = len(ch)
} else {
break
}
time.Sleep(time.Millisecond)
}
return
}
由于如果未指定大小会导致死锁,所以我指定了大小,但是我不知道合适的大小,所以我试了一下depth*depth作为大小。然后,在返回之前,虽然不是正常的用法,但我会用无限循环等待,直到确定通道数量不会再增加。
当以这个状态执行时,最终终于获得了与最初相同的结果。
接下来,只需要确保URL仅被调用一次。可以使用以URL为键的map[string]bool,在调用URL之前进行T/F判断即可。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
// 追加
ch := make(chan string, depth*depth)
var prev int
// 追加
cache := make(map[string]bool)
var mutex sync.Mutex
var crawlChild func(string, int)
crawlChild = func(url string, depth int) {
if depth <= 0 {
return
}
// 追加
if cache[url] == true {
return
}
mutex.Lock()
defer mutex.Unlock()
cache[url] = true
// 追加
ch <- url
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 {
go crawlChild(u, depth-1)
}
}
crawlChild(url, depth)
// 追加
for {
if len(ch) != prev {
prev = len(ch)
} else {
break
}
time.Sleep(time.Millisecond)
}
return
}
当它被执行时,完美地只被调用了一次。
found: https://golang.org/ "The Go Programming Language"
found: https://golang.org/pkg/ "Packages"
found: https://golang.org/pkg/os/ "Package os"
not found: https://golang.org/cmd/
found: https://golang.org/pkg/fmt/ "Package fmt"
Program exited.
这个代码在下面的链接中可以找到: a-tour-of-go/exercise-web-crawler.go at master · hirano00o/a-tour-of-go
最后
去学完A Tour of Go之后,我认为最好去实际编写一些东西,或者查看Go Wiki(英语)和CodeReviewComments。虽然学习A Tour of Go可能需要花费一段时间,但是基本的概念会学到,所以我认为它是值得一做的。
关于 CodeReviewComment,因为有人已经翻译了,所以我想你可以去看看那个。
可以参照
-
- Home · golang/go Wiki
- #golang CodeReviewComments 日本語翻訳 – Qiita