070-并发爬虫

Posted --Allen--

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了070-并发爬虫相关的知识,希望对你有一定的参考价值。

关于爬虫(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 的错误

以上是关于070-并发爬虫的主要内容,如果未能解决你的问题,请参考以下文章

Nodejs爬虫进阶教程之异步并发控制

071-并发爬虫

Java Berkeley DB 是不是有并发读取的上限?

单机Qps上限是多少?

协议并发测试工具 BoHexTest

人机界面HMI 迅为iTOP-HMI070-N触摸屏一体机 7寸工业平板电脑