1人の情弱が『Goでクローリングするならgocrawl』にイイネしています！


# 本編

お疲れ様です！
https://github.com/PuerkitoBio/gocrawl
goでクローリングするなら↑が簡単でオススメです!!

# 雑談

扇情に弱い情弱である私は果敢にもGo言語習得に挑んでおります。
まあー正直ベース、扇情ベースで初めてしまったので`fmt.Println("hoge")`で大満足。
それからしばらく音沙汰なし夫になるわけです。

最近、Twitterでソーシャルパトロールをしておりましたら、こんなトゥイート(笑)を発見。

<blockquote class="twitter-tweet" lang="ja"><p>こういう、ほぼ全てに星１つをつけてるAmazonレビュアーって、全世界に向かって自分がいかに駄本を見抜く眼力がないか、かつ暇なのかを発信しとるわけですねｗ＝。<a href="http://t.co/q0IZmcRpG3">http://t.co/q0IZmcRpG3</a>:天才バカ本さんのプロフィール <a href="http://t.co/IMwwtdEXSV">http://t.co/IMwwtdEXSV</a></p>&mdash; 田端　信太郎 (@tabbata) <a href="https://twitter.com/tabbata/status/557480109066579969">2015, 1月 20</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

あー、こういう人いるいる。
他にも結構いるのかなー、と思ってトップレビュアー一覧ページを見たら1万人もいる。
これは人力で見てたら大変だ、ということで自動的に面白レビュアーを見つけたい。
怠惰をこよなく愛するハッカー(爆)である小生はそう思うわけです。

APIあるのかと思って調べたのですがドキュメントがまったく自分のアタマに入ってこなくて何も考えられそうにありません。

とりいそぎクローラー作って

* Amazonのリンクを手当たり次第に徘徊
* レビュアーのページに到達したら星１つの数をカウント
* 星１の数が５個以上だったら面白レビュアー認定

って、流れで動かしてみようと。

> 『星１の数が５個以上だったら面白レビュアー認定』

と、記載されてますが、これは全レビュー数が1003兆件で、うち星1がたった5件でも『面白レビュアー』という認識でしょうか？

はい、その認識で問題ないと思います。

* 真面目に計算するのが思ったより面倒そう
* 最初の数件で星１が多ければ、その先のレビューもそんな感じだろうという勘

レビュアーのページを実際見てもらうと、レビューの読み込みははじめの１０数件はhtmlで返ってきます。
で、追加分はAjaxを利用して動的に取得してるようです。
なので『星1レビュー / そのユーザのレビュー数』を算出するのなかなか面倒そう、と思い簡易的な判定方法を採用するのです。

さて、何で実装してみようか。
最初はnodeとか使ってやろうと思いました。

そういえば…Goは並列処理が簡単にできる！
…的なことを何かのhtmlで読んだことを認識しました。
たくさんエージェント作って並列でAmazon中を徘徊させたら処理が早く終わりそうですね。
なので久しぶりにGoと向き合うことにしました。

`http`パッケージを使ってクライアント作って、htmlをパースして…って想像するのですが
案の定、やること多すぎて挫折するわけです。

『go crawl』でググったら冒頭のライブラリと出会いました。
あ、これだわ。

https://github.com/PuerkitoBio/gocrawl
ここのドキュメント読めば終わりなんですが、整理がてら。

ドキュメントにもこのライブラリのウリが明記されてます。
個人的に感じたgocrawlの良いところ

* ライブラリでgoroutinesを駆使したworkerのおかげで特に意識せずに並列処理ができる
* ライブラリにはgoqueryが含まれてるので、取得したhtmlを簡単に操作できる
* クロール対象hostのrobotos.txtを自動取得、これに則ったクローリングをしてくれるのでわりと安心
* クローラーの動作に沿ったフックが用意されてるので拡張が容易（な気がする）

## 導入

`go get github.com/PuerkitoBio/gocrawl`

## 作り方 

ドキュメントに書いてるサンプルをコピペしても動きません。
https://github.com/PuerkitoBio/gocrawl/blob/master/cmd/example/main.go
↑を写経しました。

基本的なgocrawlの基本的な使い方は

* クローラーの動作を定義するExtenderってヤツを作る
* Extenderからクローラーのオプションを作って
* クローラーを生成
* クローラーを実行

って感じです。

Extender,Worker,Crawlerのコードを真面目に読めばどんなふうに動作してるか分かるかと思います。
ちなみに当方は読んでません。

Extenderのメソッドを拡張することでcrawlerをカスタマイズしていきます。
Extender : https://github.com/PuerkitoBio/gocrawl/blob/master/ext.go

WorkerがこのExtenderで定義された処理を呼び出しています。
Worker : https://github.com/PuerkitoBio/gocrawl/blob/master/worker.go

で、このWorkerはCrawlerの内部で生成されています。
Crawler : https://github.com/PuerkitoBio/gocrawl/blob/master/crawler.go

で、このCrawlerを我々が生成して`run`するわけです。

というわけで、上に書いた面白レビュアーを発見するクローラーを書いてみました。
下記の通り。

```go:awesome_reviewer.go
package main

import (
  "fmt"
  "net/http"
  "time"
  "regexp"

  "github.com/PuerkitoBio/gocrawl"
  "github.com/PuerkitoBio/goquery"
  )

  type Ext struct {
    *gocrawl.DefaultExtender
  }

  var rxOk = regexp.MustCompile(`http://amazon\.co\.jp\/(review\/top-reviewers.*page.*|gp\/pdp.*pic).*`)
  var rxTopReviewer = regexp.MustCompile(`http://amazon\.co\.jp\/gp\/pdp.*pic.*`)

  func (e *Ext) Visit(ctx *gocrawl.URLContext, res *http.Response, doc *goquery.Document) (interface{}, bool) {
    if rxTopReviewer.MatchString(ctx.NormalizedURL().String()) {
      isSucker, numSucks := isSucker(doc)
      if isSucker {
        fmt.Printf("%s,%d\n", ctx.URL(), numSucks)
      }
    }
    return nil, true
  }

  func isSucker(doc *goquery.Document) (bool, int) {
    num := len(doc.Find(".a-star-medium-1").Nodes)
    return  num > 4, num
  }

  func (e *Ext) Filter(ctx *gocrawl.URLContext, isVisited bool) bool {
    return !isVisited && rxOk.MatchString(ctx.NormalizedURL().String())
  }

  func main() {
    ext := &Ext{&gocrawl.DefaultExtender{}}
    // Set custom options
    opts := gocrawl.NewOptions(ext)
    opts.CrawlDelay = 5 * time.Second
    opts.LogFlags = gocrawl.LogError
    opts.MaxVisits = 20000

    c := gocrawl.NewCrawlerWithOptions(opts)
    c.Run("http://www.amazon.co.jp/review/top-reviewers/ref=cm_cr_tr_link_2?ie=UTF8&page=1")
  }
```

`Filter`と`Visit`がExtenderのメソッドです。
たぶんこの２つだけで十分イイカンジなクローリングできそう。

`Filter`内でtrueを返したurlのみが`Visit`に引き渡されます。

```
  func (e *Ext) Filter(ctx *gocrawl.URLContext, isVisited bool) bool {
    return !isVisited && rxOk.MatchString(ctx.NormalizedURL().String())
  }
```

```
var rxOk = regexp.MustCompile(`http://amazon\.co\.jp\/(review\/top-reviewers.*page.*|gp\/pdp.*pic).*`)
```

`isVisited`は既に訪問したかどうかの真偽値です。
ここでは『未訪問でかつ訪問候補のURLが正規表現rxOkとマッチするか』どうかチェックしてます。

rxOkでは『レビュアーの一覧画面URL』と『レビュアーの詳細ページURL』に一致する（と信じてる）ような正規表現にしてあります。

無事trueが返ってくると`Visit`に処理が流れます。

`Visit`の引数としてイイカンジにパースされた`goquery.Document`が渡されてきます。

```
  func (e *Ext) Visit(ctx *gocrawl.URLContext, res *http.Response, doc *goquery.Document) (interface{}, bool) {
    if rxTopReviewer.MatchString(ctx.NormalizedURL().String()) {
      isSucker, numSucks := isSucker(doc)
      if isSucker {
        fmt.Printf("%s,%d\n", ctx.URL(), numSucks)
      }
    }
    return nil, true
  }
```

```
  func isSucker(doc *goquery.Document) (bool, int) {
    num := len(doc.Find(".a-star-medium-1").Nodes)
    return  num > 4, num
  }
```

冗長ですがrxTopReviewerは『レビュアーの詳細ページURL』の正規表現です。
ここでマッチしたら`isSucker`でDocumentの中に存在する星１の用のCSSクラスを取得し数え上げます。
5件以上であればtrueと件数を返してます。

で、それを出力する。と。

この部分でDBにInsertしたり、別のゴルーチン作ったり、いろいろ出来そうですねー(=^・^=)

## 結果

数百件ほどの面白レビュアーが抽出されました。
軽く眺めてみると、ひたすらアイドルをdisってる人や、食品被害のでた海外製品に対して星１つをつけて警告してる人、偽物の製品をひたすら晒しあげて星１つをつけてる人、などなど…

本をひたすらdisってるレビュアーたちが揃ってビッグダディーの本を酷評している謎のシンクロを目の当たりにし、今日もいい天気だなあ(^J^)と天を仰ぎました。

http://www.amazon.co.jp/dp/4391143534/ref=pdp_new_dp_review < これ

興味のある方は是非上のスクリプトを実行してみるか、sickなスクリプトを組み上げてみてください(^L^)

以上、何卒宜しくお願い致します。

# この記事を見た後に読んでいるのは？

https://github.com/PuerkitoBio/gocrawl
http://www.amazon.co.jp/dp/4391143534/ref=pdp_new_dp_review

# フォロー外から失礼します。勉強になりました、シェアさせてください！