kurukuru-papaのブログ

主に、ソフトウェア開発に関連したメモを書き溜めたいと思います。

はてなダイアリーを自動バックアップする

先日書店に行ってみると「Rubyist Magazine 出張版 Ruby on Windows」という書籍を見つけました。タイトルから想像できるように、Windows環境に特化したRubyの解説書でした。その中には、Internet Explorerを操作して、Web自動巡回という内容もありました。特段目新しいモノでもありませんが、何となく気にとまりました。

書籍やネットを参考にして、Rubyで、はてなダイアリーの内容を自動でバックアップするツールを作成してみました。次のように実行すると、Internet Explorerが起動し、自動的にはてなダイアリーのエクスポートページへ移動し、ファイル保存を行います。はてなのログインIDやパスワードなどは、事前に設定ファイルに記述しておきます。このRubyスクリプトでは、Hpricotというライブラリを使用しているので、事前にgemsを利用してインストールしておく必要があります。

実行手順

1.Hpricotインストール

Rubyスクリプトでは、HTMLの解析にHpricotというライブラリを使用していますので、事前に、gemを利用して、インストールしておきます。

gem install hpricot

2.設定ファイル編集

下記の「exporthatena.yaml.sample」を参考にして、「exporthatena.yaml」を作成します。ユーザ名やパスワードをご自分の設定に編集して下さい。必要あれば、「outbasename」項目に、パスを追加することで、任意のディレクトリにファイル保存できます。

3.Rubyスクリプト実行

次のコマンドを実行することで、Internet Explorerが起動し、はてなダイアリーのエクスポートページへ移動し、ファイル保存を行います。

ruby exporthatenablog.rb

Rubyスクリプト

exporthatenablog.rb

#!ruby -Ks
# IEを利用して、はてなからブログデータをエクスポートします。
#
# 使用時の注意事項
# ・設定ファイルサンプル(exporthatena.yaml.sample)を参考にして、
#   当ファイルと同じディレクトリに、設定ファイル(exporthatena.yaml)
#   を作成して下さい。
# ・ブログデータをファイル保存する際、ポップアップブロックにより、
#   情報バーが表示される場合、次のどちらかの方法で、情報バーが表示
#   されないようにして下さい。
#   (1)ポップアップブロックをオフにしてください
#   (2)はてな(http://*.hatena.ne.jp)を許可サイトに登録し、セキュ
#         リティレベル設定で、ゾーン間を移動したときのダイアログボッ
#         クスを非表示にして下さい。
# ・動作中は、キーボードやマウスの操作をしないで下さい。
#
# 2009/02/07 新規作成

require 'iebrowsing'
require 'win32ole'

class HatenaBlogExport < IEBrowsing
  def initialize
    super("exporthatena.yaml")
  end
  
  def run
    browsing {
      transaction {
        @ie.Navigate("http://d.hatena.ne.jp/")
      }
      
      transaction("はてなダイアリー - 無料ブログを簡単にはじめよう、すぐできるブログ(blog)") {
        get_links().get_by_innertext("ログイン").first.Click()
      }

      transaction("ログイン - はてな") {
        get_element_by_id("login-name").Value = @ini["username"]
        get_element_by_id("password").Value = @ini["password"]
        get_element_by_id("auto_login").Value = 0
        get_elements_by_tagname("form").first.Submit()
      }
      
      transaction("はてなダイアリー - 無料ブログを簡単にはじめよう、すぐできるブログ(blog)") {
        get_links.get_by_innertext("管理").first.Click()
      }
      
      transaction("#{@ini['username']}の日記 - 管理ツールトップ") {
        get_links.get_by_innertext("データの管理").first.Click()
      }
      
      path = @ini["outbasename"] + Time.now.strftime("%Y%m%d-%H%M%S") + @ini["outext"]
      transaction("#{@ini['username']}の日記 - インポート/エクスポート") {
        get_links.get_by_innertext("ダウンロード").first.Click()
        # ダイアログボックスを操作してファイル保存します。
        send_keys_download_dialog(path)
      }
      @log.info("ダウンロードしました。ファイル名=[#{path}]")
      
      transaction("#{@ini['username']}の日記 - インポート/エクスポート") {
        get_links.get_by_innertext("ログアウト").first.Click()
      }
      
      transaction("はてなダイアリー - 無料ブログを簡単にはじめよう、すぐできるブログ(blog)") {
      }
    }
  end
end

# 実行します。
begin
  HatenaBlogExport.new.run
rescue
  puts $!.message
  puts $!.backtrace
end

iebrowsing.rb

# IEブラウジングクラス
# IEと、COMオブジェクトのHTMLDocumentクラスを操作します。
#
# 前提
# ・Hpricotを使用します。事前に次のようにしてインストールしておいて下さい。
#   gem install hpricot
#
# 2009/02/06 新規作成

require 'logger'
require 'nkf'
require 'win32ole'
require 'yaml'

require 'rubygems'
require 'hpricot'

require 'htmlelementcollection'

class IEBrowsing
  attr_reader :ie
  
  # タイムアウト時間(秒)
  DEFAULT_TIMEOUT = 30
  # デフォルトの処理待ち時間(秒)
  DEFAULT_WAIT = 0.1
  
  # ログ
  @log = nil
  # 初期設定ファイル連想配列
  @ini = {}
  # IEのCOMオブジェクト
  @ie = nil
  # WScript.ShellのCOMオブジェクト
  @wsh = nil
  
  # 初期化
  def initialize(yamlfile=nil)
    # ログ出力を設定します。
    @log = Logger.new(STDOUT)
    @log.level = Logger::INFO
    
    # YAML形式の初期設定ファイルを読み込みます。
    if yamlfile
      open(yamlfile) { |io|
        @ini = YAML.load(io.read)
      }
      @log.info("設定ファイル[#{yamlfile}]を読み込みました。")
    end
    
    # WScript.ShellのCOMオブジェクトを作成します。
    @wsh = WIN32OLE.new('WScript.Shell')
  end

  # IE操作ブロック
  # 指定されたブロック処理の前段でIEを起動し、ブロック処理後、IEを
  # 終了します。
  def browsing(&block)
    browsing_start()
    yield()
    browsing_end()
  end
  
  # IE操作開始処理
  def browsing_start()
    # COMオブジェクトを作成します。
    @ie = WIN32OLE.new('InternetExplorer.Application')
    # IEのウインドウを可視化します。
    @ie.Visible = true
    
    @log.info("IEを起動しました。")
  end
  
  # IE操作終了処理
  def browsing_end()
    @ie.Quit()
    @ie = nil
    
    # IE終了後、続けてIE起動すると失敗するため、少しウエイトを入れます。
    sleep 1
    
    @log.info("IEを終了しました。")
  end
  
  # トランザクション処理ブロック
  # title - 任意引数。操作を行うページのタイトルを指定します。
  #alias :page :transaction
  def transaction(title=nil, &block)
    wait_ie(title)
    
    begin
      yield()
    rescue
      @log.error("エラー発生時のHTML出力開始")
      @log.error(@ie.document.documentElement.outerHTML)
      @log.error("エラー発生時のHTML出力終了")
      raise
    end
    
    wait_ie
  end
  
  # IE処理待ち
  # title - 任意引数。指定された場合、TITLEタグ要素の内部テキストが
  #         引数の値になるまで待ちます。未指定の場合、ページ内容に
  #         関わらず、IEのページ読み込み完了まで待ちます。
  def wait_ie(title=nil)
    wait {
      # IE処理中の間、待ちます。
      wait {
        @ie.busy
      }
      
      result = false
      if title
        @log.debug("タイトルチェック開始")
        doc = Hpricot(@ie.document.documentElement.outerHTML)
        real_title = NKF.nkf("-s", (doc/"title").inner_html)
        result = (title != real_title)
        if ! result
          @log.info("ページ[#{real_title}]を表示しました。")
        end
        @log.debug("想定=[#{title}]")
        @log.debug("実際=[#{real_title}]")
        @log.debug("結果=[#{result}]")
        @log.debug("タイトルチェック終了")
      end
      result
    }
  end
  
  # 判定処理&タイムアウト付きウエイト
  def wait(timeout=DEFAULT_TIMEOUT, &block)
    time = 0
    while yield
      sleep DEFAULT_WAIT
      time += DEFAULT_WAIT
      if time >= timeout
        raise "ERROR: タイムアウトが発生しました。"
      end
    end
  end
  
  # HTMLドキュメントから要素の取得
  def get_element_by_id(id)
    return @ie.document.getElementById(id)
  end
  
  # HTMLドキュメントから要素の取得
  def get_elements_by_name(name)
    return HtmlElementCollection.new(@ie.document.getElementsByName(name))
  end
  
  # HTMLドキュメントから要素の取得
  def get_elements_by_tagname(tagname)
    return HtmlElementCollection.new(@ie.document.getElementsByTagName(tagname))
  end
  
  # HTMLドキュメントから要素の取得
  def get_links()
    return HtmlElementCollection.new(@ie.document.links)
  end
  
  # 表示HTMLのファイル保存
  def save(filepath)
    open(filepath, "w") { |io|
      io.binmode
      io.write(@ie.document.documentElement.outerHTML)
    }
  end
  
  # IEの「名前を付けて保存」機能を使用して表示ページファイル保存(html形式)
  def save_html(filepath)
    save_by_ie(filepath, "{TAB}")
  end
  
  # IEの「名前を付けて保存」機能を使用して表示ページファイル保存(mht形式)
  def save_mht(filepath)
    save_by_ie(filepath, "{DOWN}{TAB}")
  end
  
  # IEの「名前を付けて保存」機能を使用して表示ページファイル保存
  # type_keys - ファイルの種類を選択するためのキー操作
  def save_by_ie(filepath, type_keys)
    # IEをアクティブにします。
    doc = Hpricot(@ie.document.documentElement.outerHTML)
    title = NKF.nkf("-s", (doc/"title").inner_html)
    wait {
      ! @wsh.AppActivate("#{title} - Microsoft Internet Explorer")
    }
    # メニューから、ファイル>名前を付けて保存、を選択します。
    @wsh.SendKeys("%fa")
    # 「Web ページの保存」ダイアログボックスをアクティブにします。
    wait {
      ! @wsh.AppActivate("Web ページの保存")
    }
    # ファイル名を設定します。
    @wsh.SendKeys("%n")
    @wsh.SendKeys(filepath)
    # ファイルの種類を設定します。
    @wsh.SendKeys("{TAB}")
    @wsh.SendKeys("{DOWN}")
    sleep 1
    @wsh.SendKeys(type_keys)
    # 保存を実行します。
    @wsh.SendKeys("%s")
    # ログ出力
    @log.info("ファイル[#{filepath}]を保存しました。")
  end
  
  # ファイルのダウンロードダイアログボックスの操作
  def send_keys_download_dialog(filepath)
    # ダイアログボックスをアクティブにします
    wait {
      ! @wsh.AppActivate("ファイルのダウンロード")
    }
    # ダイアログボックスが表示されても、ボタンなどが描画されていな
    # いかもしれないので、スリープします。
    sleep 1
    # 「保存」ボタンを押します
    @wsh.SendKeys("s")
    # ダイアログボックスをアクティブにします
    wait {
      ! @wsh.AppActivate("名前を付けて保存")
    }
    # ダイアログボックスが表示されても、ボタンなどが描画されていな
    # いかもしれないので、スリープします。
    sleep 1
    # ファイル名欄にカーソルを移動します
    @wsh.SendKeys("%n")
    # ファイル名を入力します
    @wsh.SendKeys(filepath)
    # 保存します
    @wsh.SendKeys("{ENTER}")
  end
end

exporthatena.yaml.sample

# exporthatenablog.rb用設定ファイル
#
# 2009/02/07 新規作成

# ユーザ設定
# はてなへログインするためのユーザ名とパスワードを設定して下さい。
username: username
password: password

# 出力ファイル設定
outbasename: hatena_blog_
outext: .xml

動作環境