id記法のリンクの修正 & お詫び

はてなダイアリーのインポートで移行した記事ですが、id記法で記述したはてなダイアリーへのリンクが全滅しています。これははてなダイアリーはてなブログではid記法の仕様が異なるせいです。なんでうまいこと変換してくれないかなーと思いつつ一括で修正してみました。

例によって HatenaBlogWriter (hbw) を使うのが前提です。

はてなダイアリーはてなブログでのid記法の違い

詳しくは今村さんのブログエントリを参照してください。

ここには書かれてなかったと思うのですが、もう一つ違いがあります。

はてなダイアリーでは a タグの href 属性値にid記法を書くとURLに展開してくれるのですが、はてなブログでは展開してくれません。はてなブログで有効な d:id:rna:20190128:p1 のようなid記法で書いてもダメです。

idリンクの置換

以下のようなスクリプトを書きました。要は正規表現置換なのですが、正規表現は苦手科目です… 何度も試行錯誤できるように元のエントリファイルをサブディレクトリにコピーして、それを入力として変換するようにしました。

convert-id-links.rb:

#!/usr/bin/env ruby
# coding: utf-8
# HatenaBlogWriter のワーキングディレクトリで実行します。
# orig ディレクトリに変換前のエントリファイルをコピーしてから実行します。

ORIG_DIR = "orig"
TB_SECTION_SEPARATOR = '<!-- trackback -->'

def load_entry_file(filename)
  header = []
  body = []
  tb = []
  File.open(filename) { |file|
    in_header = true
    in_tb = false
    file.each_line(chomp: true) { |line|
      if (line == TB_SECTION_SEPARATOR) then
        in_tb = true
        next
      elsif in_header && /^$/.match(line) then
        in_header = false
        next
      end
      if in_header then
        header.push(line)
      elsif in_tb then
        tb.push(line)
      else
        body.push(line)
      end
    }
  }
  return { :header => header, :body => body, :tb => tb }
end

def dump_entry_file(filename, entry)
  File.open(filename, "w") { |f|
    f.puts entry[:header]
    f.puts ""
    f.puts entry[:body]
    unless entry[:tb].empty? then
      f.puts TB_SECTION_SEPARATOR
      f.puts entry[:tb]
    end
  }
end

def convert_id_links(lines)
  in_super_pre = false
  new_lines = []
  lines.each { |line|
    if in_super_pre then
      new_lines.push(line)
      in_super_pre = false if line.match(/^||<$/)
      next
    end
    if !in_super_pre && line.match(/^>\|\w*\|$/) then
      new_lines.push(line)
      in_super_pre = true
      next
    end
    # id link
    new_line = line.gsub(/((\[\])|(href=['"]\[?)|[^:]|^)(id:([\w-]+):(\d{8})(:[\w]+|#[\w]+)?)(\[\])?/i) { |matched|
      pre, esc_begin, href, link, id, date, sect, esc_end = $~.captures
      unless (esc_begin && esc_end) || href
        url = "http://d.hatena.ne.jp/#{id}/#{date}"
        if sect then
          sect.gsub!(/^:/, "/")
          url += sect
        end
        "#{pre}<a href=\"#{url}\">#{link}</a>"
      else
        matched
      end
    }
    # href: diary
    new_line = new_line.gsub(/href=['"]\[?(d:)?id:([\w-]+)(:[^\]'"]+)?\]?['"]/i) { |matched|
      service, id ,path = $~.captures
      url = "http://d.hatena.ne.jp/#{id}"
      if path then
        path.scan(/^:((\d{8})$|(\d{8})(:[\w]+|#[\w]+)$)/) { |s,date1,date2,sect|
          if date1 then
            url += "/#{date1}"
          elsif date2 then
            sect.gsub!(/^:/, "/")
            url += "/#{date2}#{sect}"
          end
        }
      end
      "href=\"#{url}\""
    }
    # href: group
    new_line = new_line.gsub(/href=['"]\[?g:([\w-]+)(:id:([\w-]+)(:[^\]'"]+)?)?\]?['"]/i) { |matched|
      group, id_path, id, path = $~.captures
      url = "http://#{group}.g.hatena.ne.jp"
      if id then
        url += "/#{id}"
        if path then
          path.scan(/^:((\d{8})$|(\d{8})(:[\w]+|#[\w]+)$)/) { |s,date1,date2,sect|
            if date1 then
              url += "/#{date1}"
            elsif date2 then
              sect.gsub!(/^:/, "/")
              url += "/#{date2}#{sect}"
            end
          }
        end
      end
      "href=\"#{url}\""
    }
    # href: group keyword
    new_line = new_line.gsub(/href=['"]\[?g:([\w-]+):keyword:([^\]'']+)\]?['"]/i) { |matched|
      group, keyword = $~.captures
      url = "http://#{group}.g.hatena.ne.jp/keyword/#{keyword}"
      "href=\"#{url}\""
    }
    new_lines.push(new_line)
  }
  return new_lines
end

def print_diff(src_lines, dst_lines)
  src_lines.each_index { |i|
    if src_lines[i] != dst_lines[i] then
      puts "- #{src_lines[i]}"
      puts "+ #{dst_lines[i]}"
    end
  }
end

check = (ARGV[0] == "check")
Dir.glob("#{ORIG_DIR}/????-??-??_*.txt").sort.each { |src|
  dst = File.basename(src)
  entry = load_entry_file(src)
  body = entry[:body]
  new_body = convert_id_links(entry[:body])
  if (body != new_body) then
    puts "#{src}:"
    print_diff(body, new_body)
    unless check then
      entry[:body] = new_body
      dump_entry_file(dst, entry)
      puts "saved: #{dst}"
    end
  end
}

引数に check を指定するとエントリファイルを置換せずに置換する部分の差分表示だけして終わります。

hbw のエントリファイルのヘッダ部分を読み飛ばすようにしています。また、トラックバック以降作業後の作業ということで、以前の作業で追加したトラックバック部分も読み飛ばすようにしています。

基本的に自分の書き方でヒットするケースのみに対処しているので、人によってはこのままでは足りないかもしれません。またはてなダイアリーはてなグループ用のリンクにしか対応していません。

結果の確認

正規表現置換は思わぬ置換ミスを引き起こすことがあるので、上のスクリプトを実行後、diff を取って差分を目で一通り確認しました。350件分。2時間半くらいかかりました。

修正の実行

hbw を実行しておしまい。

やり残し

確認中に気付いたのですが、a タグの href 属性値に書いたisbn記法やasin記法もはてなブログでは展開してくれないんですね… これは別途対処しようと思います。

もう一つ、フラグメント識別子で日記内のセクションへリンクしている場合(http://d.hatena.ne.jp/rna/20190128#p1 等)、はてなブログに移行したダイアリーでリンク先のセクションに対応するエントリには飛ばないという問題があります。

これははてなの方に要望を投げているとことろです。対応してもらえるとありがたいのですが、ダメだった場合どうするか… 大抵の場合はURLを http://d.hatena.ne.jp/rna/20190128/p1 に置き換えてしまえばエントリに飛ぶんですが、なぜかそうならないブログもあるのです。

例えば上の記事で挙げた飯田さんのブログがそうです。おそらくダイアリーを日記モードで使っているとそうなるのかなとにらんでいますが、元がどっちモードだったかなんてわからないのでどうしたものか…

お詫び

最後に、今回の作業でまたidコールが飛びまくったかもしれません。もしそうでしたらお詫びします。

一応エントリの再編集では追加分のidにしかコールが飛ばないはずなのですが…

でも AtomPub からの編集でもそうなのか不明ですし、差分の取り方によっては追加分と認識されそうなので、やっぱり飛んでしまったかも…

人によっては10年以上も前のブログからコールが飛んできて不快かと思いますが、リンクはWebの命だと思っているのでリンク切れは極力なくしたいのです。どうかお許しください。