RubyのHpricotでニコニコ動画をスクレイピングしてみる(2)

| | コメント(3)

昨日の続きです。
もう一回挟もうとしたんだけど
面倒臭くなったのでので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で集計すればなんか面白いグラフが見れるんじゃないかな、と期待しています。

コメント(3)

eclipse :

て今日実行してみたらもう動かなくなってるし・・・
原因は投稿日時と動画の長さの位置が逆になったからでした。

wiz@_( (_´Д`)_ :

ダウンロードリンクを取得してirvineとかのダウンロードソフツと連携とかはやらんのかい?

eclipse :

今のところは考えてません。
ダウンロード自動でばかすかやるとなんかペナルティくらいそうで怖いし。

このブログ記事について

このページは、yuchが2007年9月 2日 20:36に書いたブログ記事です。

ひとつ前のブログ記事は「RubyのHpricotでニコニコ動画をスクレイピングしてみる(1)」です。

次のブログ記事は「転職して1年経つわけだが」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 4.01