RubyはCocProxyを試そうとして入れてみたけどよく解らなかったんですよね
そのまま封印していたのだけど、どんなジレンマさんで
http://d.hatena.ne.jp/hrkt0115311/20090717/1247806593
http://d.hatena.ne.jp/hrkt0115311/20090718/1247871223
「hrkt0115311の、迷えるプログラミング教室」Vol.90 〜URL指定すると、はてな記法を返すスクリプトの巻〜
という手ごろそうなお題があったので、挑戦してみました。
- プログラム経験はある程度ある
- でもRubyは初挑戦
という人がやってみるとこうなる、という例、かな(笑)。
ソースコード(cr_urllist.rb)
#!/usr/bin/ruby -Ks require 'rubygems' require 'hpricot' require 'open-uri' require 'kconv' require 'cgi' # 文字コード設定(変更時は先頭行の -K* も変更のこと) $KCODE = "sjis" FR_CHARSET = Kconv::UTF8 TO_CHARSET = Kconv::SJIS # タイトル自動取得 AUTO_MODE = true # true: タイトルを自動取得してHTML出力 false: はてな記法で出力 LIST_FORMAT = '<li><a href="%s">%s</a><a href="http://b.hatena.ne.jp/entry/%s"><img src="http://b.hatena.ne.jp/entry/image/%s" /></a></li>' # デバッグ用 DEBUG = false # true でデバッグモード # ページ取得時にベースとなるURL BASEPAGE = "http://hrkt0115311.tumblr.com/page/" # 正規表現 REG_TUMBLER_URL = %r{^http://.*?\.tumblr\.com\/post/.+$} # 目的となるURLの抽出用 REG_CHOP_URL_PREFIX = %r{^http://} # ブックマークURL作成用 # ブロック内で配列を定義するとスコープの問題が起きるので、外で定義。 target_urls = [] thread_infos = [] # コマンドライン引数から page_start = 1 # 開始ページ(デフォルト) page_end = 2 # 終了ページ(デフォルト) argv_size = ARGV.size if argv_size == 1 # 引数がひとつのとき page_end = ARGV[0].to_i # 終了ページとして解釈 elsif 2 <= argv_size # 引数がふたつ以上(3つ目以降は無視) page_start = ARGV[0].to_i # 開始ページ page_end = ARGV[1].to_i # 終了ページ end puts "Page:#{page_start}-#{page_end}" if DEBUG # ページ読込み処理を平行して行い、待ち時間を短縮 (page_start).upto(page_end) do |page| # page_star 〜 page_end まで繰り返し thread_info = { 'url' => BASEPAGE+"#{page}", # 目的ページのURL 'target_urls' => [], # 結果のURLリスト } # ページ毎にスレッド作成 thread_info['thread'] = Thread.new(thread_info) do |thread_info| url = thread_info['url'] turls = thread_info['target_urls'] begin doc = Hpricot( open(url).read ) # ページを読み込んでドキュメント化 # ■正規表現を用いて抽出 #(doc/'a').each do |link| # if REG_TUMBLER_URL =~ link[:href] # turls << link[:href] # end #end # ■XPathを用いて抽出 (doc/'div[@id="content"]/div[@class="post"]//a[img[@class="permalink"]]').each do |link| turls << link[:href] end puts "Done : #{url}" if DEBUG break rescue NameError => err # スレッド中で open() すると時々失敗してしまうので(NameError)リトライ処理を入れておく # 【正しい対応方法、どなたか教えて下さい】 puts "Error : #{err} (#{url})" if DEBUG Thread.pass retry end end thread_infos << thread_info end # スレッド終了待ち&URLリスト取得 thread_infos.each do |thread_info| puts "Checking %s" % [thread_info['url']] if DEBUG thread_info['thread'].join # スレッド終了待ち target_urls.concat(thread_info['target_urls']) # URLリストの結合 end # URLを降順にソート target_urls=target_urls.uniq.sort.reverse # 重複要素を取り除いて降順ソート if AUTO_MODE # 自動でHTML(リスト)を取得 thread_infos = [] target_urls.each do |url| thread_info = { 'url' => url } thread_info['thread'] = Thread.new(thread_info) do |thread_info| url = thread_info['url'] begin doc = Hpricot( open(url).read ) # タイトル取得し、連続した空白/改行文字を" "に title = CGI.escapeHTML( (doc/'title[1]')[0].inner_html.kconv(TO_CHARSET, FR_CHARSET).gsub(/\s+/, " ") ) # URLから'http://'を除く(ブックマークURL用) chop_url = url.sub(REG_CHOP_URL_PREFIX, "") # HTML(<li>〜</li>)作成 thread_info['list'] = LIST_FORMAT % [url,title,chop_url,url] rescue NameError => err puts "Error : #{err} (#{url})" if DEBUG Thread.pass retry end end thread_infos << thread_info end # スレッド終了待ち&リスト出力 thread_infos.each do |thread_info| puts "Checking %s" % [thread_info['url']] if DEBUG thread_info['thread'].join puts thread_info['list'] end else # 配列をはてな記法で出力 target_urls.each do |url| puts "-[#{url}:title:bookmark]" end end
出力例(cr_urllist.rb 1)
<li><a href="http://hrkt0115311.tumblr.com/post/145028464">読むcrossreview - 酔いがさめたら、うちに帰ろう。 ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145028464"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145028464" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145028385">読むcrossreview - オロロ畑でつかまえて (集英社文庫) ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145028385"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145028385" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145028257">読むcrossreview - ヴィズ・ゼロ ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145028257"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145028257" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145028061">読むcrossreview - 座右のメイ ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145028061"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145028061" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027940">読むcrossreview - わにわにのおでかけ (幼児絵本シリーズ) 何も起こらなくて、とても静かでいいね。 ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027940"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027940" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027819/biz">読むcrossreview - オタクで女の子な国のモノづくり (講談社BIZ) ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027819/biz"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027819/biz" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027699/4-sp">読むcrossreview - 天涯の武士~幕臣小栗上野介 4 (SPコミックス) ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027699/4-sp"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027699/4-sp" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027587/beautiful-future">読むcrossreview - Beautiful Future ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027587/beautiful-future"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027587/beautiful-future" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027516/review-best-of-glay">読むcrossreview - REVIEW〜BEST OF GLAY ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027516/review-best-of-glay"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027516/review-best-of-glay" /></a></li> <li><a href="http://hrkt0115311.tumblr.com/post/145027445/1-1-kr">読むcrossreview - からハニ 1 (1) (まんがタイムKRコミックス) ...</a><a href="http://b.hatena.ne.jp/entry/hrkt0115311.tumblr.com/post/145027445/1-1-kr"><img src="http://b.hatena.ne.jp/entry/image/http://hrkt0115311.tumblr.com/post/145027445/1-1-kr" /></a></li>
ruby 1.8.6・Windows XP上のコマンドプロンプト上で動作確認
工夫した点
- TumblrのURL抽出はXPath使用(コメントアウトしてあるけれども、正規表現版もあり)。
- リンク先タイトルも取得して、HTMLの出力(AUTO_MODE=falseで、はてな記法)。
- ページ取得の際の読み込み処理をスレッド化して並列処理することで、待ち時間を短縮。
- 取得範囲ページ範囲を引数で指定可能(デフォルトは1〜2ページ、"cr_urllist.rb 5"とすると1〜5ページ、"cr_urllist.rb 3 6"とすると3〜6ページ)。
よくわからなかった点
- 文字コードの取り扱い方(ファイル自身の文字コード、#!/usr/bin/ruby -K*、$KCODEの設定の関係とか、ページ取得したときのkconvの使い方とか)。
- スレッド内でopenしたとき、エラー(uninitialized constant OpenURI::Buffer::Tempfile(NameError))がでる場合がある。多分、スレッド間で競合して発生しているのだろうけれど、抜本的な対策がよくわからないので、retryするようにしてある(もしかして、rubyのバージョン上げればなおったりするのかな?)。あるいは、もっとスマートな非同期通信のやり方あり?
- スレッドの使い方(基本がわかってない)。
- 例外処理の書き方(これも)。
- そもそも、この解でどんなジレンマさんの目指す方向とあっているのか?
追記
ちょっと修正。スレッドでページ取得する処理が重複していたので、関数化。
#!/usr/bin/ruby -Ks require 'rubygems' require 'hpricot' require 'open-uri' require 'kconv' require 'cgi' # 文字コード設定(変更時は先頭行の -K* も変更のこと) $KCODE = "sjis" FR_CHARSET = Kconv::UTF8 TO_CHARSET = Kconv::SJIS # タイトル自動取得 AUTO_MODE = true # true: タイトルを自動取得してHTML出力 false: はてな記法で出力 LIST_FORMAT = '<li><a href="%s">%s</a><a href="http://b.hatena.ne.jp/entry/%s"><img src="http://b.hatena.ne.jp/entry/image/%s" /></a></li>' # デバッグ用 DEBUG = false # true でデバッグモード # ページ取得時にベースとなるURL BASEPAGE = "http://hrkt0115311.tumblr.com/page/" # 正規表現 REG_TUMBLER_URL = %r{^http://.*?\.tumblr\.com\/post/.+$} # 目的となるURLの抽出用 REG_CHOP_URL_PREFIX = %r{^http://} # ブックマークURL作成用 # ブロック内で配列を定義するとスコープの問題が起きるので、外で定義。 target_urls = [] thread_infos = [] # コマンドライン引数から page_start = 1 # 開始ページ(デフォルト) page_end = 2 # 終了ページ(デフォルト) argv_size = ARGV.size if argv_size == 1 # 引数がひとつのとき page_end = ARGV[0].to_i # 終了ページとして解釈 elsif 2 <= argv_size # 引数がふたつ以上(3つ目以降は無視) page_start = ARGV[0].to_i # 開始ページ page_end = ARGV[1].to_i # 終了ページ end puts "Page:#{page_start}-#{page_end}" if DEBUG # 複数ページ取得処理:ページ読込み処理を平行して行い、待ち時間を短縮 def multi_fetch(url_list) threads,docs = {},{} url_list.uniq.each do |url| # ページ毎にスレッド作成 threads[url] = Thread.new(url) do puts "[Thread] Start: #{url}" if DEBUG begin docs[url] = Hpricot( open(url).read ) # ページを読み込んでドキュメント化 puts "[Thread] End : #{url}" if DEBUG rescue NameError => err # スレッド中で open() すると時々失敗してしまうので(NameError)リトライ処理を入れておく # 【正しい対応方法、どなたか教えて下さい】 puts "Error : #{err} (#{url})" if DEBUG Thread.pass retry end end end url_list.each do |url| puts "Waiting: #{url}" if DEBUG threads[url].join puts "Done." if DEBUG end return docs end # ページ取得&目的URL(Tumblr)抽出 url_list = (page_start..page_end).to_a.map{|page| BASEPAGE+"#{page}"} # 目的ページのURLリスト作成 docs = multi_fetch(url_list) # 複数ページ取得 url_list.each do |url| # ■正規表現を用いて抽出 #(docs[url]/'a').each do |link| # if REG_TUMBLER_URL =~ link[:href] # target_urls << link[:href] # end #end # ■XPathを用いて抽出 (docs[url]/'div[@id="content"]/div[@class="post"]//a[img[@class="permalink"]]').each do |link| target_urls << link[:href] end end # URLを降順にソート target_urls=target_urls.uniq.sort.reverse # 重複要素を取り除いて降順ソート if AUTO_MODE # 自動でHTML(リスト)を取得 docs = multi_fetch(target_urls) # 複数ページ取得 target_urls.each do |url| # タイトル取得し、連続した空白/改行文字を" "に title = CGI.escapeHTML( (docs[url]/'title[1]')[0].inner_html.kconv(TO_CHARSET, FR_CHARSET).gsub(/\s+/, " ") ) # URLから'http://'を除く(ブックマークURL用) chop_url = url.sub(REG_CHOP_URL_PREFIX, "") # HTML(<li>〜</li>)作成 puts LIST_FORMAT % [url,title,chop_url,url] end else # 配列をはてな記法で出力 target_urls.each do |url| puts "-[#{url}:title:bookmark]" end end
追記2
タイトル部分をescapeしてなかったので、CGI.escapeHTML()追加。