1.简介

Go语言在分布式系统领域有着更高的开发效率,提供了海量并行的支持。本博文介绍的是采用Go语言搭建一个并行版爬虫信息采集框架,博文中使用58同城中租房网页做案例。相比较其他爬虫程序它的优点是:

  • 1.抓取信息速度非常快,因为是并行处理的,通过配置协程数量,可以比普通的爬虫信息采集程序快上上百倍。
  • 2.功能模块化,每个功能模块各司其职,配置简单。通过修改信息抓取规则,就可以采集不同网站中的数据。

程序源代码放到github上,链接地址是: https://github.com/GuoZhaoran/crawler

2.项目架构

下面是项目整体架构的示意图:

2.1 Request(请求)

该爬虫架构中Request请求可以理解为:抓取请求url的内容,例如抓取58同城北京市的租房信息时,请求的url是:https://bj.58.com/chuzu/
打开url会发现,网页页面中是房源列表信息,那么接下来要做的工作就是抓取房源详情信息和分页后的下一页房源列表信息。于是就会有新的请求Request,对应不同的url链接地址。

2.2 Worker(工作者)

我们在拿到Request请求之后,抓取到网页页面内容,就需要有单独的程序去解析页面,提取相关信息,这就是worker所要做的工作。

2.3 Request队列和Worker队列

Go语言在构建并行处理程序中有着天然的优势,在该框架中处理Request请求和使用Worker提取相关信息也都是并行工作的。程序中会同时存在着很多个Request,也会有很多个Worker在处理不同Request页面中的内容。所以分别需要一个Request队列和Worker队列来管理它们。

2.4 Scheduler(调度器)

调度器的职责是将Request分配给空闲的Worker来处理,实现任务调度。因为Request和Worker分别使用队列进行管理,可以通过调度器来控制程序的运行过程,例如:分配不同数量的Worker,将特定的Request分配给相应的Worker进行处理等。

3.功能模块和代码解析

下面我们来看一下项目的目录结构,了解一下爬虫架构的功能模块,再详细对每一个功能模块的实现过程做介绍:

3.1 定义数据结构体

通过上面对项目架构介绍可以看出,运行该爬虫程序,需要的数据结构体很简单,定义数据结构的程序文件是:engine/type.go

package engine//请求数据结构
type Request struct {Url string    //请求urlParserFunc func([]byte) ParseResult    //内容解析函数
}//经过内容解析函数解析后的返回数据结构体
type ParseResult struct {Requests []Request        //请求数据结构切片Items []interface{}       //抓取到的有用信息项
}

Request(请求)所要包含的信息是请求url和解析函数,不同的url所需的解析函数是不一样的,比如我们要提取的“58同城房源列表”和“房源详情页面”信息是不一样的,所需解析函数也是不一样的,接下来会对者者两个页面的解析函数进行介绍。
Worker对请求进行处理之后,返回的结果中可能有新的Request,比如从房源列表中提取出房源详情页面的链接。在房源详情页面中我们会拿到详情信息,这些详情信息我们通过Items进行输出即可(企业中更通用的做法是将这些信息存储到数据库,用来做数据分析,这里我们只是对并行爬虫框架实现思路做介绍)

3.2 采集器

采集器实现的功能是根据url提取网页内容,使用Go语言处理很简单,只需要封装一个简单的函数即可,下面是源代码,不做过多介绍。(如果想要将采集器做的更通用一些,同城还需要对不同网站url的编码做兼容处理),采集器相关的代码实现在:fetcher/fetcher.go

//根据网页链接获取到网页内容
func Fetch(url string) ([]byte, error) {resp, err := http.Get(url)if err != nil {return nil, err}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {return nil, fmt.Errorf("wrong status code: %d", resp.StatusCode)}bodyReader := bufio.NewReader(resp.Body)return ioutil.ReadAll(bodyReader)
}

3.3 解析器

解析器要做的工作是根据fetch拿到的网页内容,从中提取出有用的信息。上边我们提到过Request结构体中,不同的Url需要不同的解析器,下面我们就分别看一下房源列表解析器和房源详情页面解析器。房源列表解析器代码实现代码是:samecity/parser/city.go

package parserimport ("depthLearn/goCrawler/engine""regexp""strings"
)
const housesRe = `<a href="(//short.58.com/[^"]*?)"[^>]*>([^<]+)</a>`
const nextPage = `<a class="next" href="([^>]+)"><span>下一页</span></a>`func ParseCity(contents []byte) engine.ParseResult {re := regexp.MustCompile(housesRe)matches := re.FindAllSubmatch(contents, -1)result := engine.ParseResult{}for _, m := range matches {name := string(m[2])//格式化抓取的urlfmtUrl := strings.Replace(string(m[1]), "/", "https:/", 1)result.Items = append(result.Items, "User "+string(m[2]))result.Requests = append(result.Requests, engine.Request{Url: fmtUrl,ParserFunc: func(c []byte) engine.ParseResult {return ParseRoomMsg(c, name)},})}nextRe := regexp.MustCompile(nextPage)linkMatch := nextRe.FindStringSubmatch(string(contents))if len(linkMatch) >= 2 {result.Requests = append(result.Requests, engine.Request{Url:linkMatch[1],ParserFunc:ParseCity,},)}return result
}

从代码中可以看出,列表解析器所做的工作是提取房源详情链接,和下一页房源列表链接。如图所示:

正则表达式定义到函数循环外部是因为提取链接所用的正则表达式都是一样的,程序只需要定义一次,检查正则表达式是否编译通过(regexp.MustCompile)就可以了。
通过浏览器工具查看源代码我们会发现我们提取的链接并不是标准的url形式,而是如下格式的字符串://legoclick.58.com/cpaclick?target=pZwY0jCfsvFJsWN3shPf......,我们要做的就是把字符串前边加上https://,这也很容易实现,使用Go语言标准库函数strings.Replace就可以实现。
另外一个需要注意的地方就是,我们提取到的房源列表url和房源详情url所需要的解析函数(ParseFunc)是不一样的,从代码中可以看出,房源列表url的解析函数是ParseCity,而房源详情解析函数是ParseRoomMsg。我们会发现。我们通过解析房源列表url,会得到新的房源列表url和房源详情url,房源详情url可以通过解析函数直接拿到我们想要的数据,而新的房源列表url需要进一步的解析,然后得到同样的内容,直到最后一页,房源列表url解析后再也没有新的房源列表url位置,数据就抓取完毕了,这种层层递进的处理数据的方法在算法上叫做:深度优先遍历算法,感兴趣的同学可以查找资料学习一下。

3.4 信息模版

上面我们提到了解析器,信息模版代码实现文件是:/samecity/parser/profile.go,它所定以的仅仅是我们要提取信息的一个模版struct。如下图所示是一个房源详情页面,红圈部分是我们要提取的数据信息:

我们再来对比一下profile.go信息模版中所定义的数据结构:

package model//成员信息结构体
type Profile struct {Title     string       //标题Price     int          //价格LeaseStyle    string   //租赁方式HouseStyle    string   //房屋类型Community     string   //所在小区Address       string   //详细地址
}

将信息模版单独定义一个文件也是为了能够使程序更加模块化,模块化带来的好处是代码易于维护,假如我们想要抓取其他网站的信息,就可以通过修改解析器的规则,配置信息模版来使用。正如前边提到的我们的爬虫框架比较通用。

3.5 调度器

“调度器”是整个框架中最核心的部分,它实现了将请求分配到worker的调度。为了让数据爬取工作能够顺利进行,我们将Worker和每一个Request都使用队列进行管理。我们先来看一个调度器的接口和实现。
调度器的接口定义是这样的:

type Scheduler interface {Submit(Request)ConfigureWorkerMasterChan(chan chan Request)WorkerReady(chan Request)Run()
}
  • Submit:顾名思义就是将接收到的请求提交给调度器,由调度器分配给空闲的Worker执行。
  • ConfigureWorkerMasterChan:为每一个Worker都分配一个channel,我们知道Go语言的- channel是协程通信最常用手段,这种基于CSP的通信模型给我们的并发编程带来很大的遍历。我们的框架中调度器和Request,Worker之间都是使用channel进行信息传递。
  • WorkerReady:当有Worker可以被分配任务时,向调度器发送的信号,将该Worker加入队列。
  • Run是启动程序的发动机,它所做的就是将任务的初始化工作做好,启动程序。

下面我们看一下这些方法的具体实现(/scheduler/queue.go)

package schedulerimport "depthLearn/goCrawler/engine"//队列调度器
type QueuedScheduler struct {requestChan chan engine.RequestworkerChan chan chan engine.Request
}//将任务提交
func (s *QueuedScheduler) Submit(r engine.Request) {s.requestChan <- r
}//当有worker可以接收新的任务时
func (s *QueuedScheduler) WorkerReady(w chan engine.Request) {s.workerChan <- w
}//将request的channel送给调度器
func (s *QueuedScheduler) ConfigureWorkerMasterChan(c chan chan engine.Request) {s.workerChan = c
}func (s *QueuedScheduler) Run(){s.workerChan = make(chan chan engine.Request)s.requestChan = make(chan engine.Request)go func() {//建立request队列和worker队列var requestQ  []engine.Requestvar workerQ   []chan engine.Requestfor {//查看是否既存在request又存在worker,取出作为活动的request和workervar activeRequest engine.Requestvar activeWorker chan engine.Requestif len(requestQ) > 0 && len(workerQ) > 0 {activeWorker = workerQ[0]activeRequest = requestQ[0]}select {//调度器中有请求时,将请求加入到请求队列case r := <-s.requestChan:requestQ = append(requestQ, r)//调度器中有可以接收任务的worker时,将请求加入到worker中case w := <-s.workerChan:workerQ = append(workerQ, w)//当同时有请求又有worker时,将请求分配给worker执行,从队列中移除case activeWorker <- activeRequest:workerQ = workerQ[1:]requestQ = requestQ[1:]}}}()
}

我们重点看一下Run方法,首先建立好两个队列(workerChan和requestChan),然后开启一个协程挂起任务,当有request时,加入request队列;当有worker时,加入worker队列;当worker和request同时存在时,就将第一个request分配给第一个worker。这样我们就实现了调度器,worker和解析器并行工作了。
所有工作都做完之后,我们就可以通过ConcurrentEngine,实现程序了,ConcurrentEngine所做的工作就是配置worker数量,接收一个种子url,将调度器,采集器和worker都发动起来工作了,代码的实现文件是:/engine/concurrent.go

package engineimport "fmt"type ConcurrentEngine struct {Scheduler SchedulerWorkerCount int
}type Scheduler interface {Submit(Request)ConfigureMasterWorkerChan(chan chan Request)WorkerReady(chan Request)Run()
}func (e *ConcurrentEngine) Run(seeds ...Request) {out := make(chan ParseResult)e.Scheduler.Run()for i := 0; i < e.WorkerCount; i++ {createWorker(out, e.Scheduler)}for _, r := range seeds {e.Scheduler.Submit(r)}for {result := <- outfor _, item := range result.Items {fmt.Printf("Got item: %v", item)}for _, request := range result.Requests {e.Scheduler.Submit(request)}}
}func createWorker(out chan ParseResult, s Scheduler) {go func() {in := make(chan Request)for {s.WorkerReady(in)// tell scheduler i'm readyrequest := <- inresult, err := worker(request)if err != nil {continue}out <- result}}()
}

配置worker数量,让worker工作起来,createWorker就是当worker接收到Request之后开始工作,工作完成之后告诉调度器(通过WorkerReady方法)。worker的实现也很简单,如下所示:

func  worker(r Request) (ParseResult, error){log.Printf("Fetching %s", r.Url)body, err := fetcher.Fetch(r.Url)if err != nil {log.Printf("Fetcher: error " + "fetching url %s: %v", r.Url, err)return ParseResult{}, err}return r.ParserFunc(body), nil
}

至此,所有的工作都准备好了,就可以开始工作了,入口文件crawler.go:

package mainimport ("depthLearn/ConcurrentCrawler/engine""depthLearn/ConcurrentCrawler/scheduler""depthLearn/ConcurrentCrawler/zhenai/parser"
)func main() {e := engine.ConcurrentEngine{Scheduler: &scheduler.QueuedScheduler{},WorkerCount: 100,}e.Run(engine.Request{Url:       "http://www.samecity.com/zhenghun",ParserFunc: parser.ParseCityList,})
}

下面是命令行打印出来的效果图:

可以看到,我们抓取到数据了。

4.拓展与总结

我们的爬虫程序功能还算完备,当时还有很多可以改进优化的地方,我觉得最主要的有三点:

  • 程序中我们抓取到的信息是通过文本命令行打印出来的,而在企业应用中我们更多的将这些有价值的数据存储到数据库中。我们的程序设计的很合理,在parseResult中的item中,包含了我们抓取的所有信息,读者可自行编写数据存储模块来实现该功能。
  • 在程序的调度器中,我们是通过取出request队列中的第一个和worker中的第一个,将request分配给worker。因为我们是通过队列管理了,所以我们可以修改调度器的调度规则,从而实现更合理,高效的调度策略。
  • 我们实现的只是最简单的爬虫,真实的场景中,有很多安全性做的都比较好的网站。都有QPS限制,用户认证,IP过滤等多种防护手段防止数据抓取。我们也可以将这种种情况都考虑在内,把框架封装的更通用,功能更完备。

总体来说我们的并行爬虫框架还是挺不错的,其中涉及到的模块化编程,队列管理,调度器等在工作中还是值得借鉴的。当然,笔者水平有限,语言组织能力也不是太好,虽然参考了很多其他资料,代码中存在很多值得优化的地方,希望大家能够留言指正。谢谢大家!

Golang搭建并行版爬虫信息采集框架相关推荐

  1. golang笔记15--go语言单任务版爬虫

    golang笔记15--go语言单任务版爬虫 1 介绍 2 单任务版爬虫 2.1 获得初始页面内容 2.2 正则表达式 2.3 提取城市和 url 2.4 单任务版爬虫的架构 2.5 Engine 与 ...

  2. golang笔记16--go语言并发版爬虫

    golang笔记16--go语言并发版爬虫 1 介绍 2 并发版爬虫 2.1 并发版爬虫架构 2.2 简单调度器 2.3 并发调度器 2.4 队列实现调度器 2.5 重构和总结 2.6 更多城市 2. ...

  3. 如何快速搭建实用的爬虫管理平台

    本篇文章内容较多,涉及知识较广,读完需要大约 20 分钟,请读者耐心阅读. 前言 大多数企业都离不开爬虫,爬虫是获取数据的一种有效方式.对搜索引擎来说,爬虫不可或缺:对舆情公司来说,爬虫是基础:对 N ...

  4. 如何搭建一个简易的Web框架

    Web框架本质 什么是Web框架, 如何自己搭建一个简易的Web框架?其实, 只要了解了HTTP协议, 这些问题将引刃而解. 简单的理解:  所有的Web应用本质上就是一个socket服务端, 而用户 ...

  5. 小白也能看懂!教你如何快速搭建实用的爬虫管理平台

    写在前面:本篇文章内容较多,涉及知识较广,读完需要大约 20 分钟,请读者耐心阅读. 如今大多数企业都离不开爬虫,它是获取数据的一种有效方式.但是对爬虫有着规模量级要求的企业或个人需要同时处理不同类别 ...

  6. 数据篇-爬虫开源框架推荐

    花了一天的时间调研了一下主流的开源技术框架. 经过反复查看关键指标和技术框架的扩展性,筛选了一些实用的开源框架. 爬虫框架 项目 项目简介 贡献者数 主要语言 第一次发版时间 Scrapy Scrap ...

  7. 使用Golang搭建gRPC服务提供给.NetCore客户端调用

    gRPC概述 RPC 说到gRPC就不得不提RPC,所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,简单点来说就是我A机器 ...

  8. scrapy获取a标签的连接_Python爬虫 scrapy框架初探及实战!

    Scrapy框架安装 操作环境介绍 操作系统:Ubuntu19.10 Python版本:Python3.7.4 编译器:pycharm社区版 安装scrapy框架(linux系统下) 安装scrapy ...

  9. 详解从零搭建企业级 vue3 + vite2+ ts4 框架全过程

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 本文不仅仅是搭建个脚手架这么简单,还会带你了解每一步.甚至每一个配置项的作用,和每个配置的知 ...

最新文章

  1. SharePoint 2007 Web Content Management 性能优化系列 3 - IIS压缩
  2. HTML5中video标签与canvas绘图的使用
  3. 机器学习中矩阵向量求导
  4. 成大事必备9种能力、9种手段、9种心态
  5. SpringBoot三种获取Request和Response的方法
  6. Cloud一分钟 | 阿里云发布飞天2.0操作系统;京东云串联生态伙伴专治“看病难”...
  7. linux nuc 吗 支持_在你的树莓派家庭实验室中使用 Cloudinit | Linux 中国
  8. 10.2 广州集训 Day1
  9. 三种水平居中布局~详细
  10. 双边滤波及其matlab代码
  11. docker 学习之使用dockerfile 创建镜像遇到的坑
  12. 保证服务4个9的可用性的核心思路
  13. android psp 模拟器卡,手机PSP游戏闪退或卡顿的解决方法
  14. 2022年P气瓶充装考试模拟100题模拟考试平台操作
  15. 学前端到了CSS阶段,你一定要掌握这9大防御式开发技能
  16. 将HTML代码转换为图片
  17. 外贸公司怎么群发邮件?群发邮件邮箱怎么发更高效?
  18. python内存泄露memory leak排查记录
  19. 网优5g前景_5G网络优化师前景怎么样?
  20. mht文件无法打开的解决办法

热门文章

  1. 各类语言的常用正则表达式
  2. Autowired和Resource的区别
  3. mysql服务器修改ip,mysql数据库修改服务器ip
  4. 【科研系列】专利检索工具及方法简单介绍
  5. 1.嵌入式控制器EC学习,编译环境搭建
  6. 价目表制作,价目表小程序
  7. MySQL数据库自动备份脚本
  8. Kattis-torn to pieces
  9. 用Echart完成第七次人口普查
  10. 工业相机在涡轮叶片氧化铝检测成像系统中的应用