GAE/Go+ginで特定のサイトにアクセスし、そこから欲しい情報をJSON形式で表示する記事です。
自分自身、スクレイピング初心者ということもあり、それほど高度なことはできていませんが、とりあえず
・WebページのURLを指定してGETリクエストのレスポンスからスクレイピング
・Webページに対してPOSTリクエストをしてそのレスポンスをスクレイピング
の2つについてはできたのでそれに関する話をします。
内容
・GAE/Goで指定したURLに対してスクレイピング。
・RESTfulなAPIとして公開し簡単に叩いて結果を取得できるようにする
参考:
goでスクレイピングするのにgoquery + bluemonday が最強な件
基本的に上記の参考記事に則った形で行っているので、基本的なコードは変わりません。
ただ、GAE/Goで動かそうとした時にそのままでは動作せず、エラーが出ます。これをGAE/Go上で動作させるようにしようというのが今回の記事です。
GETリクエストのレスポンスからスクレイピング
まず、与えたWebページのURLを利用してGETリクエストをし、そのレスポンスからスクレイピングする例を示します。
(ここでは指定したDOM要素をすべて取得し、その中のリンクURLを抽出して配列で返すメソッドの例となります。)
type (
SearchResult struct {
URL string `json:"url" binding:"required"`
Title string `json:"title" binding:"required"`
}
)
func GetScrapingResult(g *gin.Context, uri string) []SearchResult {
c := appengine.NewContext(g.Request)
client := urlfetch.Client(c)
resp, _ := client.Get(uri)
doc, err := goquery.NewDocumentFromResponse(resp)
var results = []SearchResult{}
//doc, err := goquery.NewDocument(uri)
if err != nil {
fmt.Print("url scarapping failed")
}
doc.Find("table > tbody > tr > td.content > span > a").Each(func(_ int, s *goquery.Selection) {
url, _ := s.Attr("href")
title, _ := s.Attr("title")
dat := SearchResult{URL: url, Title: title}
results = append(results, dat)
})
return results
}
引数にURLを指定してそのページにGETReqを送りレスポンスをスクレイピングしています。
コメントアウトされている部分がGAE/Goを使わない場合にスクレイピングが行えるコードです。
それはさておき大事なのはGetScrapingResultメソッドの1~4行目の部分。
c := appengine.NewContext(g.Request)
client := urlfetch.Client(c)
resp, _ := client.Get(uri)
doc, err := goquery.NewDocumentFromResponse(resp)
GAE/Goではコメントアウトと同等のことを行うために、このように書き換えています。
理由は、AppEngineではhttp(s)リクエストではurlfetch.Clientを使うように指定されているからです。
This page describes how to issue HTTP(S) requests from your App Engine app.
App Engine uses the URL Fetch service to issue outbound HTTP(S) requests.
To issue an outbound HTTP request, use the http package as usual, but create your client using urlfetch.Client.
https://cloud.google.com/appengine/docs/go/issue-requests
なお、urlfetch.Client(context)では*http.Clientを返します。
--
話変わって、最初に話した通常のスクレイピングでは、goqueryのNewDocumentメソッドを使っているわけですが、そのメソッドの実装を見てみると以下の様になっています。
func NewDocument(url string) (*Document, error) {
// Load the URL
res, e := http.Get(url)
if e != nil {
return nil, e
}
return NewDocumentFromResponse(res)
}
これを見ると、
・urlからhttp.Get()メソッドによって、*Responseを取得
・それをNewDocumentFromResponse()メソッドに食わせる。
ということをしています。
一方で最初に書いたGAE/Goでの私のコードでは、
・urlfetch.Client(context)で*http.Clientを取得
・http.Client.Get()メソッドによって*Responseを取得
・それをNewDocumentFromResponse()メソッドに食わせる。
ということをしています。
つまり、通常のコードと私のGAEのコードのどちらもレスポンスを取得して、NewDocumentFromResponse(res)
を呼んでいるわけですね。
なので、このコードの
res, e := http.Get(url)
if e != nil {
return nil, e
}
の部分をurlfetch.Client()を使ってレスポンスを取得するコードに書き換えて、最終的にNewDocumentFromResponse(res)
に食わせてあげれば動きとしては同等のことができるわけです。
よって、レスポンスを取得するコードは以下のようになるわけです
c := appengine.NewContext(g.Request)
client := urlfetch.Client(c)
resp, _ := client.Get(uri)
あとは、
doc.Find("table > tbody > tr > td.content > span > a").Each(func(_ int, s *goquery.Selection) {
url, _ := s.Attr("href")
title, _ := s.Attr("title")
dat := SearchResult{URL: url, Title: title}
results = append(results, dat)
})
のように、レスポンスの中でこのDOM要素に該当する部分をすべて取得し、それぞれに対してEachメソッドでお好きな動作をさせています。
(ここでは取得した複数のaタグのリンク先とリンク文を取得しています。)
あとはGinでルーティングされたメソッドからこのコードを呼び出し、g.JSONでJSON化すればOKです。
func GetURL(g *gin.Context) {
url := g.DefaultQuery("url", "default-URL")//default-URLはクエリが取得できなかった時にデフォルトで代入される値
results := ScrapingAPI.GetScrapingResult(g, url)
g.JSON(http.StatusOK, results)
}
さて結果を返す処理は完成したので、それをRESTfulなAPIとして公開するにはginによるルーティングを用いています。
(ルーティングについては、以前の記事 などと同じようにやっています。)
ginによってこのGetURLメソッドはローカルでは
localhost:8080/api/scr?url=http://hogefugapiyo.com
といったURLにルーティングされているのでサーバを立ち上げてこのURLを叩いてみると
[{"url":"http://aaaa","title":"piyopiyofoobar"},{"url":"http://abbb","title":"foobarhoge"}]
のようなJSONが表示されます。
これでWebページのURLを指定してGETリクエストのレスポンスからスクレイピングした結果を返すAPIはできました。
(RESTfulなAPIができると何が嬉しいかというと、個人的にはUnityからも叩けるという嬉しさがあります。)
POSTリクエストをしてそのレスポンスをスクレイピング
基本的に同じです。
POSTして得たレスポンスに対してスクレイピングをかけるだけですね。
type (
SearchResult struct {
URL string `json:"url" binding:"required"`
Title string `json:"title" binding:"required"`
}
)
func GetScrapingResult(g *gin.Context, uri string,keyword string) []SearchResult {
c := appengine.NewContext(g.Request)
client := urlfetch.Client(c)
//resp, _ := client.Get(uri)
resp, _ := client.PostForm(uri, url.Values{"keyword": {keyword}})
doc, err := goquery.NewDocumentFromResponse(resp)
var results = []SearchResult{}
if err != nil {
fmt.Print("url scarapping failed")
}
doc.Find("table > tbody > tr > td.content > span > a").Each(func(_ int, s *goquery.Selection) {
url, _ := s.Attr("href")
title, _ := s.Attr("title")
dat := SearchResult{URL: url, Title: title}
results = append(results, dat)
})
return results
}
変えたところは
//resp, _ := client.Get(uri)
resp, _ := client.PostForm(uri, url.Values{"keyword": {keyword}})
だけですね。
Client.POSTFormを使ってPostしてそのレスポンスを取得しています。
レスポンスが取得できたらあとはこっちのものです。
終わり。