RubyでUTF8とXML書き出し

アップデート:RSSからTwitterに自動投稿をしてくれるTwitterfeedなどのサービスがありますが、これらは下記の2.の「XMLはUTF8をそのままに残す」というのができなくて文字化けを発生させているようです。”&#26085″などの記号はウェブブラウザだと正しく変換して画面に表示してくれますが、ほとんどのTwitterクライアントではこの変換をやらないためです。

丸一日、これで悩んでいました。なんとか解決したので、ここに記録します。

やりたかったこと

  1. UTF8化したデータをXMLに書き出す。
  2. XMLファイルはUTF8をそのままに残す。例えば”日本語” => “日&amp#26412;&amp#35486;”という変換はしない。
  3. 大きいXMLファイルを書き出したいので、XMLをすべてメモリに溜め込んでから書き出すのではなく、少しずつファイルに書き出す。

そんなに珍しいことをやろうという訳でもないので、簡単にできるかなと思ったのですが、これがなかなか大変でした。Ruby 1.9ではもう少し簡単になっているかもしれませんが、少なくともRuby 1.8では大変です。

その1:Ruby標準ライブラリのREXMLを使うという選択肢

採用せず

  1. まず、REXMLはバグが多い。例えばXMLをインデント整形するだけでバグ。
  2. 少しずつファイル書き出しはできない。

その2:Ruby on RailsのActiveSupportについてくるBuilder (version 2.1.2)

採用せず

  1. ファイルを少しずつ書き出すことができるのは大きなプラス。
  2. しかし”日本語” => “日&amp#26412;&amp#35486;”は起きる。

その3:Nokogiri

採用せず

  1. “日本語” => “日&amp#26412;&amp#35486;”は起きないというのは大きなプラス。
  2. しかしファイルを少しずつ書き出すことはできない。

その4:Builderの最新バージョン (Githubにある version 2.2.0以上)

採用。以下の感じで使いました。

$KCODE = 'UTF8'
require 'rubygems'
gem 'bigfleet-builder'
require 'builder'
x = Builder::XmlMarkup.new(File.open("output_file.xml", "w"), :indent => 1)
x.instruct!(:xml, :encoding => "UTF-8")
1000.times do
  x.product do
    x.name("日本語")
  end
end

Railsでは処理速度を向上させるために、fast_xs gemがインストールされていればこれを読み込んでBuilderをパッチしています。しかしバージョン 2.2.0のBuilderはXmlBase#_escape内で、String#to_xsを引数付きで呼び出しているので、引数を取らないfast_xsのto_xsとコンパチではなくなっている感じです。

例えば

$KCODE = 'UTF8'
require 'rubygems'
gem 'bigfleet-builder'
require 'builder'
require 'active_support'
x = Builder::XmlMarkup.new(File.open("output_file.xml", "w"), :indent => 1)
x.instruct!(:xml, :encoding => "UTF-8")
1000.times do
  x.product do
    x.name("日本語")
  end
end

とすると、ArgumentError: wrong number of arguments (1 for 0) : method to_xs in xmlbase.rb at line 118と怒られます。

これを解消するためには fast_xs を使わないようにmonkey patchします。

$KCODE = 'UTF8'
require 'rubygems'
gem 'bigfleet-builder'
require 'builder'
require 'active_support'

class String
  alias_method :to_xs, :original_xs if method_defined?(:original_xs)
end

x = Builder::XmlMarkup.new(File.open("output_file.xml", "w"), :indent => 1)
x.instruct!(:xml, :encoding => "UTF-8")
1000.times do
  x.product do
    x.name("日本語")
  end
end

かなり美しくないのですが、これでようやくなんとかXMLがやりたいように書き出せるようになりました。