前言
記得剛開始學習 Go 好像是在 2013 年的時候,不過後來就一直沒太多機會使用。最近剛好想寫個爬蟲,於是就決定用 Go 來練習看看。
這次的目標是 Plurk 噗浪上的「偷偷說河道」。
本次的程式放在 shaform/experiments/plurk_crawler。
環境設定
這次只有用到 schollz/progressbar 來顯示進度,其他都是使用 Go 內建的函式庫,故只要安裝一個套件:
go get -u github.com/schollz/progressbar
撰寫爬蟲
分析網頁
首先透過觀察網路流量的方式分析噗浪的偷偷說到底是讀取什麼資料來顯示的,最後就會發現,主要透過兩組 endpoints。
第一個可以得到偷偷說河道的所有噗:
GET /Stats/getAnonymousPlurks?lang=<language>
第二個則可以用來讀取一個噗的回應:
POST /Responses/get2?plurk_id=<plurk_id>&from_response=<start_num>
得知兩組 endpoints 之後就能開始撰寫了。
主程式
主程式架構如下所示,首先利用 flag
來宣告參數,主要可以控制語言、存檔資料夾,以及要間隔多久送一次請求,防止被伺服器封鎖。
再來,則是實際呼叫第一個 endpoint 取得偷偷說河道。
最後則是解析河道取得所有的噗,再根據每個噗擷取回應並存檔。
func main() {
const startUrl = "https://www.plurk.com/Stats/getAnonymousPlurks?lang=%s"
// parse args
var lang = flag.String("lang", "zh", "language of Plurks")
var outputDir = flag.String("output-dir", "output", "directory for output")
var file = flag.String("file", "", "read file instead of query URL")
var delay = flag.Int("delay", 1000, "delay between each request in milliseconds")
flag.Parse()
// get plurks
var result map[string]interface{}
if *file != "" {
result = GetPlurksFromFile(*file)
} else {
uri := fmt.Sprintf(startUrl, *lang)
result = GetPlurks(uri)
}
plurks := ProcessPlurks(result)
// start storing content...
os.MkdirAll(*outputDir, 0700)
FetchAndSavePlurks(*outputDir, plurks, *delay)
}
讀取噗浪河道
使用 HTTP GET 發送請求後,河道的 endpoint 會回傳一組 json 資料,所以我們用 json.Unmarshal
將其解譯成物件。因為 Go 是 static typing,所以不像 Python 那麼方便會根據 json 自動變成各種正確的物件。所以是用 interface{}
來暫時代表任意 type
的物件。
func ParseJson(inputs []byte) map[string]interface{} {
var result map[string]interface{}
json.Unmarshal(inputs, &result)
return result
}
// GetPlurksFromFile queris uri
// to obtain plurk timeline.
func GetPlurks(uri string) map[string]interface{} {
client := http.Client{Timeout: time.Second * 15}
resp, err := client.Get(uri)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
return ParseJson(content)
}
解析河道內容
這裡我們只拿出幾個我們關心的欄位,包含 id
、內容、張貼時間、以及有幾個回應等等。
type Plurk struct {
PlurkId int
Content string
Posted string
ResponseCount int
Responses []PlurkResponse
}
type PlurkResponse struct {
Id int
Handle string
Content string
Posted string
}
// ProcessPlurks parses the timeline,
// and produces a list of plurks.
func ProcessPlurks(result map[string]interface{}) []Plurk {
var plurks []Plurk
for _, value := range result {
// Each value is an interface{} type, that is type asserted as a string
switch obj := value.(type) {
case map[string]interface{}:
plurk := Plurk{
PlurkId: int(obj["plurk_id"].(float64)),
Content: obj["content"].(string),
Posted: obj["posted"].(string),
ResponseCount: int(obj["response_count"].(float64))}
plurks = append(plurks, plurk)
default:
}
}
return plurks
}
抓取回應
由於之前已經知道每則噗浪有幾則回應,這裡只要針對有回應的噗浪抓取就行。如果遇到錯誤,可能是該噗浪已經被刪除,所以就把該噗一併捨去。最後再存檔。
// FetchAndSavePlurks queries plurk.com to
// obtain responsese from each plurk and
// save them to disk
func FetchAndSavePlurks(outputDir string, plurks []Plurk, delay int) {
client := http.Client{Timeout: time.Second * 15}
bar := progressbar.New(len(plurks))
for _, plurk := range plurks {
var responses []PlurkResponse
var err error
if plurk.ResponseCount > 0 {
responses, err = FetchResponses(&client, plurk.PlurkId)
}
if err != nil {
log.Printf("[WARN] Responses from %d cannot be fetched\n", plurk.PlurkId)
} else {
plurk.Responses = responses
plurkJson, _ := json.Marshal(plurk)
ioutil.WriteFile(fmt.Sprintf("%s/%d.json", outputDir, plurk.PlurkId), plurkJson, 0644)
}
bar.Add(1)
if delay > 0 {
time.Sleep(time.Millisecond * time.Duration(delay))
}
}
}
至於實際的抓取程式則設計成如下。感覺用 Go 寫起來比 Python 嚴謹不少,在用 Python 時,反正錯誤就習慣讓他丟 Exception 壞掉就算了。可是寫 Go 就強迫要想想錯誤時到底要幹麻。
// FetchResponses fetches responses of a given plurk
func FetchResponses(client *http.Client, plurkId int) ([]PlurkResponse, error) {
const fetchUrl = "https://www.plurk.com/Responses/get2"
data := url.Values{}
data.Set("plurk_id", strconv.Itoa(plurkId))
data.Set("from_response", "0")
req, _ := http.NewRequest("POST", fetchUrl, strings.NewReader(data.Encode()))
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
// fetch responses
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
return []PlurkResponse{}, errors.New("connot load")
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []PlurkResponse{}, errors.New("connot load")
}
result := ParseJson(content)
var responses []PlurkResponse
for _, value := range result["responses"].([]interface{}) {
switch obj := value.(type) {
case map[string]interface{}:
response := PlurkResponse{
Id: int(obj["id"].(float64)),
Handle: obj["handle"].(string),
Content: obj["content"].(string),
Posted: obj["posted"].(string),
}
responses = append(responses, response)
default:
}
}
return responses, nil
}
如此一來就完成了。
實際抓取
實際抓的時候感覺像這樣:
go run crawl.go
15% |██████ | [13s:1m11s]
抓完則會像這樣:
.
├── crawl.go
└── output
├── 1398664726.json
├── ...
接著或許就能進行噗浪輿情分析之類等等的運用。
結語
感覺如果要快速的寫一些 scripts 還是 Python 比較好用,但 Go 寫起來已經算是很快了。本次的程式放在 shaform/experiments/plurk_crawler。