スクレイピング結果をJSONで返すAPIをGAE/Goで作る。

GAE/Go+ginで特定のサイトにアクセスし、そこから欲しい情報をJSON形式で表示する記事です。

自分自身、スクレイピング初心者ということもあり、それほど高度なことはできていませんが、とりあえず
・WebページのURLを指定してGETリクエストのレスポンスからスクレイピング
・Webページに対してPOSTリクエストをしてそのレスポンスをスクレイピング
の2つについてはできたのでそれに関する話をします。

**内容**
・GAE/Goで指定したURLに対してスクレイピング。
・RESTfulなAPIとして公開し簡単に叩いて結果を取得できるようにする

参考：
[goでスクレイピングするのにgoquery + bluemonday が最強な件](
http://qiita.com/ryurock/items/b0466ad144f5e0555a95)

基本的に上記の参考記事に則った形で行っているので、基本的なコードは変わりません。
ただ、GAE/Goで動かそうとした時にそのままでは動作せず、エラーが出ます。これをGAE/Go上で動作させるようにしようというのが今回の記事です。

#GETリクエストのレスポンスからスクレイピング
まず、与えたWebページのURLを利用してGETリクエストをし、そのレスポンスからスクレイピングする例を示します。
(ここでは指定したDOM要素をすべて取得し、その中のリンクURLを抽出して配列で返すメソッドの例となります。)

```SandScraper.go
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行目の部分。

```SandScraper.go
	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](https://cloud.google.com/appengine/docs/go/issue-requests)

なお、urlfetch.Client(context)では*http.Clientを返します。

--
話変わって、最初に話した通常のスクレイピングでは、goqueryのNewDocumentメソッドを使っているわけですが、そのメソッドの実装を見てみると以下の様になっています。

```type.go
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)`を呼んでいるわけですね。
なので、このコードの

``` type.go
res, e := http.Get(url)
	if e != nil {
		return nil, e
	}
```

の部分をurlfetch.Client()を使ってレスポンスを取得するコードに書き換えて、最終的に`NewDocumentFromResponse(res)`に食わせてあげれば動きとしては同等のことができるわけです。
よって、レスポンスを取得するコードは以下のようになるわけです

```SandScraper.go
	c := appengine.NewContext(g.Request)
	client := urlfetch.Client(c)
	resp, _ := client.Get(uri)
```


あとは、

```SandScraper.go
    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です。

```main.go

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によるルーティングを用いています。
(ルーティングについては、[以前の記事](http://qiita.com/CST_negi/items/763cb38e139b158ae9b5) などと同じようにやっています。)

ginによってこのGetURLメソッドはローカルでは
`localhost:8080/api/scr?url=http://hogefugapiyo.com`といったURLにルーティングされているのでサーバを立ち上げてこのURLを叩いてみると

```(例)results.json
[{"url":"http://aaaa","title":"piyopiyofoobar"},{"url":"http://abbb","title":"foobarhoge"}]
```
のようなJSONが表示されます。

これでWebページのURLを指定してGETリクエストのレスポンスからスクレイピングした結果を返すAPIはできました。
(RESTfulなAPIができると何が嬉しいかというと、個人的にはUnityからも叩けるという嬉しさがあります。)

#POSTリクエストをしてそのレスポンスをスクレイピング
基本的に同じです。
POSTして得たレスポンスに対してスクレイピングをかけるだけですね。

```SandScraper.go
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
}
```

変えたところは

```SandScraper.go
//resp, _ := client.Get(uri)
	resp, _ := client.PostForm(uri, url.Values{"keyword": {keyword}})
```
だけですね。
Client.POSTFormを使ってPostしてそのレスポンスを取得しています。
レスポンスが取得できたらあとはこっちのものです。


終わり。