風柳メモ

ソフトウェア・プログラミング関連の覚書が中心。

はじめてのRuby - どんなジレンマさんの例題をやってみる

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上のコマンドプロンプト上で動作確認

工夫した点

  1. TumblrのURL抽出はXPath使用(コメントアウトしてあるけれども、正規表現版もあり)。
  2. リンク先タイトルも取得して、HTMLの出力(AUTO_MODE=falseで、はてな記法)。
  3. ページ取得の際の読み込み処理をスレッド化して並列処理することで、待ち時間を短縮。
  4. 取得範囲ページ範囲を引数で指定可能(デフォルトは1〜2ページ、"cr_urllist.rb 5"とすると1〜5ページ、"cr_urllist.rb 3 6"とすると3〜6ページ)。

よくわからなかった点

  1. 文字コードの取り扱い方(ファイル自身の文字コード、#!/usr/bin/ruby -K*、$KCODEの設定の関係とか、ページ取得したときのkconvの使い方とか)。
  2. スレッド内でopenしたとき、エラー(uninitialized constant OpenURI::Buffer::Tempfile(NameError))がでる場合がある。多分、スレッド間で競合して発生しているのだろうけれど、抜本的な対策がよくわからないので、retryするようにしてある(もしかして、rubyのバージョン上げればなおったりするのかな?)。あるいは、もっとスマートな非同期通信のやり方あり?
  3. スレッドの使い方(基本がわかってない)。
  4. 例外処理の書き方(これも)。
  5. そもそも、この解でどんなジレンマさんの目指す方向とあっているのか?

追記

ちょっと修正。スレッドでページ取得する処理が重複していたので、関数化。

#!/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()追加。