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


はじめに

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

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

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

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

すべてのコードはGistに置いておきます。

流れ

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

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

まずはスクレイピング

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

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

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

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にアクセスして該当する文字列を取得します。

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を使うのでインストールしておきましょう。

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

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

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

ファイルに書き出す

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

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

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

プログラムを実行

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

ruby nogiblog.rb

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

集計する

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

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

ファイルを読み込む

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

words = []

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

同じ文字列をカウント

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

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

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

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

トップ20

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

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

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

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

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

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

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

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

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は重複を避けるためです。

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

プログラムを実行

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

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

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

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

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

付け足し

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

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