070-并发爬虫(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/q1007729991/article/details/80959450

关于爬虫(Spider/Crawler),相信你多多少少接触过。比如你老板要你从某网站获取一批企业的黄页信息,这时候爬虫就派上用场了。而本文,我们的任务是编写一个网页抓取程序,提取网页中的 url. 我们的程序可以继续访问抓取到的 url,深度抓取更多的 url.

1. 链接抓取器

关于这个程序,我就不多废口舌了,你也不必过多的去研究它,掌握用法即可,我们的目的是学习并发。这个程序位于 gopl/goroutine/link 目录下。

package link

import (
    "io/ioutil"
    "net/http"
    "regexp"
    "strings"
)

// 从指定的种子地址提取 url
func ExtractLinks(seed string) ([]string, error) {
    var urls []string

    pattern := regexp.MustCompile(`<a\b[^>]+\bhref="([^"]*)"[^>]*>[\s\S]*?</a>`)
    resp, err := http.Get(seed)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    buf, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    res := pattern.FindAllStringSubmatch(string(buf), -1)

    for _, e := range res {
        if len(e) != 2 {
            continue
        }
        if strings.HasPrefix(e[1], "http") {
            urls = append(urls, e[1])
        }
    }
    return urls, nil
}

下面是一个简单的测试示例:

// +build ignore

package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:\n\tgo run main.go <url>")
        os.Exit(1)
    }
    urls, err := link.ExtractLinks(os.Args[1])
    if err != nil {
        log.Print(err)
        return
    }
    for _, url := range urls {
        fmt.Printf("%v\n", url)
    }
}

接下来运行 go run main.go 'http://www.baidu.com/s?wd=goroutine',你的屏幕会打印类似这样的结果:


这里写图片描述
图1 链接抓取器(只展示部分结果)

2. 深度链接抓取器

看到图 1 的结果了吧,上面是我们抓取的 http://www.baidu.com/s?wd=goroutine 这个网页的链接。但是作为一只“优秀”的爬虫,这点结果根本不能满足,我们希望继续访问抓取到的链接,然后获取更多的链接。思路非常简单:

  • 每个 goroutine 从一个种子 url 提取所有的链接,保存到任务列表
  • 主协程从任务列表提取已经抓取的 url,并启动新的协程继续抓取

下面是这个深度链接抓取器的代码,在路径 gopl/goroutine/concurrence 下面:

// crawler01.go
package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

// url 抓取,返回抓取到的所有 url
func crawl(url string) []string {
    fmt.Println(url)
    urls, err := link.ExtractLinks(url)
    if err != nil {
        log.Print(fmt.Sprintf("\x1b[31m%v\x1b[0m", err))
    }
    return urls
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:\n\tgo run crawl.go <url>")
        os.Exit(1)
    }

    // 任务队列
    workList := make(chan []string)
    // 记录该 url 是否已经访问过,防止无限循环
    seen := make(map[string]bool)

    go func() {
        workList <- os.Args[1:]
    }()

    for list := range workList {
        for _, url := range list {
            if seen[url] {
                continue
            }
            seen[url] = true
            go func(url string) {
                workList <- crawl(url)
            }(url)
        }
    }
}

接下来,运行一下看看,我们把结果重定向到 links.go 文件中,错误会输出到屏幕上。

$ go run crawler01.go 'http://www.baidu.com/s?wd=goroutine' > links.txt

要不了一会儿,你的屏幕应该就会出现:


这里写图片描述
图2 输出错误

这个程序几乎永远不会停止,使用 CTRL C 终止它吧。另外我们的 links.txt 文件也抓取了不少 url 了。

2.1 为什么出现 too many open files

这是文件描述符被耗尽了。一般你的系统都有同时打开的最大文件描述符个数的限制。


这里写图片描述
图3 文件描述符限制

在我的系统中,这个限制是 7168。而每访问一个 url 都要建立一个 tcp 链接,意味着消耗掉一个描述符,因此这个程序理论上最大可以同时打开 7168 个 url。

但是我们的程序太 TM 并行了,开启的 goroutine 远远超出了这个限制。如何解决?什么?修改系统最大描述符限制的上限?别开开玩笑了,goroutine 是可以轻松开到上万的,这样会把你的系统搞死。

无穷无尽的并发并不是什么好事。是的,我们需要对并发度做控制。换句话说,我们希望把同时开启的 goroutine 上限控制在一个合理的数字上,比如允许最大 20 并发。

为了防止你学习疲劳,今天到此为止。下回我们讨论并发度控制。

3. 总结

  • 练习链接抓取器
  • 练习并发的深度链接抓取器
  • 为什么程序会出现 too many open files 的错误

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/80959450