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

この記事は最終更新日から3年以上が経過しています。

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を抽出して配列で返すメソッドの例となります。)

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

なお、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によるルーティングを用いています。
(ルーティングについては、以前の記事 などと同じようにやっています。)

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してそのレスポンスを取得しています。
レスポンスが取得できたらあとはこっちのものです。

終わり。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした