# Day24 Golang 爬蟲框架 colly 來爬鐵人賽文吧 接續昨天`colly`的內容,今天來更深入探討colly的機制吧! ## colly函式作用順序(Call order of callbacks) 1. OnRequest 在發起請求之前,可以預先對Header的參數進行設定 2. OnError 如果在請求的時候發生錯誤 3. OnResponseHeaders 收到響應的標頭時 4. OnResponse 收到響應回復的時候 5. OnHTML 收到的響應是HTML格式時(時間點比 OnResponse還晚),進行goquerySelector篩選 6. OnXML 收到的響應是XML格式時(時間點比 OnHTML還晚),進行xpathQuery篩選 7. OnScraped 抓取網頁(與OnResponse相仿),但在最後才進行調用 有漏掉什麼嗎?應該沒有吧! ```go package main import ( "fmt" "github.com/gocolly/colly/v2" ) func main() { var url = "https://member.ithome.com.tw/login" c := colly.NewCollector() c.OnRequest(func(r *colly.Request) { fmt.Println("1") }) c.OnError(func(_ *colly.Response, err error) { fmt.Println("2") }) c.OnResponseHeaders(func(r *colly.Response) { fmt.Println("3") }) c.OnResponse(func(r *colly.Response) { fmt.Println("4") }) c.OnHTML("body", func(e *colly.HTMLElement) { fmt.Println("5") }) c.OnXML("//footer", func(e *colly.XMLElement) { fmt.Println("6") }) c.OnScraped(func(r *colly.Response) { fmt.Println("7") }) c.Visit(url) } ``` 把以上會用到的函式設定好之後,再進行網頁訪問Visit (不然先發起Visit 再進行設定就來不及了) --- ## 用colly來爬鐵人賽文 今天的目標是要爬 [**Go繁不及備載**](https://ithelp.ithome.com.tw/users/20125192/ironman/3155) 此一系列好文。 伸手之前 ### 開始來爬文 我想自動爬我自己寫的文章, 雖然看似有點沒意義,但人生有時候就是想沒意義一下, 到底該怎麼實作餒? 先打開`開發者工具`找到鐵人賽文章的標題。 ![開發者工具](https://i.imgur.com/BurIMqk.png) 首要任務是**找到進到我各個文章的連結的tag**。 哦,找到了, `class="qa-list__title-link"` 決定是你了,就從這個標籤開始下手。 ```go package main import ( "fmt" "github.com/gocolly/colly" "strings" ) func main() { c := colly.NewCollector() c.OnHTML(".qa-list__title-link", func(e *colly.HTMLElement) { fmt.Println(e.Text, e.Attr("href")) // e.Text 印出 <a> tag 裡面的文字,也就是文章標題 // e.Attr("href") 則是找到 <a> tag裡面的 href元素 }) c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=1") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=2") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=3") } ``` ![文章連結](https://i.imgur.com/uFCGKvB.png) 有了`<a>`中的`網址連結字串`,想辦法點進去每個連結裡面。 ```go func main() { c := colly.NewCollector() c.OnHTML(".qa-list__title-link", func(e *colly.HTMLElement) { // fmt.Println(e.Text, e.Attr("href")) // e.Text 印出 <a> tag 裡面的文字,也就是文章標題 // e.Attr("href") 則是找到 <a> tag裡面的 href元素 linksStr := e.Attr("href") linksStr = strings.Replace(linksStr, " ", "", -1) // 把空白以""取代掉 links := strings.Split(linksStr, "\n") // 以換行符號(\n)做為分隔來切割字串 for _, link := range links { c.OnResponse(func(r *colly.Response) { fmt.Println(string(r.Body)) // 印出所有返回的Response物件r.Body }) c.Visit(link) } }) c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=1") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=2") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=3") } ``` 但我想抓的並不是整個 **Go繁不及備載** 鐵人賽30天文章的原始碼, 而是想要這位作者的精華 的內文。 再稍微修改一下,就完成哩! ```go func main() { c := colly.NewCollector() c.OnHTML(".qa-list__title-link", func(e *colly.HTMLElement) { // fmt.Println(e.Text, e.Attr("href")) // e.Text 印出 <a> tag 裡面的文字,也就是文章標題 // e.Attr("href") 則是找到 <a> tag裡面的 href元素 linksStr := e.Attr("href") linksStr = strings.Replace(linksStr, " ", "", -1) // 把空白以""取代掉 links := strings.Split(linksStr, "\n") // 以換行符號(\n)做為分隔來切割字串 for _, link := range links { c2 := colly.NewCollector() // 這邊要在迴圈一開始再宣告一個 Collector,才不會與原本的混合到 c2.OnHTML(".qa-markdown", func(e2 *colly.HTMLElement) { fmt.Println(e2.Text) // 印出 qa-markdown class中的文字(Go繁不及備載 文章的內文) }) c2.Visit(link) // 找到<a>連結網址後,點進去訪問 } }) c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=1") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=2") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=3") } ``` 爬蟲結果(擷取回傳結果的一小小部分) ![成果展示](https://i.imgur.com/jkArq77.png) 天哪,我的`鐵人30天`文章居然被一覽無遺! ~~豪害羞阿~~ 另外可以像以下這樣再做字數計算, 爬下來就能方便知道自己的文章總共佔了多少字數哩。 ```go package main import ( "fmt" "strings" "github.com/gocolly/colly" ) var wordCount = 0 var chineseCount = 0 func main() { c := colly.NewCollector() c.OnRequest(func(r *colly.Request) { // iT邦幫忙需要寫這一段 User-Agent才給爬 r.Headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36") }) c.OnHTML(".qa-list__title-link", func(e *colly.HTMLElement) { // fmt.Println(e.Text, e.Attr("href")) // e.Text 印出 <a> tag 裡面的文字,也就是文章標題 // e.Attr("href") 則是找到 <a> tag裡面的 href元素 linksStr := e.Attr("href") linksStr = strings.Replace(linksStr, " ", "", -1) // 把空白以""取代掉 links := strings.Split(linksStr, "\n") // 以換行符號(\n)做為分隔來切割字串 for _, link := range links { c2 := colly.NewCollector() // 這邊要在迴圈一開始再宣告一個 Collector,才不會與原本的混合到 c2.OnHTML(".qa-markdown", func(e2 *colly.HTMLElement) { fmt.Println(e2.Text) // 印出 qa-markdown class中的文字(Go繁不及備載 文章的內文) countWord(e2.Text) }) c2.OnRequest(func(r *colly.Request) { // iT邦幫忙需要寫這一段 User-Agent才給爬 r.Headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36") }) c2.Visit(link) // 找到<a>連結網址後,點進去訪問 } }) c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=1") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=2") c.Visit("https://ithelp.ithome.com.tw/users/20125192/ironman/3155?page=3") fmt.Println("英文+中文+數字 共", wordCount, "字") fmt.Println("純中文字 共", chineseCount, "字") } func countWord(input string) { for _, word := range input { if word != 32 && word != 10 { // 計算有多少非空白(space)以及換行(\n)的字數 wordCount++ } if word > 256 { // 計算有多少中文字數(編碼比ASCII大的字) chineseCount++ } } } ``` ###### 有些HTML元素在**會員登入**後才會顯示,像是右上角的`使用者名稱`。這部分就需要帶值(帳號密碼)做模擬登入,會比較搞剛。