Rubyで乃木坂46流行語大賞を集計する

## はじめに

乃木坂46のメンバーである伊藤かりんさんが毎年年末になると乃木坂46流行語大賞というものを発表していて、その投票をかりんさんのブログのコメントにて受け付けています。

2018年の当該ポストはこちらです。

<http://blog.nogizaka46.com/karin.itou/2018/11/047843.php>

開票作業は毎年かりんさん本人が「正」の字を書いて集計してるらしく、大変そうなのでプログラミングの力を借りましょう。

投票方法の詳細は上記のブログに委ねますが、コメントの名前欄に投票したい言葉を記入するルールになったので、スクレイピングしやすそうです。

すべてのコードは[Gist](https://gist.github.com/hachy/c54a035d69fb03bc5de646a047541617)に置いておきます。

### 流れ

1. スクレイピングをしてデータを取得→ファイルに保存
2. 保存したファイルのデータを取得してそれを加工していく

というやり方でやっていきます。

## まずはスクレイピング

[ブログ][blog]にアクセスしてコメントの総数と「次へ」リンクを押したときのURLの変化を見てみましょう。

URLの一部の数字が50ずつ増えているのがわかると思います。

例えばコメントの総数が1000だったら以下のようにすると50ずつの配列が得られます。

```ruby
max = 1000
page = 0.step(max, 50).to_a
p page # => [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000]
```

### Nokogiriを使う

上記の配列を使いそれぞれのURLにアクセスして該当する文字列を取得します。

```ruby
require "nokogiri"
require "open-uri"

words = []

page.each do |p|
  url = "http://blog.nogizaka46.com/karin.itou/2018/11/047843.php?cp=#{p}#comments"
  doc = Nokogiri::HTML(URI.open(url, "User-Agent" => "firefox"))
  words << doc.css(".vcard").map { |i| i.text.gsub(/\s+/, "") }
end
```

[Nokogiri](http://www.nokogiri.org)を使うのでインストールしておきましょう。

ちなみに`User-Agent`を適当に指定しないと`open_http': 403 Forbidden (OpenURI::HTTPError))'`エラーがでてしまいました。

取得したい文字列は`vcard`classを指定すれば良さそうです。`gsub`では余分なスペースを取り除いています。

<img width="852" alt="スクリーンショット 2018-11-21 8.55.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/39782/93ec2286-a08c-ad13-301a-a3f2c18eba0c.png">

取得した文字列を`words`配列に追加しています。

### ファイルに書き出す

`words`配列を適当なファイルに書き出します。

```ruby
File.open("word.txt", "w") do |f|
  words.each { |w| f.puts(w) }
end
```

わざわざファイルに書き出さなくてもこの後の処理はできますが、デバッグなどをしながら外部サイトに何度もアクセスするのは良くないのでこのような形にしました。

### プログラムを実行

ここまでをひとつのファイルとして（ここでは`nogiblog.rb`とする）保存し、Rubyプログラムを実行してみましょう。

```shell
ruby nogiblog.rb
```

`word.txt`が作成され、データが保存できているはずです。

## 集計する

取得したデータをもとに集計していきます。

今回は投票数の多い言葉トップ20を出してみましょう。`buzzword.rb`というファイルを作成します。

### ファイルを読み込む

`words.txt`の中身を`words`配列にします。

```ruby
words = []

File.open("word.txt", "r") do |f|
  words = f.readlines
end
```

### 同じ文字列をカウント

`each_with_object`とハッシュを使うことでという形で配列内の要素数を得ることができます。

同じ文字列があればカウントが増えていきます。

`chomp!`は改行文字を取り除いています。

```ruby
word_count = words.each_with_object(Hash.new(0)) do |word, hash|
  hash[word.chomp!] += 1
end
p word_count # => {"単語１"=>1, "単語２"=>1, "単語３"=>1 ... }`
```

### トップ20

`word_count`ハッシュをハッシュの値であるカウントした数字でソートします。

`-`をつけることで逆順になります。さらに`take`を使って20個の要素を先頭から取り出します。

```ruby
top = word_count.sort_by { |_, count| -count }.take(20)
```

### 似たような文字列を集める

同じ事柄を表現した場合でも投票する人によって書き方が違うことがあります。

例えば`シンクロニシティ`,`シンクロ`,`シンクロライブ`といったようにバラけてしまうので、それらを同じとしてカウントするかどうかは置いといて、似たような文字列は隣同士に置いてわかりやすくしたいと思います。

今回はトップ20の文字列の頭文字三文字を使って、もし`word_count`ハッシュのキーがその三文字で始まっていたらその文字列を表示する、というやり方でやってみます。

正直もっと他に良い方法がありそうなので誰か教えて下さい🤔

```ruby
initial = top.map { |t| t[0].slice(0..2) }.uniq

initial.each do |i|
  word_count.each do |word, count|
    if word.start_with?(i)
      puts [count.to_s.rjust(4), word].join(" | ")
    end
  end
end
```

`initial`では頭文字三文字を取得しています。`uniq`は重複を避けるためです。

`rjust`や`join`は出力を整形して見やすくするために使っています。

### プログラムを実行

以上を`buzzword.rb`に書いてプログラムを実行してみましょう。

この記事を書いている時点（2018年11月21日）で4000以上の投票がされていて結果は以下のようになっています。

結果を知りたくなければ、これより下を見ないことをおすすめします😏

```shell
ruby buzzword.rb
```

```shell
 620 | ヤラカシタヤラカシタ
  39 | ヤラカシタ
  95 | ヤラカシタ、ヤラカシタ
  16 | ヤラカシタ　ヤラカシタ
   1 | ヤラカシタヤラカシタ(棒読み)
   2 | ヤラカシタやらかした
   2 | ヤラカシタ､ヤラカシタ
   1 | ヤラカシタ、ヤラカシタ。
 301 | 卒業
  38 | 卒業ラッシュ
   1 | 卒業（ラッシュ）
   1 | 卒業メンバーロス
   1 | 卒業です
   1 | 卒業。
   1 | 卒業発表
 183 | 乃木撮
   1 | 乃木撮か、でんちゃん！の棒読みかな？
   1 | 乃木撮！！！
   1 | 乃木撮り
...
```

これで乃木坂46流行語大賞を集計することができました。

### 付け足し

今回はトップ20をだしてみましたが、`take`のところの数字を増やしたり、あるいは`take`自体を削除して実行してみると面白いかもしれません。

もし似たような文字列も同一のものとみなすなら順位の変動もあり得ます。

[blog]: http://blog.nogizaka46.com/karin.itou/2018/11/047843.php