剛好七八雙月結(jié)束勺远,工作整理完畢橙喘,下個雙月OKR還沒開始。做久了IOS開發(fā)也來擴展下領(lǐng)域胶逢,抽空幾天學(xué)了下Golang厅瞎,實現(xiàn)一個爬蟲。
一初坠、知識要點
1和簸、爬蟲
1.1 工作方式
傳統(tǒng)爬蟲從一個或若干初始網(wǎng)頁的URL開始,獲得初始網(wǎng)頁上的URL碟刺,在抓取網(wǎng)頁的過程中锁保,不斷從當(dāng)前頁面上抽取新的URL放入隊列,直到滿足系統(tǒng)的一定停止條件。聚焦爬蟲的工作流程較為復(fù)雜,需要根據(jù)一定的網(wǎng)頁分析算法過濾與主題無關(guān)的鏈接爽柒,保留有用的鏈接并將其放入等待抓取的URL隊列吴菠。然后,它將根據(jù)一定的搜索策略從隊列中選擇下一步要抓取的網(wǎng)頁URL浩村,并重復(fù)上述過程做葵,直到達(dá)到系統(tǒng)的某一條件時停止。另外心墅,所有被爬蟲抓取的網(wǎng)頁將會被系統(tǒng)存貯蜂挪,進(jìn)行一定的分析、過濾嗓化,并建立索引,以便之后的查詢和檢索谬哀;對于聚焦爬蟲來說刺覆,這一過程所得到的分析結(jié)果還可能對以后的抓取過程給出反饋和指導(dǎo)。
1.2 分類
- 全網(wǎng)爬蟲史煎,爬行對象從一些種子 URL 擴充到整個 Web谦屑,主要為門戶站點搜索引擎和大型 Web 服務(wù)提供商采集數(shù)據(jù)。
- 聚焦網(wǎng)絡(luò)爬蟲篇梭,是指選擇性地爬行那些與預(yù)先定義好的主題相關(guān)頁面的網(wǎng)絡(luò)爬蟲氢橙。
- 增量式網(wǎng)絡(luò)爬蟲,是指對已下載網(wǎng)頁采取增量式更新和只爬行新產(chǎn)生的或者已經(jīng)發(fā)生變化網(wǎng)頁的爬蟲恬偷,它能夠在一定程度上保證所爬行的頁面是盡可能新的頁面悍手。
- Deep Web 爬蟲,表層網(wǎng)頁是指傳統(tǒng)搜索引擎可以索引的頁面袍患,以超鏈接可以到達(dá)的靜態(tài)網(wǎng)頁為主構(gòu)成的Web頁面坦康。Deep Web 是那些大部分內(nèi)容不能通過靜態(tài)鏈接獲取的、隱藏在搜索表單后的诡延,只有用戶提交一些關(guān)鍵詞才能獲得的 Web 頁面滞欠。
1.3爬蟲算法
-
深度優(yōu)先策略
其基本方法是按照深度由低到高的順序,依次訪問下一級網(wǎng)頁鏈接肆良,直到不能再深入為止筛璧。 爬蟲在完成一個爬行分支后返回到上一鏈接節(jié)點進(jìn)一步搜索其它鏈接。 當(dāng)所有鏈接遍歷完后惹恃,爬行任務(wù)結(jié)束夭谤。 這種策略比較適合垂直搜索或站內(nèi)搜索, 但爬行頁面內(nèi)容層次較深的站點時會造成資源的巨大浪費座舍。
-
廣度優(yōu)先策略
此策略按照網(wǎng)頁內(nèi)容目錄層次深淺來爬行頁面沮翔,處于較淺目錄層次的頁面首先被爬行。 當(dāng)同一層次中的頁面爬行完畢后,爬蟲再深入下一層繼續(xù)爬行采蚀。 這種策略能夠有效控制頁面的爬行深度疲牵,避免遇到一個無窮深層分支時無法結(jié)束爬行的問題,實現(xiàn)方便榆鼠,無需存儲大量中間節(jié)點纲爸,不足之處在于需較長時間才能爬行到目錄層次較深的頁面
2、golang
2.1 語法學(xué)習(xí)
為了讓學(xué)習(xí)更加快速妆够,想要優(yōu)先上手识啦,而不是沉浸在大量語法里面,找了一遍基礎(chǔ)的語法文檔神妹,直接全讀整體語法颓哮,先有個基礎(chǔ)但是全面的認(rèn)識。
語法教程鏈接然后直接手一本web教程書籍鸵荠,此本書籍是開源的冕茅,在github上有1.78萬star,5年前就開始書寫蛹找,一直被追捧和使用姨伤。書籍鏈接
2.2 環(huán)境安裝
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環(huán)境變量:
export PATH=$PATH:/usr/local/go/bin
- 注意:MAC 系統(tǒng)下你可以使用 .pkg 結(jié)尾的安裝包直接雙擊來完成安裝乍楚,安裝目錄在 /usr/local/go/ 下。
1.2 其他方式
二届慈、代碼實現(xiàn)
先確立一個小目標(biāo)徒溪,就是我們要爬取的網(wǎng)頁的數(shù)據(jù)源是什么。一直覺得國內(nèi)的大學(xué)排名爭議比較有趣拧篮,TOP2的兩所词渤,但是TOP5的有8所,TOP10的有20所串绩,哈哈缺虐,所以來爬個大學(xué)排行榜玩玩吧。
1礁凡、網(wǎng)頁抓取
1.1 定義一個學(xué)校
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函數(shù)包和上面定義的學(xué)校結(jié)構(gòu)題
發(fā)起一個網(wǎng)頁請求返回高氮,go語言會返回網(wǎng)頁的<html>以下全部的html格式字符串
如何從這些字符串中遍歷查找和解析出我們需要的學(xué)校排名字段?
因為有過前端開發(fā)的經(jīng)驗顷牌,我自然而然想到剪芍,使用CSS選擇器會比直接使用遍歷算法來得高效,有CSS的選擇規(guī)則窟蓝,我可以批量規(guī)律的獲取和處理HTML的DOM結(jié)構(gòu)數(shù)據(jù)罪裹。端開發(fā)中的jQuery提供了方便的操作 DOM 的 API。使用 Go 語言做服務(wù)器端開發(fā),有時候需要解析 HTML 文件状共,比如抓取網(wǎng)站內(nèi)容套耕、寫一個爬蟲等。這時候如果有一個類似 jQuery 的庫可以使用峡继,操作 DOM 會很方便冯袍,而且,上手也會很快碾牌。果然康愤,還真有這樣的工具,此處推薦一個GitHub的開源框架 --- Goquery 舶吗。
A征冷、使用介紹:
goquery定義了一個Document結(jié)構(gòu),直接對應(yīng)網(wǎng)頁Javascript的Document節(jié)點誓琼,通過一個NewDocument方法资盅,傳入?yún)?shù)地址為網(wǎng)頁的url地址,直接生產(chǎn)一個虛擬的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里面一直每庆,傳入目標(biāo)字符串的css選擇器即可筐带。通過對Document執(zhí)行find查找方法,獲得全部學(xué)校目標(biāo)的字符串?dāng)?shù)組缤灵。
doc.Find(".bangTable table tr")
這里的選擇器怎么來的呢伦籍,我們在chrome里面打開url地址,找到我們想要收集的數(shù)據(jù)排名腮出,右鍵打開審查元素帖鸦,可以看到HTML的選擇器名稱。這里需要有一點CSS基礎(chǔ)胚嘲,因為有的選擇器不是直接唯一的作儿,需要自己去判斷,怎樣的選擇器組合才能準(zhǔn)確的拿到想要的目標(biāo)字符串馋劈。
Document有定義each方法攻锰,用于遍歷數(shù)組,也就是各個大學(xué)所對應(yīng)的dom節(jié)點妓雾。在each方法中繼續(xù)使用查找方法娶吞,并最后獲得想要的字符串。
每一個dom對應(yīng)一個SchoolStruct械姻,新建并賦值妒蛇,放入數(shù)組中返回。
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绣夺、接口請求
我們再爬去數(shù)據(jù)的時候吏奸,一般都能直接抓取網(wǎng)頁數(shù)據(jù),但是有的數(shù)據(jù)在第一頁炳輝展示出來乐导,需要有點擊操作苦丁,比如加載更多。此處的大學(xué)排行有200位物臂,第一頁請求只有20位旺拉,這時候就會發(fā)現(xiàn),接口請求的方便棵磷。
有的網(wǎng)頁在接口上做了cookie校驗蛾狗,摸清別人的請求規(guī)則,才能正確模擬出請求獲得返回數(shù)據(jù)仪媒。
我們此處拿樂學(xué)高考作文例子沉桌,獲取各個類型的大學(xué)排行榜。通過charles代理算吩,我們獲得請求的各類參數(shù)留凭。
- 拼接請求url
url := LexueHost+"/college/ranking?page="+pageStr+"&rank_type="+rankObj.RankType+"&page_size=15"
- 發(fā)送HTTP請求,獲取返回
網(wǎng)絡(luò)請求返回的是一個字符串結(jié)構(gòu)的數(shù)據(jù)偎巢,我們需要把它映射成map結(jié)構(gòu)好獲取key對應(yīng)的value值蔼夜。
這里推薦一個go語言在json解析上的一個開源庫Simplejson,將返回的數(shù)據(jù)進(jìn)行JSON結(jié)構(gòu)化压昼,然后通過get方法可以直接獲得對應(yīng)的參數(shù)值求冷。
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
jsonBody,err := simplejson.NewJson(data)
schoolJsonArray,err := jsonBody.Get("schools").Array()
- 多頁請求使用遞歸的方式,不斷改變get請求的pageStr參數(shù)窍霞,pageindex ++ 匠题,當(dāng)判斷請求返回的json為空的時候,則說明接口請求已經(jīng)到到了最后一頁但金,跳出遞歸
var nextArray [] SchoolStruct.SchoolObj
nextArray = 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
前兩部獲得了網(wǎng)絡(luò)數(shù)據(jù)冷溃,并解析生成了對應(yīng)的SchoolStruct數(shù)組掠哥,這個時候我們只需要創(chuàng)建excel邊。遍歷數(shù)組秃诵,把數(shù)組里面的數(shù)據(jù)字段都存入表格即可,git開源庫xlsx能夠讓我們輕松的創(chuàng)建续搀、查找、賦值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())
}
}