Ruby & Mechanize で Yahoo! Auction のアクセス数自動記録

テーマ:
今日は、プログラミング言語Rubyの話です。
Rubyは、日本人が開発したプログラミング言語として、世界的に広まって最も成功した言語であると言えると思います。
スクリプト言語として有名なものにPerlやPHPがありますが、Rubyもそれらの言語に劣らない魅力があります。
ファイルの自動編集、ネット上のデータの自動取得&解析などなど、いろいろなことに使える便利な言語で、私も最近習い始めました。

Ruby に Mechanize というライブラリを併用して、Yahoo! Japan に自動ログイン後、オークションのアクセス数を1分毎に自動記録するためのスクリプトを書きましたので、そのソースコードを公開します。
ソースコードは、自由に改変して使って頂いても構いません。

ログイン時に、Hidden タグに JavaScript で値をセットする必要があるため、JavaScript に対応していない Mechanize でログインしようとすると、画像認証のページが出てしまいます。
JavaScript を手動で解析、値を設定し、画像認証を回避するようにしました。

アクセス数は、"auction_g123456789_log.txt" といったオークション ID が入ったファイル名にタブ区切りで記録されます。
なので、ファイルをテキストエディタで開き、データをコピーして Excel に張り付けるとセルに自動配置されますので、あとはグラフを作成するなり好きにデータを利用してください。

これはアクセス数と価格の推移をエクセルでグラフ化した例です。

$ほしとねこが好き


スクリプトは UTF-8 で保存してください。
名前は yahoo.rb などとします。
実行時に yahoo.rb とパスワードを第1引数として実行することもできます。
(指定しない場合は、入力を求められます。)
自分のオークションの URL と、ユーザー名はスクリプト内で記述してください。

スクリプトは、オークション終了時に自動終了します。
自分の好きなようにカスタマイズして、お楽しみください。

※動作検証環境
OS: Windows 7 64bit SP1
Ruby: Ver 1.9.3-p194
Mechanize: Ver 2.5.1 ("gem install mechanize" コマンドでインストール)

-----------------------------------------------

#!/usr/bin/ruby
# coding: utf-8

# The first line is shebang.
# The second line is magic comment representing a script encoding.
# スクリプトエンコーディング - Ruby スクリプトを書くのに使われているエンコーディングで、
# 非 ASCII 文字を使う場合は必ず設定する必要がある(エラーになる)。
# なお、全ての String などのオブジェクトは自身のエンコーディング情報を保持しており、
# 同じプロセスの中で異なるエンコーディングの文字列が同時に存在することができる。

$datetime_start = Time.now

# You need install Mechanize by the command, "gem install mechanize".
require 'mechanize'


# Constant variables

USERNAME = "yahoo_tarou" # ここに自分のログイン名を入れる

AUCTION_PAGE = "http://page5.auctions.yahoo.co.jp/jp/auction/e123456789" # ここに出品した自分のオークションの URL を入れる

# Regular expression test : http://rubular.com/
AUCTION_PAGE =~ /\/([a-zA-Z0-9]+$)/ # オークション ID を抜き出す - 正規表現での最後のマッチを $+ に入れる(なければ nil)
if !$+ then
    puts "Failed to extract the auction ID from the URL \"#{AUCTION_PAGE}\". Please confirm."
    exit
end

LOG_FILE_NAME = "auction_#{$+}_log.txt" # ex. "auction_e123456789_log.txt"

$program_name = File.basename $PROGRAM_NAME # ex. "yahoo.rb"

ACCESS_INTERVAL = 60 # アクセス頻度 in seconds


# Functions

def puts_console(*args)
    datetime_now = Time.now
    elapse = datetime_now - $datetime_start # Float in seconds
    days = elapse.divmod(24*60*60) #=> [2.0, 45000.0]
    hours = days[1].divmod(60*60) #=> [12.0, 1800.0]
    minutes = hours[1].divmod(60) #=> [30.0, 0.0]
    seconds = minutes[1].divmod(1)
    header = sprintf("[%s] %s (", $program_name, datetime_now.strftime("%Y/%m/%d %H:%M:%S.%L")) #=> ex. "2012/08/02 15:02:05.332"
    header += sprintf("%.0f ", days[0]) if days[0] > 0
    header += sprintf("%02.0f:", hours[0]) if hours[0] > 0
    header += sprintf("%02.0f:", minutes[0]) if minutes[0] > 0
    header += sprintf("%02.0f.%03.0f) ", seconds[0], seconds[1] * 1000)
    print header
    puts args
end


# Main scripts

# ログを記録するためのライブラリ
# require 'logger'

puts_console "Program started."

# 引数チェック
password = ""
if ARGV.size == 0 then
    puts "You can specify your password as \"#{$program_name} <password>\"."
    print 'Please type your password: '
    password = gets.chomp
else
    password = ARGV[0]
end

agent = Mechanize.new
# agent.log = Logger.new 'mech.log'
agent.user_agent_alias = 'Windows IE 9'

# You need to set the environmental value for OpenSSL to identify the root certificates.
# set SSL_CERT_FILE=C:\Ruby193\lib\ruby\1.9.1\rubygems\ssl_certs\ca-bundle.pem

# Top page (UTF-8)
agent.get "http://www.yahoo.co.jp/"
puts_console "Opened: #{agent.page.uri.to_s}"

# File.write("yahoo_top.html", agent.page.body)
# p agent.page.meta_charset() #-> ["utf-8"]
# p agent.page.response_header_charset() #-> ["utf-8"]
# p agent.page.encodings #-> [nil, "UTF-8", "utf-8", "utf-8"]
agent.page.link_with(:href => 'r/pl1').click
puts_console "Opened: #{agent.page.uri.to_s}"

# Login page (EUC-JP)
# File.write("yahoo_login.html", agent.page.body)
formLogin = agent.page.form_with(:name => "login_form")
formLogin.field_with(:name => "login").value = USERNAME
formLogin.field_with(:name => "passwd").value = password
# JavaScript でページロード時に、hidden タグの値を書き換えている。
# これを行なわないと、画像による認証のページに飛ばされてしまう。
# <input type="hidden" name=".albatross" value="dD0vaDJFUUImc2s9UDJSa0hZcmFremh1V095aElEVkc1MGJoU2pVLQ==">
# document.getElementsByName(".albatross")[0].value = "dD1hdDJFUUImc2s9VkpGQmlZTnpYUWFEeWhqUTlacnJqOFpVdWcwLQ==";
StringJava = 'document.getElementsByName(".albatross")[0].value = "'
agent.page.body.each_line do |line|
    index = line.index(StringJava)
    if index then
        value = line[index + StringJava.length...line.rindex('"')]
        formLogin.field_with(:name => ".albatross").value = value
        break
    end
end
agent.submit formLogin
puts_console "Opened: #{agent.page.uri.to_s}"

# Redirect page after login
# File.write("yahoo_top2.html", agent.page.body)
# ここでは <meta http-equiv="Refresh" content="0; url=http://www.yahoo.co.jp"> によってトップページへのリダイレクトが行われている。
# 自動での追随設定も可能のようだが、ここでは手動での追随を行うこととする。
# p agent.page.meta_refresh.class
# p agent.page.meta_refresh.count
# p agent.page.meta_refresh
# p agent.page.meta_refresh[0]
# p agent.page.meta_refresh[0].uri
if agent.page.meta_refresh[0] && agent.page.meta_refresh[0].uri then
    # URI::HTTP クラスではなく String を渡さないと動かないようだ。
    agent.get(agent.page.meta_refresh[0].uri)
else
    puts_console "Failed to log in as \"#{USERNAME}\". Please confirm the password."
    exit
end
puts_console "Opened: #{agent.page.uri.to_s}"

# Top page after login
# File.write("yahoo_top3.html", agent.page.body)
# ここでようやくログイン後のトップページに到達する。
# 以下のメッセージを探して、ログインに成功したかどうかを判定する。
# <h3 id="pbhello">こんにちは、<span>yahoo_tarou</span>さん</h3>
if !agent.page.at("h3[@id='pbhello']/span") || USERNAME != agent.page.at("h3[@id='pbhello']/span").inner_text then
    puts_console "Failed to log in as \"#{USERNAME}\". Please confirm the password."
    exit
end
puts_console "Succeeded in logging in as \"#{USERNAME}\"."

# 現在の価格:722 円
# 残り時間 :終了 (詳細な残り時間)
# 入札件数 :8 (入札履歴)
#
# このオークションの統計情報
#
# アクセス総数 : 79
# ウォッチリストに追加された数 : 11
# 友だちにメールを送られた数 : 0
# 違反商品の申告をされた数 : 0
#
# アフィリエイト経由
# アフィリエイト報酬率 :1%
# アクセス総数 : 12
# ウォッチリストに追加された数 : 4
# 入札件数 : 0
statics = Array::new
statics << ['price', 0, '//*[@id="modPdtInfo"]/div[2]/table[1]/tr/td[2]/div[2]/table/tr[1]/td[2]/p']
statics << ['time_remaining', "", '//*[@id="modPdtInfo"]/div[2]/table[1]/tr/td[2]/div[2]/table/tr[2]/td[2]/b'] # 即決価格がある場合は tr[2] ではなく tr[3] となる
statics << ['bid', 0, '//*[@id="modPdtInfo"]/div[2]/table[1]/tr/td[2]/div[2]/table/tr[3]/td[2]/b']
statics << ['viewed', 0, '//*[@id="modSellInfo"]/div[2]/div/table/tr[1]/td']
statics << ['watch_listed', 0, '//*[@id="modSellInfo"]/div[2]/div/table/tr[2]/td']
statics << ['emailed', 0, '//*[@id="modSellInfo"]/div[2]/div/table/tr[3]/td']
statics << ['violated', 0, '//*[@id="modSellInfo"]/div[2]/div/table/tr[4]/td']
statics << ['viewed_af', 0, '//*[@id="modSellInfo"]/div[4]/div/table/tr[2]/td']
statics << ['watch_listed_af', 0, '//*[@id="modSellInfo"]/div[4]/div/table/tr[3]/td']
statics << ['bid_af', 0, '//*[@id="modSellInfo"]/div[4]/div/table/tr[4]/td']

# Output header to the log file
log_header = "time"
statics.each { |item|
    log_header += "\t" + item[0]
}

if FileTest.exist?(LOG_FILE_NAME) then
    puts_console "Opened \"#{LOG_FILE_NAME}\"."
else
    puts_console "Created \"#{LOG_FILE_NAME}\"."
    File.open(LOG_FILE_NAME, 'a') { |f|
        f.puts log_header
    }
end
puts_console log_header

flag_break = false
while true
    datetime_access = Time.now
    begin
        agent.get(AUCTION_PAGE)
    rescue => exception
        # 以下のようなエラーが発生したことがあるので例外をハンドルしないとプログラムがストップしてしまう:
        # C:/Ruby193/lib/ruby/gems/1.9.1/gems/net-http-persistent-2.7/lib/net/http/persistent.rb:770:
        # in `rescue in reset': connection refused: page5.auctions.yahoo.co.jp:80 (Net::HTTP::Persistent::Error)
        # from C:/Ruby193/lib/ruby/gems/1.9.1/gems/net-http-persistent-2.7/lib/net/http/persistent.rb:763:in `reset'
        # from C:/Ruby193/lib/ruby/gems/1.9.1/gems/net-http-persistent-2.7/lib/net/http/persistent.rb:503:in `connection_for'
        # from C:/Ruby193/lib/ruby/gems/1.9.1/gems/net-http-persistent-2.7/lib/net/http/persistent.rb:806:in `request'
        # from C:/Ruby193/lib/ruby/gems/1.9.1/gems/mechanize-2.5.1/lib/mechanize/http/agent.rb:258:in `fetch'
        # from C:/Ruby193/lib/ruby/gems/1.9.1/gems/mechanize-2.5.1/lib/mechanize.rb:407:in `get'
        # from C:/scripts/yahoo_auction.rb:189:in `<main>'
        puts_console exception.to_s
    else
        # File.write("yahoo_auction.html", agent.page.body)
        puts_console "Opened: #{agent.page.uri.to_s}"

        # XPath は Chrome のデベロッパーツールで取得できるが、<tbody> など実際にはソースにないタグが勝手に付け加えられていることがあるので要注意。
        statics.each { |item|
            if agent.page.at(item[2]) then
                value = agent.page.at(item[2]).inner_text # ex. ": 32"
                if item[0] == 'time_remaining' then
                    # "残り時間" に関しては何も処理をしない
                    # agent.page.at が返す Nokogiri::XML::Text は EUC-JP のページでも UTF-8 で文字データを保持するようだ。
                    item[1] = value
                    if value == "終了" then
                        flag_break = true # オークションが終了したのでプログラムを終了する
                    end
                else
                    value =~ /([0-9,]+)/ # 数字部分だけを取り出す - 正規表現での最後のマッチを $+ に入れる(なければ nil)
                    item[1] = $+.gsub(/,/, "") if $+ # 桁区切りのコンマがあれば除く
                end
            end
        }

        # Output data to the log file
        log_data = datetime_access.strftime("%Y/%m/%d %H:%M:%S.%L") #=> ex. "2012/08/02 15:02:05.332"
        statics.each { |item|
            log_data += "\t" + item[1].to_s
        }
        puts_console log_data
        File.open(LOG_FILE_NAME, 'a') { |f|
            f.puts log_data
        }

        if flag_break then
            break
        end
    ensure
        # Sleep with time adjustment - 1 回のアクセスに 260 - 296 msec 程度かかるのでそれを補正
        elapse = Time.now - datetime_access # Float in seconds
        sleep_duration = ACCESS_INTERVAL - elapse
        if sleep_duration > 0 then
            puts_console sprintf("Sleep for %.3f seconds...", sleep_duration)
            sleep sleep_duration
        end
    end
end

puts_console sprintf("The auction ended.")

--------------

たのしいRuby 第3版/ソフトバンククリエイティブ

¥2,730
Amazon.co.jp

AD