在学习《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无法确保并发执行的安全性!

爬虫网页代码的问题exercise-web-crawler.go
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)不存在(即未找到)之前一直重复。

答案之路

根据问题的内容,需要做的事情大致有两个方面。

    1. 并行获取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