Yasuriでお手軽スクレイピング


こんにちは．[先日リリースしたYasuri](http://qiita.com/tac0x2a/items/69ac8a33b0a8a3d46edd)ですが、思ったより反響があって驚いています．ありがとうございます．

せっかくなので、もう少しまともな導入とサンプルを作ってみました．

# Yasuriのインストール

## gemでインストールする場合
```shell
$ gem install yasuri
```
これだけ．

## bundlerを使う場合
Gemfileに以下の一行を追加して、

```ruby:Gemfile
gem 'yasuri'
```

bundlerコマンドでインストールする．

```shell
$ bundle install
```

これでインストールできます．

# サンプル: QiitaでRubyに関する新着投稿を3ページだけ取得する

## コード
```ruby
require 'yasuri'

# "Rubyに関する新着投稿 - Qiita" を起点にする
agent = Mechanize.new
page = agent.get('http://qiita.com/tags/Ruby/items')

# 最新3ページをスクレイピングする
init_page = Yasuri.pages_init '//*[@id="main"]/div/div/div[1]/section/div[2]/ul/li[7]/a', limit:3 do

  # 現在のページ数
  text_page_idx '//*[@id="main"]/div/div/div[1]/section/div[2]/ul/li[@class="active"]/a', proc: :to_i

  # 各エントリをスクレイピング
  struct_entries '//*[@id="main"]/div/div/div[1]/section/div[1]/article' do
    text_author './div[2]/div[1]/a'
    text_date   './div[2]/div[1]/text()', truncate:/posted on (.+)/
    text_title  './div[2]/div[2]/h1/a'
    text_stock_count   './div[3]/ul/li[1]',   proc: :to_i
    text_comment_count './div[3]/ul/li[2]/a', proc: :to_i
  end
end

require 'json'
jj init_page.inject(agent, page)
```

## 実行結果
```json:実行結果
[
  {
    "page_idx": 1,
    "entries": [
      {
        "author": "akicho8",
        "date": "May 07, 2015",
        "title": "to_prepareブロックの中はスコープが異なっているので注意",
        "stock_count": 1,        
        "comment_count": 0
      },
      {
        "author": "Robinia",
        "date": "May 07, 2015",
        "title": "centOS6.5にrails4.2の環境構築",
        "stock_count": 2,
        "comment_count": 0
      },
            
    ...中略...
    
    ]
  },
  {
    "page_idx": 2,
    "entries": [
      {
        "author": "qtwi",
        "date": "May 04, 2015",
        "title": "Windows でログ収集データ化ツール logstash を動かす",
        "stock_count": 3,
        "comment_count": 0
      },
      {
        "author": "tatsu_nishiki",
        "date": "May 04, 2015",
        "title": "Jenkinsでrubyのビルドでハマった点",
        "stock_count": 2,
        "comment_count": 0
      },
      
    ...以下略...
```



## 解説

### 複数ページの取得
```ruby
# 最新3ページをスクレイピングする
init_page = Yasuri.pages_init '//*[@id="main"]/div/div/div[1]/section/div[2]/ul/li[7]/a', limit:3 do
```
ページネーション[(こんなの)](https://www.google.co.jp/search?q=%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3&espv=2&biw=2304&bih=1229&tbm=isch&tbo=u&source=univ&sa=X&ei=qWBLVfD1J-K_mwWQ2YHYDw&ved=0CCEQsAQ)でナビゲートされるページを順に取得するための記述です．いわゆる、"次のページヘ移動" リンクをXPathで指定しています．
このようなページ中の要素を指すXPathは、Chromeならば "要素の検証" -> "Copy Xpath" を使うと楽に取得できます．

`limit:3` は最大3ページまで取得せよ、という指示です．省略した場合は末尾まで全ページ取得します．
また、最も外側のブロックを開始する場合、このように`Yasuri.*`とする必要があります．

その後、取得した各ページに対するスクレイピング内容をブロック内に宣言していきます．



--
### ページ内のテキストをスクレイピング
```ruby
  # 現在のページ数
  text_page_idx '//*[@id="main"]/div/div/div[1]/section/div[2]/ul/li[@class="active"]/a', proc: :to_i
```
現在が何ページ目かを取得しています．
ありがたいことに、Qiitaでは現在のページ番号だけスタイルが変わるようになっているので、XPathで `@class="active"`を指定することで取得しています．
`text_*` では、`proc: :to_i` のように、削りだした文字列(String)に対してメソッドを呼んで簡単な加工をすることができます．
ここでは、`to_i` メソッドを呼んで、文字列を整数へ変換しています．

--
### エントリを部分木として取り出してスクレイピング
```ruby
  # 各エントリをスクレイピング
  struct_entries '//*[@id="main"]/div/div/div[1]/section/div[1]/article' do
```

さて、ここからいよいよ投稿をスクレイピングします．
`struct_*` を使用することで、XPathに一致する、HTMLソース中の部分木を取り出して子要素に解析させることができます．
XPathに複数の部分木がマッチする場合、この例のように配列として結果を取得することができます．

--
### エントリ内の各要素をスクレイピング
```ruby
    text_author './div[2]/div[1]/a'
    text_date   './div[2]/div[1]/text()', truncate:/posted on (.+)/
    text_title  './div[2]/div[2]/h1/a'
    text_stock_count   './div[3]/ul/li[1]',   proc: :to_i
    text_comment_count './div[3]/ul/li[2]/a', proc: :to_i
```

部分木として抽出した各エントリに対してスクレイピングを行います．
部分木中の要素を指すため、XPathとして相対パスを指定しています．

`truncate:/posted on (.+)/` では、削りだした文字列から不要な部分を正規表現で削除しています．
`()`でグループを指定した場合、最初のグループがスクレイピング結果になります．グループを省略した場合、マッチした部分全体が結果になります．


## さいごに

ソースはGithubで公開しています．
[tac0x2a/yasuri](https://github.com/tac0x2a/yasuri "tac0x2a/yasuri")


### これバグじゃね？
[Issues](https://github.com/tac0x2a/yasuri/issues)に投げてください．直すかもしれません．

### この機能欲しいんだけど！
[Issues](https://github.com/tac0x2a/yasuri/issues)に投げてください．実装するかもしれません．

### 修正しといた/実装しといた
pull request 投げてくださいお願いします．