這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
剛好七八雙月結束,工作整理完畢,下個雙月OKR還沒開始。做久了IOS開發也來擴充下領域,抽空幾天學了下Golang,實現一個爬蟲。
一、知識要點
1、爬蟲
1.1 工作方式
傳統爬蟲從一個或若干初始頁面的URL開始,獲得初始頁面上的URL,在抓取網頁的過程中,不斷從當前頁面上抽取新的URL放入隊列,直到滿足系統的一定停止條件。聚焦爬蟲的工作流程較為複雜,需要根據一定的網頁分析演算法過濾與主題無關的連結,保留有用的連結並將其放入等待抓取的URL隊列。然後,它將根據一定的搜尋策略從隊列中選擇下一步要抓取的網頁URL,並重複上述過程,直到達到系統的某一條件時停止。另外,所有被爬蟲抓取的網頁將會被系統存貯,進行一定的分析、過濾,並建立索引,以便之後的查詢和檢索;對於聚焦爬蟲來說,這一過程所得到的分析結果還可能對以後的抓取過程給出反饋和指導。
1.2 分類
- 全網爬蟲,爬行對象從一些種子 URL 擴充到整個 Web,主要為門戶網站搜尋引擎和大型 Web 服務供應商採集資料。
- 聚焦網路爬蟲,是指選擇性地爬行那些與預先定義好的主題相關頁面的網路爬蟲。
- 增量式網路爬蟲,是指對已下載網頁採取增量式更新和只爬行新產生的或者已經發生變化網頁的爬蟲,它能夠在一定程度上保證所爬行的頁面是儘可能新的頁面。
- Deep Web 爬蟲,表層網頁是指傳統搜尋引擎可以索引的頁面,以超連結可以到達的靜態網頁為主構成的Web頁面。Deep Web 是那些大部分內容不能通過靜態連結擷取的、隱藏在搜尋表單後的,只有使用者提交一些關鍵詞才能獲得的 Web 頁面。
1.3爬蟲演算法
深度優先策略
其基本方法是按照深度由低到高的順序,依次訪問下一級網頁連結,直到不能再深入為止。 爬蟲在完成一個爬行分支後返回到上一連結節點進一步搜尋其它連結。 當所有連結遍曆完後,爬行任務結束。 這種策略比較適合垂直搜尋或站內搜尋, 但爬行頁面內容層次較深的網站時會造成資源的巨大浪費。
廣度優先策略
此策略按照網頁內容目錄層次深淺來爬行頁面,處於較淺目錄層次的頁面首先被爬行。 當同一層次中的頁面爬行完畢後,爬蟲再深入下一層繼續爬行。 這種策略能夠有效控制頁面的爬行深度,避免遇到一個無窮深層分支時無法結束爬行的問題,實現方便,無需儲存大量中間節點,不足之處在於需較長時間才能爬行到目錄層次較深的頁面
2、golang
2.1 文法學習
2.2 環境安裝
1.1 在MacOSX上安裝
- 下載地址
- 源碼包:go1.4.linux-amd64.tar.gz。
- 將下載的源碼包解壓至 /usr/local目錄。
tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz
- 將 /usr/local/go/bin 目錄添加至PATH環境變數:
export PATH=$PATH:/usr/local/go/bin
- 注意:MAC 系統下你可以使用 .pkg 結尾的安裝包直接雙擊來完成安裝,安裝目錄在 /usr/local/go/ 下。
1.2 其他方式
參考連結
二、代碼實現
先確立一個小目標,就是我們要爬取的網頁的資料來源是什麼。一直覺得國內的大學排名爭議比較有趣,TOP2的兩所,但是TOP5的有8所,TOP10的有20所,哈哈,所以來爬個大學熱門排行榜玩玩吧。
1、網頁抓取
1.1 定義一個學校
type SchoolObj struct { rankTypeName string RankIndex int SchoolName string EnrollOrder string StarLevel string LocationName string SchoolType string UrlAddress string SchoolTags []string}
1.2 單頁面html解析
引入go語言的http函數包和上面定義的學校結構題
發起一個網頁請求返回,go語言會返回網頁的<html>以下全部的html格式字串
如何從這些字串中遍曆尋找和解析出我們需要的學校排名欄位?
因為有過前端開發的經驗,我自然而然想到,使用CSS選取器會比直接使用遍曆演算法來得高效,有CSS的選擇規則,我可以批量規律的擷取和處理HTML的DOM結構資料。端開發中的jQuery提供了方便的操作 DOM 的 API。使用 Go 語言做伺服器端開發,有時候需要解析 HTML 檔案,比如抓取網站內容、寫一個爬蟲等。這時候如果有一個類似 jQuery 的庫可以使用,操作 DOM 會很方便,而且,上手也會很快。果然,還真有這樣的工具,此處推薦一個GitHub的開源架構 --- Goquery 。
A、使用介紹:
goquery定義了一個Document結構,直接對應網頁Javascript的Document節點,通過一個NewDocument方法,傳入參數地址為網頁的url地址,直接生產一個虛擬go語言上的dom。
type Document struct { *Selection Url *url.URL rootNode *html.Node}func NewDocument(url string) (*Document, error) { // Load the URL res, e := http.Get(url) if e != nil { return nil, e } return NewDocumentFromResponse(res)}
Document有定義find方法,方法的使用和JQuery裡面一直,傳入目標字串的css選取器即可。通過對Document執行find尋找方法,獲得全部學校目標的字串數組。
doc.Find(".bangTable table tr")
這裡的選取器怎麼來的呢,我們在chrome裡面開啟url地址,找到我們想要收集的資料排名,右鍵開啟審查元素,可以看到HTML的選取器名稱。這裡需要有一點CSS基礎,因為有的選取器不是直接唯一的,需要自己去判斷,怎樣的選取器組合才能準確的拿到想要的目標字串。
Document有定義each方法,用於遍曆數組,也就是各個大學所對應的dom節點。在each方法中繼續使用尋找方法,並最後獲得想要的字串。
每一個dom對應一個SchoolStruct,建立並賦值,放入數組中返回。
B、代碼如下:
import ( "github.com/PuerkitoBio/goquery" "SchoolReptile/struct" "net/http")func GaokaoquanRank(urlAddress string) []SchoolStruct.SchoolObj { var array [] SchoolStruct.SchoolObj doc, err := goquery.NewDocument(urlAddress) if err != nil { log.Fatal(err) } // Find the review items doc.Find(".bangTable table tr").Each(func(i int, s *goquery.Selection) { // For each item found, get the band and title var obj SchoolStruct.SchoolObj obj.RankIndex = s.Find(".t1 span").Text() obj.SchoolName = s.Find(".t2 a").Text() obj.UrlAddress ,_ = s.Find(".t2 a").Attr("href") obj.LocationName = s.Find(".t3").Text() obj.SchoolType = s.Find(".t4").Text() obj.StarLevel = s.Find(".t5").Text() obj.EnrollOrder = "本科第一批" array = append(array, obj) }) return array}
2、介面請求
我們再爬去資料的時候,一般都能直接抓取網頁資料,但是有的資料在第一頁炳輝展示出來,需要有點擊操作,比如載入更多。此處的大學排行有200位,第一頁請求只有20位,這時候就會發現,介面請求的方便。
有的網頁在介面上做了cookie校正,摸清別人的請求規則,才能正確類比出請求獲得返回資料。
我們此處拿樂學高考作文例子,擷取各個類型的大學熱門排行榜。通過charles代理,我們獲得請求的各類參數。
url := LexueHost+"/college/ranking?page="+pageStr+"&rank_type="+rankObj.RankType+"&page_size=15"
網路請求返回的是一個字串結構的資料,我們需要把它映射成map結構好擷取key對應的value值。
這裡推薦一個go語言在json解析上的一個開源庫Simplejson,將返回的資料進行JSON結構化,然後通過get方法可以直接獲得對應的參數值。
defer resp.Body.Close()data, err := ioutil.ReadAll(resp.Body)jsonBody,err := simplejson.NewJson(data)schoolJsonArray,err := jsonBody.Get("schools").Array()
- 多頁請求使用遞迴的方式,不斷改變get請求的pageStr參數,pageindex ++ ,當判斷請求返回的json為空白的時候,則說明介面請求已經到到了最後一頁,跳出遞迴
var nextArray [] SchoolStruct.SchoolObjnextArray = LexueRankEachList(rankObj,pageIndex)
B、代碼如下:
import ( "SchoolReptile/struct" "net/http" "io/ioutil" "fmt" "bytes" "encoding/json" "strings" "github.com/bitly/go-simplejson" "strconv")func LexueRankEachList(rankObj SchoolStruct.RankTypeObj,pageIndex int ) []SchoolStruct.SchoolObj { pageStr := strconv.Itoa(pageIndex) url := LexueHost+"/college/ranking?page="+pageStr+"&rank_type="+rankObj.RankType+"&page_size=15" resp, err := http.Get(url) if err != nil { // handle error } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) jsonBody,err := simplejson.NewJson(data) schoolJsonArray,err := jsonBody.Get("schools").Array() var array [] SchoolStruct.SchoolObj if len(schoolJsonArray) <= 0 { println("請求到頭了") return array } for i,_ := range schoolJsonArray { schoolJson := jsonBody.Get("schools").GetIndex(i) var obj SchoolStruct.SchoolObj obj.RankIndex = strconv.Itoa(schoolJson.Get("school_rank").MustInt()) obj.SchoolName = schoolJson.Get("school_name").MustString() obj.SchoolTags = schoolJson.Get("school_tags").MustStringArray() array = append(array, obj) println(obj.RankIndex,obj.SchoolName,obj.SchoolTags) } pageIndex++ var nextArray [] SchoolStruct.SchoolObj nextArray = LexueRankEachList(rankObj,pageIndex) if len(nextArray) > 0 { for _,obj := range nextArray { array = append(array,obj) } } return array}
3、儲存到Excel
前兩部獲得了網路資料,並解析產生了對應的SchoolStruct數組,這個時候我們只需要建立excel邊。遍曆數組,把數組裡面的資料欄位都存入表格即可,git開源庫xlsx能夠讓我們輕鬆的建立、尋找、賦值Excel表。
代碼如下:
func SaveSchoolRank(schoolArray [] SchoolStruct.SchoolObj,excelName string,sheetName string) { var file *xlsx.File var sheet *xlsx.Sheet var row *xlsx.Row var cell *xlsx.Cell var err error file,err = xlsx.OpenFile(excelName + ".xlsx") if err != nil { file = xlsx.NewFile() sheet,err = file.AddSheet(sheetName) } else { sheet = file.Sheet[sheetName] } if err == nil { for i := 0; i < len(schoolArray); i++ { obj := schoolArray[i] row = sheet.AddRow() cell = row.AddCell() cell.Value = obj.RankIndex cell = row.AddCell() cell.Value = obj.SchoolName cell = row.AddCell() cell.Value = obj.StarLevel cell = row.AddCell() cell.Value = obj.LocationName cell = row.AddCell() cell.Value = obj.EnrollOrder cell = row.AddCell() cell.Value = obj.SchoolType cell = row.AddCell() cell.Value = obj.UrlAddress var tagStr string for _,value := range obj.SchoolTags { tagStr += "+" + value } cell = row.AddCell() cell.Value = tagStr if err != nil { fmt.Printf(err.Error()) } } } err = file.Save(excelName + ".xlsx") if err != nil { fmt.Printf(err.Error()) }}