如何用 Go 實作一個簡單的 PTT 爬蟲

2023/10/04閱讀時間約 9 分鐘


raw-image

👨‍💻 簡介

最近想要透過小實作來撰寫筆記,達到做中學的效果,因此就來實作個小爬蟲順便結合前面學到的package做一個小複習。

建立HTTP Client

Go的net/http package 提供了一個HTTP Client,用來發送各種HTTP請求。

  • http.Get:發送GET請求。
  • http.Post:發送POST請求。
  • http.NewRequest:建立一個新的HTTP請求。

語法如下:

// 發送GET請求
func http.Get(url string) (resp *http.Response, err error)

// 發送POST請求
func http.Post(url, contentType string, body io.Reader) (resp *http.Response, err error)

// 建立一個新的HTTP請求
func http.NewRequest(method, url string, body io.Reader) (*http.Request, error)

常見的Response可以使用以下欄位

type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
Header Header
Body io.ReadCloser
...
}

接著看一下io.ReadCloser

type ReadCloser interface {
Reader
Closer
}

可以看到ReadCloser是 interface,接著來看一下Reader

type Reader interface {
Read(p []byte) (n int, err error)
}

Reader也是一個interface,裡面有定義了Read方法,因此可以推測出resp.Body(作為一個 io.ReadCloser)也實現了 io.Reader interface。

package main

import (
"fmt"
"io"
"net/http"
)
func main() {
// 使用 http.Get 發送GET請求
resp, err := http.Get("https://www.example.com")
if err != nil {
fmt.Println("Error:", err)
return
}

// 拿到Body最後要確保有關閉連線
defer resp.Body.Close()

// 語法為func ReadAll(r io.Reader) ([]byte, error)
// 會回傳[]byte,要透過string package轉為string
body, _ := io.ReadAll(resp.Body)
fmt.Println("status code: ", resp.StatusCode)
fmt.Println("body: ")
fmt.Println(string(body))
}

在上面的範例中,簡單的使用http.Get取得網頁的相關資訊。接下來試著寫一個簡單的爬蟲,來爬取ptt。

爬取PTT八卦版標題

1. 建立http client以及自定義請求

client := &http.Client{}
req, err := http.NewRequest("GET", "https://www.ptt.cc/bbs/Gossiping/index.html", nil)
if err != nil {
log.Fatal(err)
}

因為八卦版會有詢問是否年滿18,因此會需要設定cookie,而要設定cookie則必須使用自定義請求的方式,使用自定義請求則必須自己建立client進行請求的發送。

2. 設定cookie

先去網站上看cookie的name跟value為多少

raw-image

接著設定對應的參數

// 設定cookie,可
req.AddCookie(&http.Cookie{Name: "over18", Value: "1"})

3. 發送請求

resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()

使用剛剛建立的client放入自定義請求就可以完成請求的發送並取得回傳。

4. 解析回傳資訊

在進行這一步之前需要先安裝一個 go package,github.com/PuerkitoBio/goquery,主要用來解析html的標籤屬性。

go get github.com/PuerkitoBio/goquery

等一下會用到的方法為

func NewDocumentFromReader(r io.Reader) (*Document, error)

而Document是一個struct

type Document struct {
*Selection
}

要根據css selector下去找資料,可以使用Find方法,而當找到匹配的元素時,對這個元素接著使用Each方法取得相關屬性,像是Text或是Attr等

func (s *Selection) Find(selector string) *Selection
func (s *Selection) Each(f func(int, *Selection)) *Selection

接著依照ptt的網站結構查看title的標籤為div,class為title,而裡面還有一層包著超連結,因此要爬取的路徑就可以依照下面的格式撰寫

doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatal(err)
}

// 提取文章標題
doc.Find("div.title a").Each(func(index int, item *goquery.Selection) {
title := item.Text()
fmt.Println(title)
})

以下為完整程式碼

package main

import (
"fmt"
"log"
"net/http"
"github.com/PuerkitoBio/goquery"
)

func main() {
client := &http.Client{}
req, err := http.NewRequest("GET", "https://www.ptt.cc/bbs/Gossiping/index.html", nil)
if err != nil {
log.Fatal(err)
}

// 設定cookie模擬已滿18歲的使用者
req.AddCookie(&http.Cookie{Name: "over18", Value: "1"})

resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()

doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatal(err)
}

// 提取文章標題
doc.Find("div.title a").Each(func(index int, item *goquery.Selection) {
title := item.Text()
fmt.Println(title)
})
}

📚Reference

16會員
75內容數
golang
留言0
查看全部
發表第一個留言支持創作者!