Go中的Webcrawler

时间:2015-04-07 12:37:47

标签: go web-crawler

我试图在Go中构建一个Web爬网程序,我想指定最大并发工作数。只要队列中有探索链接,它们都将正常工作。当队列中的元素少于工作人员时,工作人员应该大声喊叫,但如果找到更多链接则恢复。

我试过的代码是

const max_workers = 6
// simulating links with int
func crawl(wg *sync.WaitGroup, queue chan int) {
    for element := range queue {   
        wg.Done() // why is defer here causing a deadlock?
        fmt.Println("adding 2 new elements ")
        if element%2 == 0 {
            wg.Add(2)
            queue <- (element*100 + 11)
            queue <- (element*100 + 33)
        }

    }
}

func main() {
    var wg sync.WaitGroup
    queue := make(chan int, 10)
    queue <- 0
    queue <- 1
    queue <- 2
    queue <- 3
    var min int
    if (len(queue) < max_workers) {
        min = len(queue)
    } else {
        min = max_workers
    }
    for i := 0; i < min; i++ {
        wg.Add(1)
        go crawl(&wg, queue)
    }
    wg.Wait()
    close(queue)
}

Link to playground

这似乎有效,但有一个问题:当我开始时,我必须用多个元素填充队列。我希望它从(单个)种子页面(在我的示例中为queue <- 0)开始,然后动态地扩展/缩小工作池。

我的问题是:

  • 我如何获得行为?

  • 为什么延迟wg.Done()导致死锁?该函数实际完成后,wg.Done()函数是否正常?我认为没有defer goroutine就不会等待其他部分完成(在解析HTML的实际工作示例中可能需要更长的时间)。

2 个答案:

答案 0 :(得分:4)

如果您使用自己喜欢的网络搜索“Go web crawler”(或“Golang网络抓取工具”) 你会发现许多例子,包括: Go Tour Exercise: Web Crawler。 在Go中也有一些关于并发性的讨论,涵盖了这种事情。1

在Go中执行此操作的“标准”方法根本不需要涉及等待组。 要回答您的一个问题,使用defer排队的内容仅在函数返回时运行。你有一个长时间运行的功能,所以不要在这样的循环中使用defer

“标准”方式是在他们自己的goroutines中启动你想要的许多工人。 他们都从同一个频道读取“工作”,阻止是否/什么时候无事可做。 完成后,该通道关闭,它们都退出。

在类似爬虫的情况下,工人会发现更多的“工作”,并希望将它们排入队列。 你不希望他们写回同一个频道,因为它会有一些有限的缓冲(或者没有!)你最终会阻止所有试图排队更多工作的工人!

一个简单的解决方案是使用单独的通道 (例如,每个工人都有in <-chan Job, out chan<- Job) 以及读取这些请求的单个队列/过滤器goroutine, 将它们附加到切片上,它可以让它任意变大或者进行一些全局限制, 并且还从切片的头部馈送另一个通道 (即从一个通道读取并写入另一个通道的简单的选择循环)。 此代码通常还负责跟踪已经完成的操作 (例如,访问过的URL地图)并删除重复的传入请求。

队列goroutine可能看起来像这样(这里的参数名称过于冗长):

type Job string

func queue(toWorkers chan<- Job, fromWorkers <-chan Job) {
    var list []Job
    done := make(map[Job]bool)
    for {
        var send chan<- Job
        var item Job
        if len(list) > 0 {
            send = toWorkers
            item = list[0]
        }
        select {
        case send <- item:
            // We sent an item, remove it
            list = list[1:]
        case thing := <-fromWorkers:
            // Got a new thing
            if !done[thing] {
                list = append(list, thing)
                done[thing] = true
            }
        }
    }
}

在这个简单的例子中,有一些东西被掩盖了。 如终止。如果“工作”是一个更大的结构,您可能希望使用chan *Job[]*Job。 在这种情况下,您还需要将地图类型更改为从作业中提取的某些键 (例如,Job.URL) 并且你想在list[0] = nil之前做list = list[1:]以摆脱对*Job指针的引用,并让垃圾收集器更早地使用它。

编辑:关于彻底终止的一些注意事项。

有几种方法可以像上面这样彻底地终止代码。可以使用等待组,但是需要小心地完成Add / Done调用的放置,并且您可能需要另一个goroutine来执行Wait(然后关闭其中一个通道以启动关闭)。工人不应该关闭他们的输出渠道,因为有多个工人,你不能多次关闭一个渠道;队列goroutine无法告知何时关闭工具的通道而不知道工人何时完成。

过去当我使用与上面非常类似的代码时,我在“queue”goroutine中使用了一个本地“优秀”计数器(这避免了对等号组所具有的互斥锁或任何同步开销的需要)。当工作被发送给工人时,未完成工作的数量会增加。当工人说它已经完成时,它再次减少。我的代码碰巧有另一个通道(我的“队列”也收集结果以及进一步排队的节点)。它可能在它自己的频道上更干净,但是可以使用现有频道上的特殊值(例如,零作业指针)。无论如何,使用这样的计数器,本地列表上的现有长度检查只需要看到当列表为空并且是时候终止时没有任何未完成的内容;只需关闭通道给工人并返回。 例如为:

    if len(list) > 0 {
        send = toWorkers
        item = list[0]
    } else if outstandingJobs == 0 {
        close(toWorkers)
        return
    }

答案 1 :(得分:1)

我使用Go的互斥(Mutex)功能编写了一个解决方案。

在并发上运行时,一次限制一个实例访问URL映射可能很重要。我相信我按照下面的描述实现了它。请随时尝试。非常感谢您的反馈,因为我也将从您的评论中吸取教训。

package main

import (
    "fmt"
    "sync"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}




// ! SafeUrlBook helps restrict only one instance access the central url map at a time. So that no redundant crawling should occur.
type SafeUrlBook struct {
    book map[string]bool
    mux  sync.Mutex
    }

func (sub *SafeUrlBook) doesThisExist(url string) bool {
    sub.mux.Lock()
    _ , key_exists := sub.book[url]
    defer sub.mux.Unlock()
    
    if key_exists {
    return true
    }  else { 
    sub.book[url] = true
    return false 
    }  
}
// End SafeUrlBook


// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
// Note that now I use safeBook (SafeUrlBook) to keep track of which url has been visited by a crawler.
func Crawl(url string, depth int, fetcher Fetcher, safeBook SafeUrlBook) {
    if depth <= 0 {
        return
    }
    
    
    exist := safeBook.doesThisExist(url)
    if exist { fmt.Println("Skip", url) ; 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, safeBook)
    }
    return
}

func main() {
    safeBook := SafeUrlBook{book: make(map[string]bool)}
    Crawl("https://golang.org/", 4, fetcher, safeBook)
}

// fakeFetcher is Fetcher that returns canned results.
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 is a populated 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/",
        },
    },
}