ブログ自動投稿ツール"はてなブログライター"を再びカスタマイズ

現在の"はてなブログ"になる前の"はてなダイアリー"時代から、日記の投稿は半自動(?)ツールを利用していました。


具体的にはPC本体に記事ごとのテキストファイルを用意しておき、それを投稿する形。
こうしておくとネットと繋がっていない状況で記事を書いて一気に投稿したり、はたまた検索もローカルにあるテキストファイルをgrepしたり置換も出来ちゃうのです。


"はてなダイアリー"時代は『はてなダイアリーライター』なるPerlで書かれたスクリプトを利用していましたが"はてなダイアリー"が廃止され"はてなブログ"に移行したら便利なツールも使えなくなってしまった・・・・


すると世の中の誰かがRubyスクリプトで作ってくれたのが『はてなブログライター』。

自動投稿のお話のうちの一件
https://…/2018/09/19/ はてなダイアリーライター → はてなブログライターへの移行を考える

記事の投稿はHtml/formの送信で誤魔化すのではなく、"はてなブログAPI"を利用するという正攻法で実装されています。
API仕様が記事一つずつにランダム付与される記事IDをキーにしてアクセスする必要があり、必然的にローカル側に過去の全記事と記事IDの紐づけデータベースが必要になってしまうのが難点。(初回投稿は付与される記事IDを記録し、2回目以降の修正時は記事IDとテキストを送る必要がある。)


正攻法のためローカル側からのアップロードの他、サイト側から記事をダウンロードすることもできる(はてなブログダウンローダー)のが"はてブラ"の利点
今までは記事データはPC→サイトへの一方通行だったため記事の修正は必ずPCで行う必要がり、またはサイト上で記事を修正した場合は自分でそれを覚えておいてPC側のファイルも修正しておく必要がありました。(←これをよく忘れる。)


ここまでが経緯な訳で、ここからが本題
Rubyスクリプトで書かれた"はてなブログダウンローダー"ですがその仕組み上・・・・

  1. 起動すると過去に投稿した全テキストファイルを読み込んでメモリ上にデータベースを構築
  2. その次にWebサイトにアクセスして日記日付順の最新n件の記事のリストを取得(記事ID・URL・更新日時)して記事IDからメモリ上のDBを検索してローカルファイル名(日記の日付)を特定
  3. サイト側記事更新日時とメモリDB上のローカルファイルの更新日時を比較しての更新の有無を判別
  4. 上記で更新ありと判断した場合は、更新された記事を記事ID指定で一つずつダウンロードしてローカルファイルを更新

・・・・のような動きをします。
おそらく普通に使うぶんにはこの仕様で問題ないと思われますが、私の場合は過去記事が7,000件近く存在するのです。


すると過去ファイルをすべて読み込んでメモリ上にDB(ハッシュテーブル)を構築するのにそこそこ時間がかかる・・・・
待てなくはないけれど便利なツールなので結構手動で実行しますし、同期忘れがないようにログインスクリプトにも組み込んでいてPCを起動したりネットワークにつなげると自動的に同期させるようにもしていたりするので、結構ストレス!


でもなぁ~、Rubyってじっくりと触ったことがないのよね。(←以前若干カスタマイズはしていますがね。)

簡単にカスタマイズした日記
https://…/2018/11/13/ はてなブログへの移行作業 その後の状況です

改造したいけれど『出来るかな?』のほうが先立ってしまい長年放置していたのですが、頑張ってみることに。


アルゴリズムは次のような感じに。

  1. 廃止!  過去の記事ファイルをすべて読み込みメモリ上にDB構築 
  2. Webサイトにアクセスして日記日付順の最新n件の記事のリスト(記事ID・URL・更新日時)を取得
  3. はてなブログでは日記記事のURLの付け方を設定で選択可能になっていて、私の場合は日付を付けるようにしているのでこれを最大限利用して日付を特定する
  4. ローカルのテキストファイルを読み込んでメモリ上にDBを構築する。この時すべてのファイルを読み込むのではなく先で判別している日付ファイルだけに対象を絞り込んで読み込むため高速!(←昔は一日に複数記事を投稿していたので、日付だけでは記事を一つに絞り込めないため複数ファイルを読み込む。)
  5. 以下は現行の処理と同じで、サイト側記事更新日時とメモリDB上のローカルファイルの更新日時を比較しての更新の有無を判別
  6. 上記で更新ありと判断した場合は、更新された記事を記事ID指定で一つずつダウンロードしてローカルファイルを更新


意外とあっさりとコーディングできてしまった・・・・
Rubyそのものの言語目的が『プログラミングを楽しく』というくらいだからな。他の言語でのソフトウェアを知っていれば何とかなるのかも。


今までは実行前に7,000ファイルの読み込みを待っていたのですが、この変更により待ち無しで直ちにWebアクセスし、(標準では)7個の記事リストを取得して7ファイルだけメモリに読み込むのでとっても高速!


もっと早く修正しておくべきだった・・・・

オリジナルはこちら
https://github.com/rnanba/HatenaBlogWriter

ソースコードはこちら。(無保証)

HatenaBlogDownloader.rb
#!/usr/bin/env ruby
# coding: utf-8
require_relative './HatenaBlogWriter.rb'
require_relative './HatenaBlogDownloader.rb'

VERSION = "0.1.1"
ESC_BGRED    = "\e[41m"		#@@TSM
ESC_BGYELLOW = "\e[43m"		#@@TSM
ESC_BGBLUE   = "\e[44m"		#@@TSM
ESC_RESET    = "\e[0m"		#@@TSM

#@@TSM BEGIN 2023.03.11
#def load_db
#  db = {}
#  HBW::EntryMetaData.listData().each() { |data|
#    db[data.location] = data
#  }
#  return db
#end
#@@TSM END   2023.03.11

#@@TSM BEGIN 2023.03.11
def load_db1rec(edit,url)
  datestr = url.gsub("https://tarsama.hatenadiary.com/entry/","")
  datestr = datestr.slice(0,4) +"-"+ datestr.slice(4,2) +"-"+ datestr.slice(6,2)
  db1rec = {}
  HBW::EntryMetaData.listData1Rec(datestr).each() { |data|
    db1rec[data.location] = data
  }
  return db1rec
end
#@@TSM END   2023.03.11

def update_entry(data_file, entry_file, time_edited)
  entry_filename = data_file.entry_filename
  i = 0
  loop do
    i += 1
    filename = sprintf("#{entry_filename}.%d", i)
    next if File.exists?(filename)
    File.rename(entry_filename, filename)
    break
  end
  entry_file.save_as(entry_filename)
  puts "OK: " +ESC_BGYELLOW+ "#{entry_filename}: エントリファイルを更新しました。" +ESC_RESET
  data_file.set_updated(time_edited, entry_file.sha1)
  data_file.save()
end

def save_new_entry(entry_file, time_edited, location, url)
  base_filename = entry_file.date.strftime("%04Y-%02m-%02d")
  i = 0
  loop do
    i += 1
    filename = sprintf("#{base_filename}_%02d.txt", i)
    next if File.exists?(filename)
    entry_file.save_as(filename)
    puts "OK: " +ESC_BGBLUE+ "#{filename}: エントリファイルを作成しました。" +ESC_RESET
    data_file = HBW::EntryMetaData.new(filename)
    data_file.set_posted(location, time_edited, entry_file.sha1)
    data_file.set_url(url)
    data_file.save()
    break
  end
end

def update_data(data_file, entry, entry_file)
  data_file.set_updated(Time.parse(entry.edited.text), entry_file.sha1)
  data_file.save
end

entry_count_limit = 7
if ARGV.length > 0 then
  if ARGV[0] == 'version' then
    puts "HatenaBlogWriterDownloader v#{VERSION}"
    exit

#@@TSM BEGIN 2023.03.18
  elsif ARGV[0] == 'until' then
    entry_count_until = ARGV[1].to_i
    entry_count_limit = 0

#@@TSM END   2023.03.18
  else
    entry_count_until = 0
    entry_count_limit = ARGV[0].to_i
  end
  ARGV.shift
end

puts "HatenaBlogWriterDownloader v#{VERSION}: Start."
loader = HBW::FeedLoader.new()
#@@TSM BEGIN 2023.03.11
#db = load_db()
#@@TSM END   2023.03.11
entry_count = 0
update_count = 0	#@@TSM
rnd = Random.new
loader.each_feed { |feed|
# sleep(0.5 + rnd.rand(0.5)) if entry_count > 0
  break if entry_count_limit > 0 && entry_count_limit <= entry_count
  feed.entries.each { |entry|
    break if entry_count_limit >0 && entry_count_limit <= entry_count
    entry_count += 1
    
    puts "\e[K---"
    puts "title: " + entry.title
    url = nil
    edit = nil
    entry.links.each { |link|
      if link.rel == "edit"
        edit = link.href
      elsif link.rel == "alternate"
        url = link.href
      end
    }
    entry_file = nil
    begin
      entry_file = HBW::EntryFile.new(entry)
    rescue
      puts "エントリの解析に失敗しました。: #{$!}"
      next
    end
    time_edited = Time.parse(entry.edited.text)
    sha1 = entry_file.sha1()
    puts "URL: " + url
    puts "sha1: " + sha1
#@@TSM BEGIN 2023.03.11
    datestr = url.gsub("https://tarsama.hatenadiary.com/entry/","")
    datestr = datestr.slice(0,4) + datestr.slice(4,2) + datestr.slice(6,2)
    if datestr.to_i == entry_count_until
      entry_count_limit = 1
    end

    db1rec = load_db1rec(edit,url)
#   data = db[edit]	#@@TSM
    data = db1rec[edit]
#@@TSM END   2023.03.11
    if data != nil then
      puts "既存のエントリです: " + data.entry_filename
      if data.sha1 == sha1 then
        puts "変更はありません。"
      else
        local_entry_file = HBW::EntryFile.new(data.entry_filename)
        if local_entry_file.date == nil then
          dateless_entry_file = HBW::EntryFile.new
          dateless_entry_file.parse_entry(entry, true)
          sha1 = dateless_entry_file.sha1()
          if data.sha1 == sha1 then
            puts "変更はありません。(date ヘッダなしのエントリ)"
            next
          end
        end
        puts "変更があります。"
        if data.mtime < File.mtime(data.entry_filename) then
          puts "警告: " +ESC_BGRED+ "エントリファイルは投稿後に更新されています。" +ESC_RESET
        end
#       puts "エントリファイルをリモートの内容で更新しますか? (yes/No)"
#       if gets.chomp == 'yes'
          data.set_url(url)
          update_entry(data, entry_file, time_edited)
          update_count += 1	#@@TSM
#       end
      end
    else
      puts "新規エントリです。"
      save_new_entry(entry_file, time_edited, edit, url)
    end
  }
  break if entry_count_limit > 0 && entry_count_limit <= entry_count
  print "Waiting: #{entry_count} articles. #{update_count} updated.\r"	#@@TSM
  sleep(0.5 + rnd.rand(0.5)) if entry_count > 0
}
puts ""									#@@TSM
puts "Results: #{entry_count} articles. #{update_count} updated."	#@@TSM