これはどんな話か
Webサイトデータのバックアップの重要性はいくら強調しても強調しすぎということはないのですが、とはいえ、何らかのアクシデントでデータが失われてしまうこともあるものです。
今回はそのようなアクシデントに見舞われた状態において、MTで書き出されたHTMLとメールのデータから、コメントデータをデータベースに復元した際の手順をご紹介します。
前提条件とスタートライン
前提条件は以下の通りです。
- 書き出されたHTMLは最新のデータのバックアップがある
- データベースのバックアップはあるものの、最新のデータではない
- 更新したブログ記事は、ローカルのPCにデータとして残されている
- コメントを受け付けた際に送信されるメールは全て Gmail に残っている
この状況からまず最初に、
- HTMLをバックアップデータから復元する
- データベースをバックアップデータから復元する
- ローカルのPCに残っていた投稿済みの記事のデータを、再度投稿する
という作業を行い、ここをコメントデータの復元のスタートラインとします。
復元手順
コメントデータの復元は以下のような手順で行いました。
- 書き出されたHTMLファイルからコメントデータを抜き出す
- 全てのブログ記事アーカイブのファイルを検査し、内容に「id="entry-xxx」が含まれていれば、それが「xxx」をIDにもつブログ記事のファイルだと判断する
- ファイルの中の「class="comment"」を抜き出し、コメントのデータの一覧を作成する
- コメントを受け付けた際に送信されるメールから、「投稿者名」「メールアドレス」「URL」「IPアドレス」のデータを抜き出し、一覧を作成する
- HTMLファイルから抜き出したコメントデータと、メールから抜き出した投稿者のメールアドレスなどのデータを突き合わせる
- (3)で突き合わせたデータをデータベースに保存する
プログラム
上記の復元手順を行うプログラムがこちらになります。書き捨てのスクリプトなので完全ではない部分もありますが、このプログラムで概ね期待通りの結果を得ることができました。
※ プログラムを動かすためには Ruby のセットアップが必要ですがその手順については割愛します
※ この記事はプログラムの動作結果を保証するものではありません。実行する場合には自己の責任において行ってください
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# -*- encoding: utf-8 -*- | |
require 'pp' | |
require 'json' | |
require 'date' | |
require 'nokogiri' | |
require 'mail' | |
require 'pit' | |
require 'active_record' | |
target_dir = ARGV.shift or raise 'Usage: ' + $0 + ' target-directory' | |
config = Pit.get('restore-comment-data-from-entry', :require => { | |
:gmail => { | |
:address => "imap.gmail.com", | |
:port => 993, | |
:user_name => 'username', | |
:password => 'password', | |
:enable_ssl => true | |
}, | |
:database => { | |
:adapter => 'mysql2', | |
:host => 'localhost', | |
:username => 'username', | |
:password => 'password', | |
:database => 'database', | |
}, | |
:threshold => '2011-1-1', | |
:adminname => 'MT User', | |
}) | |
users_file = './users.js' | |
users = {} | |
if File.exists? users_file | |
users = JSON.parse(open(users_file).read) | |
else | |
Mail.defaults do | |
retriever_method :imap, config[:gmail] | |
end | |
Mail.all(:delete_after_find => false).each do |email| | |
begin | |
body = email.body.to_s.encode("UTF-8", "ISO-2022-JP") | |
user = body.match(/^コメント投稿者: (.*)/)[1].sub(/\s*\z/, '') | |
next if user == '' | |
users[user] ||= {'name' => user} | |
users[user]['mail'] ||= body.match(/^メールアドレス: (.*)/)[1] | |
users[user]['url'] ||= body.match(/^URL: (.*)/)[1] | |
users[user]['ip'] ||= body.match(/^IPアドレス: (.*)/)[1] | |
rescue => e | |
# ignore | |
end | |
end | |
open(users_file, 'w').write(users.to_json) | |
end | |
comments = [] | |
Dir[File.join(target_dir, '**/**.html')].each do |f| | |
content = open(f).read | |
begin | |
m = content.match(/id="entry-(\d+)"/) | |
rescue => e | |
next | |
end | |
next unless m | |
doc = Nokogiri.HTML(content) | |
th = DateTime.new(*(config[:threshold].split(/\D+/).map(&:to_i))) | |
doc.search('.comment').each do |comment| | |
date = DateTime.new(*( | |
comment.search('.comment-footer a').text.split(/\D+/).map(&:to_i) | |
)) | |
next if date < th | |
name = comment | |
.search('.comment-header').text.sub(/\A\s*/, '').sub(/\s*:\s*$/, '') | |
u = users[name] || {} | |
comments << { | |
:comment_blog_id => 1, | |
:comment_entry_id => m[1], | |
:comment_author => name, | |
:comment_commenter_id => name == config[:adminname] ? 1 : nil, | |
:comment_email => u['mail'], | |
:comment_url => u['url'], | |
:comment_ip => u['ip'], | |
:comment_text => comment | |
.search('.comment-content > p') | |
.map{|p| p.inner_html.gsub(/<br\s*\/?>\n?/, "\n")} | |
.join("\n\n"), | |
:comment_modified_on => date.strftime("%Y-%m-%d %H:%M:%S"), | |
:comment_created_on => date.strftime("%Y-%m-%d %H:%M:%S") | |
} | |
end | |
end | |
comments.sort! {|a, b| a[:date] <=> b[:date]} | |
ActiveRecord::Base.establish_connection(config[:database]) | |
class Comment < ActiveRecord::Base | |
self.table_name = 'mt_comment' | |
end | |
raise 'Please comment out this line if you really want to save comments' | |
comments.each do |c| | |
c = Comment.new c | |
c.save or raise 'Can not save' | |
end |
結び
「バックアップ重要!」に尽きる話ではあるのですが、書き出されたファイルが残っていれば多くのデータが復元できたりもします。また今回のケースでは、書き出されていることにより復旧作業中もサイトを公開することができたので、その点でもファイルに助けられました。
コメントする