這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前言
如何?一個爬蟲系統或則簡單的小指令碼?一般是定義一個入口頁面,然後一個頁面會有其他頁面的URL,於是從當前頁面擷取到這些URL加入到爬蟲的抓取隊列中,然後進入到新頁面後再遞迴的進行上述的操作,其實說來就跟深度遍曆或廣度遍曆一樣。
golang由於其編譯速度很快,而且對並發(goroutine)的天然支援,配合chan的協程處理,可以很好地實現一個穩定高效的爬蟲系統.
用到的包
完全不藉助第三方的架構,通過go sdk的標準庫來實現一個爬蟲應用,主要用到的包
- net/http 標準庫裡內建了對http協議的支援,實現了一個http client,可以直接通過其進行get,post等請求
- strings 不像java的String是一個參考型別,go語言中的字串類型是一個內建的基礎類型, 而且go語言預設只支援UTF-8編碼,strings包實現了一些簡單的針對utf-8字串操作的函數
- regexp go sdk中的Regex包
- io/ioutil io處理的工具包
- encoding/xml 解析xml的包
channel機制
Golang在並發設計方面參考了C.A.R Hoare的CSP,即Communicating Sequential Processes並行存取模型理論。 CSP模型的訊息傳遞在收發訊息進程間包含了一個交會點,即發送方只能在接收方準備好接收訊息時才能發送訊息。
golang在其並發實現中,主要是用channel來實現通訊的。其中channel包括兩種,緩衝的channel和非緩衝的channel.
- 緩衝的channel:保證往緩衝中存資料先於對應的取資料,簡單說就是在取的時候裡面肯定有資料,否則就因取不到而阻塞.
- 非緩衝的channel:保證取資料先於存資料,就是保證存的時候肯定有其他的goroutine在取,否則就因放不進去而阻塞。
Go Channel基本操作文法
Go Channel的基本操作文法如下:
c := make(chan bool) //建立一個無緩衝的bool型Channel
c <- x //向一個Channel發送一個值<- c //從一個Channel中接收一個值x = <- c //從Channel c接收一個值並將其儲存到x中x, ok = <- c //從Channel接收一個值,如果channel關閉了或沒有資料,那麼ok將被置為false
不帶緩衝的Channel兼具通訊和同步兩種特性,適合協調多個routines。
for/select的基本操作
我們在使用select時很少只是對其進行一次evaluation,我們常常將其與for {}結合在一起使用,並選擇適當時機從for{}中退出。
for { select { case x := <- somechan: // … 使用x進行一些操作 case y, ok := <- someOtherchan: // … 使用y進行一些操作, // 檢查ok值判斷someOtherchan是否已經關閉 case outputChan <- z: // … z值被成功發送到Channel上時 default: // … 上面case均無法通訊時,執行此分支 }}
range操作
Golang中的for range除了可以迭代一些集合類型還可以來迴圈從channel中取資料,當channel中無資料時便阻塞當前迴圈。
for url := range urlChannel { fmt.Println("routines num = ", runtime.NumGoroutine(), "chan len = ", len(urlChannel)) go Spy(url)}
goroutine
Go語言通過goroutine提供了對於並發編程的非常清晰直接的支援,但goroutine是Go語言運行庫的功能,不是作業系統提供的功能,goroutine不是用線程實現的.goroutine就是一段代碼,一個函數入口,以及在堆上為其分配的一個堆棧。所以它非常廉價,我們可以很輕鬆的建立上萬個goroutine,但它們並不是被作業系統所調度執行
除了被系統調用阻塞的線程外,Go運行庫最多會啟動$GOMAXPROCS個線程來運行goroutine
實現CSDN博文爬蟲
由於實現的爬蟲功能簡單,所以所有代碼均再main包下完成.
首先我們需要在main包下聲明一個全域的urlchannel用來同步開啟的多個routines在某個頁面擷取的<a> 標籤的href屬性
var urlChannel = make(chan string, 200) //chan中存入string類型的href屬性,緩衝200
聲明在html文檔中擷取<a> 的Regex
var atagRegExp = regexp.MustCompile(`<a[^>]+[(href)|(HREF)]\s*\t*\n*=\s*\t*\n*[(".+")|('.+')][^>]*>[^<]*</a>`) //以Must首碼的方法或函數都是必須保證一定能執行成功的,否則將引發一次panic
入口函數main
當進入main函數時,將啟動一個goroutine來從入口url=”http:/blog.csdn.net”開始爬取(Spy函數)頁面內容分析<a> 標籤
接下來通過for range urlChannel來迴圈取出爬取到的<a> 標籤中的href屬性,並再次開啟一個新的goroutine來爬取這個href屬性對應的html文檔內容
func main() { go Spy("http:/blog.csdn.net") //go Spy("http://www.iteye.com/") for url := range urlChannel { fmt.Println("routines num = ", runtime.NumGoroutine(), "chan len = ", len(urlChannel)) //通過runtime可以擷取當前運行時的一些相關參數等 go Spy(url) } fmt.Println("a")}
Spy函數
由於每個爬取goroutine都是調用Spy函數來分析一個url對應的html文檔,所以需要在函數開始就defer 一個匿名函數來處理(recover)可能出現的異常(panic),防止異常導致程式終止,defer執行的函數會在當前函數執行完成後結果返回前執行,無論該函數是panic的還是正常執行
defer func() { if r := recover(); r != nil { log.Println("[E]", r) } }()
由於go內建了對http協議的支援,可以直接通過http包下的http.Get或則http.Post函數來請求url.但由於大部分網站對請求都有防範DDOS等的限制,需要自訂請求的header,設定Proxy 伺服器(CSDN好像對同一IP的請求平率限制並不嚴格,iteye親測很嚴格,每分鐘上萬會被封住IP)等操作,可以使用http包下的http.NewRequest(method, urlStr string, body io.Reader) (*Request, error)函數,然後通過Request的Header對象設定User-Agent,Host等,最後調用http包下內建的DefaultClient對象的Do方法完成請求.
當拿到伺服器響應後(*Response)通過ioutil包下的工具函數轉換為string,找出文檔中的<a>標籤 分析出href屬性,存入urlChannel中.
func Spy(url string) { defer func() { if r := recover(); r != nil { log.Println("[E]", r) } }() req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", GetRandomUserAgent()) client := http.DefaultClient res, e := client.Do(req) if e != nil { fmt.Errorf("Get請求%s返回錯誤:%s", url, e) return } if res.StatusCode == 200 { body := res.Body defer body.Close() bodyByte, _ := ioutil.ReadAll(body) resStr := string(bodyByte) atag := atagRegExp.FindAllString(resStr, -1) for _, a := range atag { href,_ := GetHref(a) if strings.Contains(href, "article/details/") { fmt.Println("☆", href) }else { fmt.Println("□", href) } urlChannel <- href } }}
隨機偽造User-Agent
var userAgent = [...]string{"Mozilla/5.0 (compatible, MSIE 10.0, Windows NT, DigExt)", "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, 360SE)", "Mozilla/4.0 (compatible, MSIE 8.0, Windows NT 6.0, Trident/4.0)", "Mozilla/5.0 (compatible, MSIE 9.0, Windows NT 6.1, Trident/5.0,", "Opera/9.80 (Windows NT 6.1, U, en) Presto/2.8.131 Version/11.11", "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, TencentTraveler 4.0)", "Mozilla/5.0 (Windows, U, Windows NT 6.1, en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", "Mozilla/5.0 (Macintosh, Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", "Mozilla/5.0 (Macintosh, U, Intel Mac OS X 10_6_8, en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", "Mozilla/5.0 (Linux, U, Android 3.0, en-us, Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13", "Mozilla/5.0 (iPad, U, CPU OS 4_3_3 like Mac OS X, en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, Trident/4.0, SE 2.X MetaSr 1.0, SE 2.X MetaSr 1.0, .NET CLR 2.0.50727, SE 2.X MetaSr 1.0)", "Mozilla/5.0 (iPhone, U, CPU iPhone OS 4_3_3 like Mac OS X, en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", "MQQBrowser/26 Mozilla/5.0 (Linux, U, Android 2.3.7, zh-cn, MB200 Build/GRJ22, CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"}var r = rand.New(rand.NewSource(time.Now().UnixNano()))func GetRandomUserAgent() string { return userAgent[r.Intn(len(userAgent))]}
解析<a> 元素
<a> 可以當做一份xml文檔(只有一個a為根節點的簡單xml)來解析出href/HREF屬性,通過go標準庫中xml.NewDecoder來完成
func GetHref(atag string) (href,content string) { inputReader := strings.NewReader(atag) decoder := xml.NewDecoder(inputReader) for t, err := decoder.Token(); err == nil; t, err = decoder.Token() { switch token := t.(type) { // 處理元素開始(標籤) case xml.StartElement: for _, attr := range token.Attr { attrName := attr.Name.Local attrValue := attr.Value if(strings.EqualFold(attrName,"href") || strings.EqualFold(attrName,"HREF")){ href = attrValue } } // 處理元素結束(標籤) case xml.EndElement: // 處理字元資料(這裡就是元素的文本) case xml.CharData: content = string([]byte(token)) default: href = "" content = "" } } return href, content}
總結
通過以上代碼,一個簡單的網路爬蟲就實現了.而且對goroutine和range的配合使用基本就瞭解了.但如你所見,goroutine的運行機制和chan的設計原理絕非以上寥寥數句代碼就可窺見其真面目.
go語言實現的簡單爬蟲來爬取CSDN博文