[Go] 利用goquery + otto來分析網頁

Reading time ~3 minutes

2017的第一篇blog就先又來回的go跟爬蟲了

goquery在Go是像jquery一樣的存在, 可以讓你用jquery一樣的selector來解析html檔案內容, 像這樣:

doc.Find(".sidebar-reviews article .content-block")

只要有一些jquery的知識大概就不難上手, 但一般的網頁內容並不只有html這麼簡單而已, 有非常多的網頁是根本把資料給放在javascript, 在browser端再重組而成, 碰到這種, 光解析網頁原始檔是不夠的, 因為很多tag的內容都是之後被javascript所產生的

因此解析這類的網頁, 便可能需要去解析javascript的內容, 甚至是執行javascript, 在Go有一個好用的套件叫Otto可以來解決這麻煩

這邊以Line Today的網頁當作範例來說明, 如果你打開Line today的網頁原始檔來看, 絕大部分都是javascript, 而且資料似乎都在categoryJson內(如下)

<script>
    var categoryJson = {
		"categories":[
			{"id":100003,"name":"今日頭條","source":0,"title":null,"thumbnail":null,"url":{"type":"FILE","hash":
		...
		}
		...
</script>

一個土法煉鋼的方式是找到categoryJson前後的{},把它substring出來, 這內容當Json來分析, 但這樣其實還蠻容易出錯的

另一個方式是先用goquery取得這個script內容, 然後用Otto去執行它後把值取出來, 像:

url := "https://today.line.me/TW"
doc, err := goquery.NewDocument(url)

if err != nil {
	log.Println(err)
	return
}

s := doc.Find("script")
script := s.Nodes[1].FirstChild.Data

vm := otto.New()
vm.Run(script)

if value, err := vm.Get("categoryJson"); err == nil {
	goData, _ := value.Export()
	mapData := goData.([]map[string]interface{})
	...
}

otto很方便, 可以直接執行這個javascript, 還可以取裡面的值, 取出來的值其實是一個叫Value的struct, 如果要方便用的話, 就要用Export()來把資料轉成interface{}

interface{}對object來說還是很不好取用, 如果確定你要的值是個javascript object, 那轉成[]map[string]interface{}會稍稍方便點

不過針對很複雜也很深的物件來說, []map[string]interface{}也不是那麼的好用, 當你要存取的值要往下好多層時, 光轉型就要做到瘋掉, 多層次的map很麻煩的呀!

因此我的做法是, 多加一段javascript來減低取出的結構的複雜度, 像這樣:

vm := otto.New()
vm.Run(script)
_, err = vm.Run(`
	var news = categoryJson.categories[0].templates[0].sections[0].articles;

	var articles = [];

	for(var n in news) {
		var article = {
			'title': news[n].title,
			'link': news[n].url.url,
			'publisher': news[n].publisher
		};
		articles.push(article);
	}
`)

if err != nil {
	fmt.Println(err)
}

if value, err := vm.Get("articles"); err == nil {
	goData, _ := value.Export()

	mapData := goData.([]map[string]interface{})
	todayNews = make([]TodayNews, 0)
	for _, data := range mapData {
		news := TodayNews{}
		news.Title = data["title"].(string)
		news.Link = data["link"].(string)
		news.ImageUrl = data["imageUrl"].(string)
		todayNews = append(todayNews, news)
	}
}

這樣一來就稍微簡單多了, 不過目前碰到的缺點是, 似乎有些ES6的語法, 它不太認得呀