匠心精神 - 良心品质腾讯认可的专业机构-IT人的高薪实战学院

咨询电话:4000806560

【实战经验】用Golang构建一款分布式爬虫的完整实践流程

【实战经验】用Golang构建一款分布式爬虫的完整实践流程

在如今信息爆炸的时代,海量的数据可以让人头疼,但是数据的质量和价值是我们不能忽视的,因此,网站爬虫成为了我们获取数据的重要手段之一。本文将介绍如何使用Golang构建一款分布式爬虫来获取想要的数据。

一、分析需求

在开始构建分布式爬虫前,我们需要充分了解需求,梳理好需要爬取的数据、网站的结构,以及要使用的技术栈等方面。在此,我们以爬取拉勾网招聘信息为例来讲解如何构建一个分布式爬虫。

我们需要爬取的信息包括职位名称、职位链接、公司名称、公司所在城市、薪资范围和工作经验等信息。我们可以通过浏览器访问拉勾的招聘页面,可以看到页面是由多个职位的信息组成的,而每个职位的信息又包括多个字段。通过Chrome浏览器的Inspect功能,我们可以看到页面的结构如下:

![Alt text](https://cdn.jsdelivr.net/gh/sunjiaqing/blog_images/img/20210628151344.png)

在分析页面结构后,我们需要选择一个合适的技术栈来实现爬虫的功能。本次我们选择使用Go语言,并借助其强大的并发、网络和解析等能力来完成爬虫的功能。

二、准备工作

在开始编写代码前,我们需要先安装Go语言并配置好环境变量。具体操作可以参考官方文档。

1. 安装Go

从Go官网下载适合自己操作系统的安装包,进行安装。安装完成后,可以打开终端输入以下命令查看版本号:

```
go version
```

2. 配置环境变量

在macOS和Linux系统下,需要将export PATH=/usr/local/go/bin:$PATH写入到~/.bashrc或~/.zshrc中,并执行source ~/.bashrc或source ~/.zshrc来使其生效。

在Windows系统下,需要将%GOPATH%\bin;%GOROOT%\bin;写入到系统的环境变量中,具体操作可以参考官方文档。

三、编写代码

在准备工作完成后,我们就可以开始编写代码了。我们可以将整个爬虫分成三个部分:URL管理器、HTML下载器和HTML解析器。其中,URL管理器用于管理所有待下载的URL,HTML下载器用于下载URL对应的HTML源码,HTML解析器用于解析HTML源码中我们需要的信息。

1. URL管理器

URL管理器主要负责管理待下载URL的集合和已下载URL的集合,以及对URL的去重和添加等操作。在本爬虫中,我们可以使用一个map[string]bool类型的变量来作为URL的集合,并使用sync.Mutex类型的锁来保证URL的并发安全。代码如下:

```
type urlManager struct {
    urls map[string]bool
    mu   sync.Mutex
}

func newURLManager() *urlManager {
    return &urlManager{
        urls: map[string]bool{},
    }
}

func (m *urlManager) addURL(url string) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if !m.urls[url] {
        m.urls[url] = true
    }
}

func (m *urlManager) addURLs(urls []string) {
    m.mu.Lock()
    defer m.mu.Unlock()
    for _, url := range urls {
        if !m.urls[url] {
            m.urls[url] = true
        }
    }
}

func (m *urlManager) hasURL() bool {
    m.mu.Lock()
    defer m.mu.Unlock()
    return len(m.urls) > 0
}

func (m *urlManager) getURL() string {
    m.mu.Lock()
    defer m.mu.Unlock()
    for url := range m.urls {
        delete(m.urls, url)
        return url
    }
    return ""
}
```

2. HTML下载器

HTML下载器主要负责下载URL对应的HTML源码,并返回对应的HTTP响应状态码。在本爬虫中,我们可以使用Go的net/http包来实现HTTP的请求和响应。代码如下:

```
func download(url string) (string, int, error) {
    client := &http.Client{}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return "", 0, err
    }
    req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
    resp, err := client.Do(req)
    if err != nil {
        return "", 0, err
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", 0, err
    }
    return string(body), resp.StatusCode, nil
}
```

3. HTML解析器

HTML解析器主要负责解析HTML源码,并提取我们需要的信息。在本爬虫中,我们可以使用Go的goquery包来实现HTML的解析和信息提取。代码如下:

```
func parse(body string) []job {
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
    if err != nil {
        return nil
    }
    var jobs []job
    doc.Find(".job-list li").Each(func(i int, s *goquery.Selection) {
        j := job{}
        j.Title = s.Find(".position_link h3").Text()
        j.Link, _ = s.Find(".position_link").Attr("href")
        j.Company = s.Find(".company_name a").Text()
        j.City = s.Find(".position .add em").Eq(0).Text()
        j.Salary = s.Find(".money").Text()
        j.Exp = s.Find(".position .add em").Eq(1).Text()
        jobs = append(jobs, j)
    })
    return jobs
}
```

四、构建分布式爬虫

在完成单机版爬虫的编写后,我们就可以开始构建分布式爬虫了。在分布式爬虫中,我们需要将爬虫分成两个部分:任务调度器和工作节点。

1. 任务调度器

任务调度器主要负责管理所有待爬取URL的集合和已爬取URL的集合,对URL进行去重和添加等操作,并将待爬取URL分发到各个工作节点上执行。任务调度器还需要负责维护工作节点的数量。

在本分布式爬虫中,我们可以使用一个map[string]bool类型的变量来作为URL的集合,并使用sync.Mutex类型的锁来保证URL的并发安全。

为了使待爬取URL在各个工作节点上均匀分散,我们采用一致性哈希算法来确定需要爬取的URL的工作节点。具体实现可以参考一致性哈希算法。

代码如下:

```
type scheduler struct {
    urlm   *urlManager
    nodes  []string
    circle *consisthash.Map
    mu     sync.Mutex
}

func newScheduler(nodes []string) *scheduler {
    return &scheduler{
        urlm:   newURLManager(),
        nodes:  nodes,
        circle: consisthash.New(),
    }
}

func (s *scheduler) addURL(url string) {
    s.urlm.addURL(url)
}

func (s *scheduler) addURLs(urls []string) {
    s.urlm.addURLs(urls)
}

func (s *scheduler) hasURL() bool {
    return s.urlm.hasURL()
}

func (s *scheduler) getURL() string {
    url := s.urlm.getURL()
    return url
}

func (s *scheduler) addNode(node string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.nodes = append(s.nodes, node)
    s.circle.Add(node)
}

func (s *scheduler) removeNode(node string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    for i, n := range s.nodes {
        if n == node {
            s.nodes[i] = s.nodes[len(s.nodes)-1]
            s.nodes = s.nodes[:len(s.nodes)-1]
            break
        }
    }
    s.circle.Remove(node)
}

func (s *scheduler) getNode(key string) string {
    s.mu.Lock()
    defer s.mu.Unlock()
    if node, ok := s.circle.Get(key); ok {
        return node
    }
    return ""
}
```

2. 工作节点

工作节点主要负责从任务调度器中获取待爬取URL并进行爬取,将结果保存到本地或者上传到云端等操作。在本分布式爬虫中,我们可以使用Go的goroutine来实现并发操作。

每个工作节点的代码结构和单机版爬虫完全一致,唯一的区别在于获取待爬取URL的方式。每个工作节点可以通过轮询的方式从任务调度器中获取待爬取URL。代码如下:

```
func run(node string, s *scheduler, result chan<- []job) {
    for {
        url := s.getNode(node)
        if url == "" {
            break
        }
        body, _, err := download(url)
        if err != nil {
            fmt.Println(err)
            continue
        }
        jobs := parse(body)
        result <- jobs
    }
}
```

五、总结

综上所述,我们通过使用Golang构建了一款分布式爬虫,可以实现高效、快速地获取我们需要的数据。在代码的编写过程中,我们掌握了URL管理器、HTML下载器、HTML解析器、一致性哈希算法等一系列技术知识点。通过此次实践,我们不仅深入了解了Golang的并发、网络和解析能力,也提升了我们的技术水平和实践能力。