并发版爬虫
架构
之前单任务版爬虫的架构是:传入一个种子(request)给engine,engine将url传给fetch,fetch将从url获取到的内容传给parse,parse解析出request和item,再将request传给engine队列.具体如下图:
并发版爬虫基于原来的单任务版爬虫,在耗时长的部分使用goroutine,通过channel来传送数据
首先,我们可以看到fetch的输出就是parse的输入,可以把fetch,parse以及engine的一部分合并成一个worker
func Run(seeds ...Request){
for len(seeds) > 0 {
r := seeds[0]
seeds = seeds[1:]
fmt.Printf("%s\n", r.URL)
result, err := worker(r)
if err != nil {
log.Printf("error : %s : %v", r.URL, err)
continue
}
for _, v := range result.Item {
fmt.Printf("%+v\n", v)
}
for _, vv := range result.Requests {
seeds = append(seeds, Request{vv.URL, vv.ParseFunc})
}
}
}
//封装成worker
func worker(r Request) ParseResult,error{
s,err := fetcher.Fetch(r.URL)
if err != nil {
log.Printf("error : %s : %v", r.URL, err)
return ParseResult{}, err
}
result := r.ParseFunc(s)
return result, nil
}
因为要满足并发,所以需要同时有多个worker工作,这时需要一个scheduler调度器来为每一个worker分发任务,所以应该是这样的形式
这里传递的形式不是参数,而是channel
实现的原理
-
engine和scheduler都是一个goroutine,worker开多个goroutine
-
scheduler向一个channel发送数据,所有worker都从这一个channel中接收数据,谁接收到了,谁就进行处理
-
这就实现了简单的分发任务
代码实现
//首先定义一个struct,将run方法挂在其下边,方便以后写不同的实现方式
type ConCurrentEngine struct{
Scheduler Scheduler
WorkerCount int
}
//先写一个接口,后面再实现
type Scheduler interface{
Submit(Request)
//用于为scheduler向worker输入的channel赋值
ConfigMasterChan(chan Request)
}
//具体的run方法
func (e *ConCurrentEngine) Run(seeds ...Request){
//直接将request交给scheduler处理
for _,r := range seeds{
e.Scheduler.Submit(r)
}
//scheduler向worker传输的channel
in := make(chan Request)
//worker发送返回数据的channel
out := make(chan ParseResult)
e.Scheduler.ConfigMasrerChan(in)
for i:= 0;i<e.WorkerCount;i++{
//根据in和out创建worker
createWorker(in,out)
}
for{
result := <- out
for _,item := range result.Item{
log.Printf("Got Item : %v",item)
}
for _,request := range result.Requests{
e.Scheduler.Submit(request)
}
}
}
//创建worker的方法实现
func createWorker(in chan Request,out chan ParseResult){
go func(){
for{
request := <- in
result,err := worker(request)
if err != nil{
continue
}
out <- result
}
}()
}
//最后就是对scheduler的实现了
//可以封装到一个包中
package scheduler
type SimpleScheduler struct{
//向worker输入的channel
WorkerChan chan Request
}
func (s *SimpleScheduler) Submit(r engine.Request){
//开一个goroutine向WorkerChan发送request
//因为在Run方法中,从out取完第一层的城市列表之后,就一直循环向WorkerChan发送数据
//这样没有人从out取数据,worker就发不了,也就没办法再接收WorkerChan的数据
//所以开一个goroutine避免锁住
go func(){
s.WorkerChan <- r
}()
}
//为scheduler的channel赋值
func (s *SimpleScheduler) ConfigMasterChan(in chan engine.Request){
s.WorkerChan = in
}
//这样就可以在main函数中直接地调用了
func main(){
e := engine.ConCurrentEngine{&scheduler.SimpleScheduler{},100}
e.Run(engine.Request{URL: url, ParseFunc: parser.ParseCityList})
}
//但是这样有可能爬取的太快了,网站可能会组织
//为了降低速度,可以直接减小开的worker的数量
//也可以加一个定时器,每个多长时间爬取一次
var rateLimit = time.Tick(100*time.MilliSecond)
//每次爬取先接收一下,接收到再往下进行
<- rateLimit
现在的流程如下图,scheduler为每一个request开一个goroutine,每个goroutine都向channel发送数据,然后所有worker都从这个channel中接收数据
上述方式还有一定缺陷,比如开出来很多goroutine,都无法收回,对程序的控制力量比较弱
因此改进一下,scheduler只开一个goroutine,分别创建request队列和worker队列,在这个goroutine中将request发送给空闲的worker,在这里,worker可以直接看成chan Request类型,因为这里就是一个接收request的东西,实际上并不是完整的worker模块,再接收到之后,还要取出request,然后进行处理。
具体的流程图如下
代码实现
package scheduler
type QueuedScheduler struct{
RequestChan chan engine.Request
WorkerChan chan chan engine.Request
}
func (s *QueuedScheduler) Submit(r engine.Request){
s.RequestChan <- r
}
func (s *QueuedScheduler) WorkerReady(c chan engine.Request){
s.WorkerChan <- c
}
func (s *QueuedScheduler) Run(){
s.RequestChan = make(chan engine.Request)
s.WorkerChan = make(chan chan engine.Request)
go func(){
var requestQ []engine.Request
var workerQ []chan engine.Request
for{
var actualRequest engine.Request
var actualWorker chan engine.Request
if len(requestQ) > 0 && len(workerQ) > 0{
actualRequest = requestQ[0]
actualWorker = workerQ[0]
}
select{
case r := <- s.RequestChan:
requestQ = append(requestQ,r)
case w := <- s.WorkerChan:
workerQ = append(workerQ,w)
case actualWorker <- actualRequest:
requestQ = requestQ[1:]
workerQ = workerQ[1:]
}
}
}()
}
func (s *QueuedScheduler) ConfigeMasterChan(c chan engine.Request) {
panic("")
}
Scheduler接口中也需要加上这几个方法
type Scheduler interface {
Submit(Request)
ConfigeMasterChan(chan Request)
WorkerReady(chan Request)
Run()
}
Run方法也需要修改一下,不再直接创建in,而是由worker自己创建
func (e *ConCurrentEngine) Run(seeds ...Request){
out := make(chan ParseResult)
for i:= 0;i<e.WorkerCount;i++{
createWorker(out,e.Scheduler)
}
e.Scheduler.Run()
for _,r := range seeds{
e.Scheduler.Submit(r)
}
for {
result := <- out
for _,item := range result.Item{
log.Printf("Got Item :%v",item)
}
for _,request := range result.Request{
e.Scheduler.Submit()
}
}
}
func createWorker(out chan ParseResult,s Scheduler){
in := make(chan Request)
go func(){
s.WorkerReady(in)
request := <- in
result,err := worker(request)
if err!= nil{
continue
}
out <- result
}()
}
func worker(r Request) (ParseResult, error) {
log.Printf("Fetching URL:%v", r.URL)
s, err := fetcher.Fetch(r.URL)
if err != nil {
log.Printf("error : %s : %v", r.URL, err)
return ParseResult{}, err
}
result := r.ParseFunc(s)
return result, nil
}
重构
我们这里使用了两种方式来实现scheduler,写完第二种方式之后,发现第一种没办法使用了,因为接口Scheduler的一些方法没有实现,并且engine.Run方法也改变了
所以需要重构一下,使两种方式可以方便的切换使用。
两种方式的主要区别在于,一个只使用一个channel发送给worker,另一个对每个worker都有一个channel,所以可以在第一种方式中,为channel赋值的函数只调用一次
scheduler修改
package scheduler
import "GOProject/bingfa/engine"
type SimpleScheduler struct {
workerChan chan engine.Request
}
func (s *SimpleScheduler) Submit(r engine.Request) {
go func() { s.workerChan <- r }()
}
func (s *SimpleScheduler) ConfigeMasterChan(c chan engine.Request) {
s.workerChan = c
}
//二者都添加一个方法,用于返回具体使用的channel
func (s *SimpleScheduler) WorkerRequest() chan engine.Request {
return s.workerChan
}
func (s *SimpleScheduler) WorkerReady(w chan engine.Request) {
}
//在该方法中赋值,因为只调用了一遍,只生成一个channel
func (s *SimpleScheduler) Run() {
c := make(chan engine.Request)
s.workerChan = c
}
//QueuedScheduler则是在每次调用返回函数时创建一个与worker对应的channel
func (s *QueuedScheduler) WorkerRequest() chan engine.Request {
c := make(chan engine.Request)
return c
}
//engine中的createWorker也需要修改一下
func createWorker(in chan Request, out chan ParseResult,s Scheduler){
go func(){
for{
s.WorkerReady()
request := <- in
result,err := worker(request)
if err != nil{
continue
}
out <- result
}
}()
}
去重
只需要创建一个map,判断一个url是否存在,如果存在就跳过,不存在就爬取,并且将这个url放到map中
var mmm = make(map[string]bool)
func IsRepeat(url string) bool{
if _,ok := mmm[url];ok{
return true
}
mmm[url] = true
return false
}
//在engine.Run方法中,每次调用Scheduler.Submit前进行判断
if IsRepeat(r.URL){
continue
}