GoLang - 并發(fā)版爬蟲

原文鏈接

并發(fā)版爬蟲架構(gòu)

go_spider.png

將爬蟲分為兩部分:

一酪劫、隊(duì)列調(diào)度器:提供下載請求給Process

二舀凛、Process:包括下載請求扼仲、解析下載的內(nèi)容远寸、返回新請求列表給隊(duì)列調(diào)度器、輸出下載內(nèi)容屠凶。

具體實(shí)現(xiàn):

  • 在主線程中使用一個(gè)隊(duì)列調(diào)度器來儲存待訪問的請求驰后,若隊(duì)列調(diào)度器為空并且沒有正在運(yùn)行的Process線程,則認(rèn)為爬蟲結(jié)束矗愧。
  • 主線程控制Process線程并發(fā)的數(shù)量灶芝,執(zhí)行處理在隊(duì)列調(diào)度器中得到的請求。下載請求的內(nèi)容唉韭,交由頁面處理器處理夜涕,下載失敗則重新添加該鏈接到隊(duì)列調(diào)度器中。
  • 判斷頁面處理器返回的請求鏈接是否訪問過属愤,若未訪問則加入到隊(duì)列調(diào)度器女器。將解析得到的內(nèi)容交由輸出線程處理。

爬蟲引擎

package spider

import (
    "downloader"
    "github.com/PuerkitoBio/goquery"
    "log"
    "pageprocess"
    "pipeline"
    "scheduler"
    "strconv"
    "time"
)

// threadnum   - 線程數(shù)量
// scheduler   - 調(diào)度器
// downloader  - 下載器
// pageprocess - 頁面處理  
// pipeline    - 輸出      
type Spider struct {
    threadnum uint8
    scheduler scheduler.Scheduler
    downloader downloader.DownLoader
    pageprocess pageprocess.PageProcess
    pipeline pipeline.PipeLine
}
// NewSpider 創(chuàng)建一個(gè)爬蟲引擎
func NewSpider(threadnum int,path string) *Spider{
    return &Spider{
        scheduler:scheduler.NewQueueSCheduler(),
        downloader:downloader.NewHttpDownLoader(),
        pageprocess:pageprocess.NewPageProcess(),
        pipeline:pipeline.NewFilePipeLine(path),
        threadnum:uint8(threadnum),
    }
}
// Run 引擎運(yùn)行
func (s *Spider) Run(){
  // Process并發(fā)數(shù)量
    rm := NewResourceManagerChan(s.threadnum)
    log.Println("[Spider] 爬蟲運(yùn)行 - 處理線程數(shù):" + strconv.Itoa(rm.Cap()))
    for{
        url,ok := s.scheduler.Pop()
        // 爬取隊(duì)列為空 并且 沒有Process線程在處理 認(rèn)為爬蟲結(jié)束
        if ok == false && rm.Has() == 0{
            log.Println("[Spider] 爬蟲運(yùn)行結(jié)束")
            break
        }else if ok == false{ // Process線程正在處理住诸,可能還會有新的請求加入調(diào)度
            log.Println("[Spider] 爬取隊(duì)列為空 - 等待處理")
            time.Sleep(500 * time.Millisecond)
            continue
        }
        // 控制Process線程并發(fā)數(shù)量
        rm.GetOne()
        go func(url string) {
            defer rm.FreeOne()
            s.Process(url)
        }(url)
    }
}
// 添加請求鏈接
func (s *Spider) AddUrl(url string) *Spider{
    s.scheduler.Push(url)
    return s
}
func (s *Spider) AddUrls(urls []string) *Spider{
    for _,url := range urls{
        s.scheduler.Push(url)
    }
    return s
}
// 處理請求鏈接
func (s *Spider) Process(url string){
  // 下載鏈接
    resp := s.downloader.DownLoad(url)
    if resp == nil{
        /*下載失敗重新加入調(diào)度隊(duì)列中*/
        if !s.downloader.Visited(url){
            s.scheduler.Push(url)
        }
        return
    }
  // 頁面處理 - 使用goquery包簡單處理
    doc,err := goquery.NewDocumentFromReader(resp.Body)
    if err != nil{
        log.Println("[Process] 解析錯(cuò)誤")
        s.scheduler.Push(url)
        return
    }
    // 將新請求鏈接加入到調(diào)度器中
    links := s.pageprocess.Process(doc)
    for _,url := range links{
        if !s.downloader.Visited(url){
            s.scheduler.Push(url)
        }
    }
    // 輸出文檔
    go s.pipeline.Process(doc)
}
// 控制線程并發(fā)數(shù)
package spider

type ResourceManager struct {
    tc chan uint8
}

func NewResourceManagerChan(num uint8) *ResourceManager{
    tc := make(chan uint8,num)
    return &ResourceManager{tc:tc}
}

func (r *ResourceManager) GetOne(){
    r.tc <- 1
}

func (r *ResourceManager) FreeOne(){
    <- r.tc
}

func (r *ResourceManager) Cap() int{
    return cap(r.tc)
}

func (r *ResourceManager) Has() int{
    return len(r.tc)
}

func (r *ResourceManager) Left() int{
    return cap(r.tc) - len(r.tc)
}

隊(duì)列調(diào)度器

隊(duì)列調(diào)度器實(shí)現(xiàn)獲取以及儲存請求驾胆。

請求的重復(fù)性交給下載器來判斷(考慮只有下載成功的請求才不需要訪問)。

簡化的請求為string類型的url鏈接贱呐。

package scheduler

import (
    "container/list"
    "crypto/md5"
    "sync"
)

type QueueScheduler struct {
    queue    *list.List
    locker   *sync.Mutex
    listkey  map[[md5.Size]byte] *list.Element
}

func NewQueueSCheduler() *QueueScheduler{
    queue   := list.New()
    locker  := new(sync.Mutex)
    listkey := make(map[[md5.Size]byte] *list.Element)

    return &QueueScheduler{
        queue:queue,
        locker:locker,
        listkey:listkey}
}

// Pop - 從隊(duì)列中獲取一個(gè)鏈接
func (s *QueueScheduler) Pop() (string,bool){
    s.locker.Lock()
    if s.queue.Len() <= 0{
        s.locker.Unlock()
        return "",false
    }
    e := s.queue.Front()
    ret := e.Value.(string)
    // 清除listkey中該元素,加入到訪問隊(duì)列中
    key := md5.Sum([]byte(ret))
    delete(s.listkey,key)
    s.queue.Remove(e)
    s.locker.Unlock()
    return ret,true
}

// Push - 將鏈接放入隊(duì)列中
func (s *QueueScheduler) Push(url string){
    s.locker.Lock()
    key := md5.Sum([]byte(url))
    // 鏈接已存在
    if _,ok := s.listkey[key]; ok{
        s.locker.Unlock()
        return
    }
    e := s.queue.PushBack(url)
    s.listkey[key] = e
    s.locker.Unlock()
}

下載器

下載器提供接口下載請求丧诺,并返回下載得到的內(nèi)容。

下載器提供接口判斷請求是否已經(jīng)被處理過吼句。

若下載失敗則標(biāo)記當(dāng)前請求訪問失敗锅必,反之標(biāo)記當(dāng)前請求訪問成功,使用map儲存惕艳。

簡化的下載器僅使用的http包中的Get方法。

package downloader

import (
   "crypto/md5"
   "log"
   "net/http"
   "sync"
)

type HttpDownLoader struct {
   locker *sync.Mutex
   downloaded map[[md5.Size]byte] bool
}

func NewHttpDownLoader() *HttpDownLoader{
   locker := new(sync.Mutex)
   downloaded := make(map[[md5.Size]byte]bool)
   return &HttpDownLoader{
      locker:locker,
      downloaded:downloaded,
   }
}

// 下載鏈接
func (h *HttpDownLoader) DownLoad(url string) *http.Response{
   key := md5.Sum([]byte(url))
   resp,err := http.Get(url)
   h.locker.Lock()
   // 已經(jīng)被訪問過了驹愚,不需要訪問远搪。
   if ok,has := h.downloaded[key]; has && ok{
      h.locker.Unlock()
      return nil
   }
   // 訪問失敗
   if err != nil || resp.StatusCode != http.StatusOK{
      log.Println("[DownLoader] 下載鏈接失敗:" + url)
      h.downloaded[key] = false
      h.locker.Unlock()
      return nil
   }
   h.downloaded[key] = true
   h.locker.Unlock()
   log.Println("[DownLoader] 下載鏈接成功:" + url)
   return resp
}

// 鏈接是否被訪問
func (h *HttpDownLoader) Visited(url string) bool{
   key := md5.Sum([]byte(url))
   var ret bool
   h.locker.Lock()
   if ok,has := h.downloaded[key]; has && ok{
      ret = true
   }else{
      ret = false
   }
   h.locker.Unlock()
   return ret
}

頁面處理

頁面處理需要返回鏈接請求集合,這里簡化為[]string類型逢捺。

頁面處理需要返回文檔谁鳍,這里直接簡化為goquery包中的document

package pageprocess

import (
    "github.com/PuerkitoBio/goquery"
)

type PageProcess struct {
}

func NewPageProcess() PageProcess{
    return PageProcess{}
}

// 返回鏈接函數(shù)
func (p *PageProcess) Process(d *goquery.Document) []string{
    var links []string
  // 獲取鏈接的處理代碼
    return links
}

輸出

package pipeline

import (
    "github.com/PuerkitoBio/goquery"
    "log"
    "os"
)

type FilePipeLine struct {
    dir string
}

func NewFilePipeLine(dir string) *FilePipeLine{
    return &FilePipeLine{dir:dir}
}

func (p *FilePipeLine) Process(doc *goquery.Document){
    // 文件寫入實(shí)現(xiàn)
}

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市倘潜,隨后出現(xiàn)的幾起案子绷柒,更是在濱河造成了極大的恐慌,老刑警劉巖涮因,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件废睦,死亡現(xiàn)場離奇詭異,居然都是意外死亡养泡,警方通過查閱死者的電腦和手機(jī)嗜湃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澜掩,“玉大人购披,你說我怎么就攤上這事〖玳牛” “怎么了刚陡?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長株汉。 經(jīng)常有香客問我橘荠,道長,這世上最難降的妖魔是什么郎逃? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任哥童,我火速辦了婚禮,結(jié)果婚禮上褒翰,老公的妹妹穿的比我還像新娘贮懈。我一直安慰自己,他們只是感情好优训,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布朵你。 她就那樣靜靜地躺著,像睡著了一般揣非。 火紅的嫁衣襯著肌膚如雪抡医。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天早敬,我揣著相機(jī)與錄音忌傻,去河邊找鬼。 笑死搞监,一個(gè)胖子當(dāng)著我的面吹牛水孩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琐驴,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼俘种,長吁一口氣:“原來是場噩夢啊……” “哼秤标!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宙刘,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤苍姜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后悬包,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體衙猪,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年玉罐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屈嗤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吊输,死狀恐怖饶号,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情季蚂,我是刑警寧澤茫船,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站扭屁,受9級特大地震影響算谈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜料滥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一然眼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葵腹,春花似錦高每、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阻肩,卻和暖如春带欢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烤惊。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工乔煞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撕氧。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓瘤缩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伦泥。 傳聞我的和親對象是個(gè)殘疾皇子剥啤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344