RubyのHpricotでニコニコ動画をスクレイピングしてみる(2)
昨日の続きです。
もう一回挟もうとしたんだけど
面倒臭くなったのでのでCSV出力までやることにしました。
やりたいことは、
「ニコニコ動画のランキング一覧をファイルに書き出す」
だけです。
まずは書き出す情報を定義します。
module Nico
class Douga
attr_accessor(
:title, # タイトル
:url, # URL
:posted, # 投稿日
:length, # 動画の長さ(秒)
:mylist, # マイリスト登録数
:view, # 再生数
:res # コメント数
)
def self.head_csv
"URL,タイトル,投稿日,動画の長さ,マイリスト登録数,再生数,コメント数"
end
def to_csv
cell = Array.new(7)
cell[0] = @url
cell[1] = "\"#{@title}\""
cell[2] = @posted.strftime("%Y/%m/%d %H:%M:%S")
cell[3] = @length
cell[4] = @mylist
cell[5] = @view
cell[6] = @res
cell.join(",")
end
end # class
HTMLから取得したデータの動画情報1件分がDougaクラスのインスタンスに対応します。
データの設定はアクセッサ経由で外部でやります。
to_csvでCSVデータ1行分の文字列を返します。
続いてランキング一覧をクラス化します。
今回利用するランキングは「今までの合計」に限定しました。
取得するデータのマイリスト数、再生数、コメント数は投稿時点からの累積になります。
ランキングの種類としてはマイリスト順、再生順、コメント順の3パターンがありますが、
コンストラクタ(initialize)に種類(kind)を引数として渡すことで対応します。
マイリスト順ランキング:http://www.nicovideo.jp/ranking/mylist/total/all?page=1
再生順ランキング:http://www.nicovideo.jp/ranking/view/total/all?page=1
コメント順ランキング:http://www.nicovideo.jp/ranking/res/total/all?page=1
URLを見比べると、/raking/○○/total/allとなっているので、
○○の部分がmylistならマイリスト、viewなら再生、resならコメントだとわかります。
ランキングは1ページに100件まで載っています。
そしてページが10まであります。なので1000位まで取れるということになります。
Rankingクラスには一覧データ取得のreadメソッドと、
取得したデータをファイル出力するsaveメソッドを定義します。
class Ranking
def initialize (kind)
@kind = kind
@dougas = Array.new(1000) {Douga.new}
end
def read (sid)
Net::HTTP.start("www.nicovideo.jp", 80) do |http|
(1..10).each do |page|
warn "read #{@kind}:#{page}..."
response = http.get(
"/ranking/#{@kind}/total/all?page=#{page}",
"Cookie" => "user_session=user_session#{sid}")
doc = Hpricot.parse(response.body.tosjis)
i = (page - 1) * 100
doc.search("h3/a").each do |e|
@dougas[i].title = e.inner_text
@dougas[i].url = e[:href]
i += 1;
end
i = (page - 1) * 100
doc.search('p.TXT12[@style="color:#666;"]').each do |e|
@dougas[i].mylist = /([0-9,]+)/.match(e.inner_text)[1].delete(",").to_i
i += 1;
end
i = (page - 1) * 100
n = 0;
doc.search('td/p.TXT12/strong').each do |e|
case n % 4
when 0: @dougas[i].posted = DateTime.strptime(e.inner_text, "%Y年%m月%d日 %H:%M:%S")
when 1: @dougas[i].length = $1.to_i * 60 + $2.to_i if /(\d+)分(\d+)秒/ =~ e.inner_text
when 2: @dougas[i].view = e.inner_text.delete(",").to_i
when 3: @dougas[i].res = e.inner_text.delete(",").to_i
i += 1;
end
n += 1
end
end # each
end # start
end # def
def save (fname)
File.open(fname, "w") do |file|
file.puts Douga.head_csv
@dougas.each {|d| file.puts d.to_csv }
end # open
end # def
end # class
readメソッドが実際にHpricotを使ってスクレイピングしている部分になります。
Hpricotで使われる主なメソッドはsearchとatです。
searchはマッチする要素を全て取得します。
atはマッチする最初の要素のみ取得します。
まず1回目のsearchでh3タグの中のaタグを全てピックアップし、
タグ中の文字列を動画のタイトルとし、href属性をURLと判断します。
Hpricotの構文ではタグの階層を下っていくのに「/」が使われます。
i = (page - 1) * 100
doc.search("h3/a").each do |e|
@dougas[i].title = e.inner_text
@dougas[i].url = e[:href]
i += 1;
end
2回目のsearchでマイリスト数を取得します。
ちょっとややこしいですが、「p.TXT12[@style="color:#666;"]」の部分です。
「.」はCSS同様class属性を限定します。
また、[@hoge="..."]でhoge属性の値が...なものに限定します。
よって「p.TXT12[@style="color:#666;"]」は<p class="TXT12" style="color:#666;">...</p>にマッチします。
そしてマッチした文字列から数値部分のみ切り出してカンマを削除して整数変換して保持します。
i = (page - 1) * 100
doc.search('p.TXT12[@style="color:#666;"]').each do |e|
@dougas[i].mylist = /([0-9,]+)/.match(e.inner_text)[1].delete(",").to_i
i += 1;
end
3回目のsearchで残りの、投稿日時、動画の長さ、再生数、コメント数をまとめて取得します。
この4つはHTMLで同じレベルでタグ記述されているため、全部持ってきて、何個目かによって判断させています。
こういう時はおなじみのパターンですが、ループの中でインデックスの余りをとって処理を振り分けます。
1個目だったら投稿日時、2個目だったら動画の長さ、3個目だったら再生数、4個目だったらコメント数です。
でそれぞれ都合の良いように加工してから保持します。
Hpricotのタグ表現は、「td/p.TXT12/strong」となっているので、
tdタグの中の、pタグでclassがTXT12の中の、strongタグにマッチします。
i = (page - 1) * 100
n = 0;
doc.search('td/p.TXT12/strong').each do |e|
case n % 4
when 0: @dougas[i].posted = DateTime.strptime(e.inner_text, "%Y年%m月%d日 %H:%M:%S")
when 1: @dougas[i].length = $1.to_i * 60 + $2.to_i if /(\d+)分(\d+)秒/ =~ e.inner_text
when 2: @dougas[i].view = e.inner_text.delete(",").to_i
when 3: @dougas[i].res = e.inner_text.delete(",").to_i
i += 1;
end
n += 1
end
Nicoモジュールの最後に、セッションID取得処理を関数化します。
メールアドレスとパスワードを引数にとってセッションIDを返します。
中身は昨日のものと同じです。
def self.get_sid (mail, password)
sid = nil
https = Net::HTTP.new("secure.nicovideo.jp", 443)
https.use_ssl = true
https.start do |w|
data = "next_url=&mail=#{mail}&password=#{password}"
response = w.post("/secure/login?site=niconico", data, "Content-Length" => "#{data.length}")
sid = $1 if response["Set-Cookie"] =~ /user_session=user_session([0-9_]+)/
end
sid
end
end # module
ここまでのコードがNicoモジュールで、nico.rbという1ファイルになります。
次がこのモジュールを利用する側のコードです。
#! ruby -Ks
require 'net/https'
require 'date'
require 'kconv'
require 'rubygems'
require 'hpricot'
require 'nico'
today = Date.today.strftime("%Y%m%d")
sid = Nico::get_sid(メールアドレス, パスワード)
["mylist", "res", "view"].each do |kind|
r = Nico::Ranking.new(kind)
r.read sid
r.save "#{kind}_#{today}.csv"
end
これでマイリスト登録数・再生数・コメント数の3つの一覧CSVファイルが出力されます。
タスクスケジューラやcronで毎日回して、1ヶ月くらいとってから
Excelで集計すればなんか面白いグラフが見れるんじゃないかな、と期待しています。

て今日実行してみたらもう動かなくなってるし・・・
原因は投稿日時と動画の長さの位置が逆になったからでした。
ダウンロードリンクを取得してirvineとかのダウンロードソフツと連携とかはやらんのかい?
今のところは考えてません。
ダウンロード自動でばかすかやるとなんかペナルティくらいそうで怖いし。