几分钟后,从输出通道中选择爬行器档位

时间:2014-04-06 08:46:39

标签: go blocking channel goroutine

我写了一个简单的爬虫,看起来像这样:

type SiteData struct {
    // ...
}

func downloadURL(url string) (body []byte, status int) {
    resp, err := http.Get(url)

    if err != nil {
        return
    }

    status = resp.StatusCode
    defer resp.Body.Close()

    body, err = ioutil.ReadAll(resp.Body)
    body = bytes.Trim(body, "\x00")

    return
}


func processSiteData(resp []byte) SiteData {
    // ...
}    

func worker(input chan string, output chan SiteData) {

    // wait on the channel for links to process
    for url := range input {

        // fetch the http response and status code
        resp, status := downloadURL(url)

        if resp != nil && status == 200 {
            // if no errors in fetching link
            // process the data and send 
            // it back
            output <- processSiteData(resp)
        } else {
            // otherwise send the url for processing
            // once more
            input <- url
        }
    }
}

func crawl(urlList []string) {
    numWorkers := 4
    input := make(chan string)
    output := make(chan SiteData)

    // spawn workers
    for i := 0; i < numWorkers; i++ {
        go worker(input, output)
    }

    // enqueue urls
    go func() {
        for url := range urlList {
            input <- url
        }
    }()

    // wait for the results
    for {
        select {
        case data := <-output:
            saveToDB(data)
        }
    }

}

func main() {
    urlList := loadLinksFromDB()
    crawl(urlList)
}

它可以抓取一个网站并且运行良好 - 下载数据,处理数据并将其保存到数据库中。然而,在几分钟(5-10)之后,它会“卡住”并需要重新启动。该网站没有将我列入黑名单,我已经与他们进行了验证,并且可以在程序阻止后随时访问任何网址。此外,它会在所有网址完成处理之前阻止。显然它会在列表花费时阻止,但它远不及那个。

我在这里做错了吗?我使用for { select { ... } }而不是for _, _ = range urlList { // read output }的原因是,如果处理失败,任何网址都可以重新入队。此外,数据库似乎也不是问题。任何输入都会有所帮助 - 谢谢。

1 个答案:

答案 0 :(得分:1)

我相信当你让所有N名工人在input <- url等待时,这就会挂起,因此没有更多的工人从input中取出东西。换句话说,如果4个URL大致同时失败,它将会挂起。

解决方案是将失败的URL发送到某个不是工作者输入通道的地方(以避免死锁)。

一种可能性是拥有一个单独的failed频道,匿名goroutine始终接受来自它的输入。像这样(未经测试):

package main

func worker(intput chan string, output chan SiteData, failed chan string) {
    for url := range input {
        // ...
        if resp != nil && status == 200 {
            output <- processSideData(resp)
        } else {
            failed <- url
        }
    }
}

func crawl(urlList []string) {
    numWorkers := 4
    input := make(chan string)
    failed := make(chan string)
    output := make(chan SiteData)

    // spawn workers
    for i := 0; i < numWorkers; i++ {
        go worker(input, output, failed)
    }

    // Dispatch URLs to the workers, also receive failures from them.
    go func() {
        for {
            select {
            case input <- urlList[0]:
                urlList = urlList[1:]
            case url := <-failed:
                urlList = append(urlList, url)
            }
        }
    }()

    // wait for the results
    for {
        data := <-output
        saveToDB(data)
    }
}

func main() {
    urlList := loadLinksFromDB()
    crawl(urlList)
}

(请注意,正如您在评论中所说,不要在for _, _ = range urlList { // read output }函数中使用crawl(),这是正确的,因为网址可以重新排队;但您不需要选择据我所知。)