とっても強力な道具

2004/1/4 牧野


元ネタは ここの Ruby プログラム。さして長いわけではないので全体を再現しておく。

プログラムの終わりに移動

#!/usr/bin/ruby

## <URL:http://www.t-doi.org/linux/autopic.html>を勝手に拝借させていただき
## 自分用に改造。
## == Usage
##   ./mailparse.rb < aMail
## == たぶん必要な環境 (deb package)
## * Ruby (ruby 1.6.7-3)
## * Uconv (libuconv-ruby 0.4.9-1)
## * REXML (librexml-ruby 1.2.5-1)
## * TMail (libtmail-ruby 0.10.0-1)

$SAVE_DIR = `echo $HOME`.chomp + '/public_html/mp'

require 'uconv'
require 'rexml/document'
require 'time'
require 'ftools'
require 'kconv'
require 'tmail'
require 'base64'

logfile = $SAVE_DIR + "/photos.xml"
doc = nil
if File.exist? logfile
  File.open(logfile) {|f|
    doc = REXML::Document.new(Uconv::euctou8(f.read))
  }
else
  doc = REXML::Document.new "<photolist></photolist>"
  doc << REXML::XMLDecl.new
  doc.xml_decl.encoding = 'euc-jp'
end

class PhotoMail
  attr_accessor :text, :filename, :origname
  attr_reader :pdate
  def initialize(date, subject)
    @mdate = date
    if(%r|\[pic\:(\d\d\-\d\d\-\d\d)\_(\d\d)\-(\d\d)\]|.match(subject))
      @pdate = Time.parse("#$1 #$2:#$3")
    end
  end
  def savefile(data, basedir=".")
    File.mkpath(File.dirname("#{basedir}#@filename"))
    file = File.open("#{basedir}#@filename",'w+')
    file.chmod(0644)
    file.write(data)
    file.close()
  end
  def to_xml
    xml = REXML::Element.new("photo")
    xml.attributes["maildate"] = @mdate.xmlschema
    if @pdate then xml.attributes["photodate"] = @pdate.xmlschema end
    xml.attributes["origname"] = @origname
    if @filename
      xml.attributes["filename"] = ".#@filename"
      xml.attributes["id"] = "mp#{@filename.gsub(%r|/|, '')}"
    end
    xml.add_text (Uconv::euctou8(Kconv.toeuc(@text)))
    xml
  end
end

mailstring = ''
while (line = gets())
  mailstring += line
end

mail = TMail::Mail.parse(mailstring)

if mail.multipart? then
  pm = PhotoMail.new(mail.date, mail.subject)

  # ここにメールを処理する部分が入るが省略。

  doc.root.add_element(pm.to_xml)
  # 実行する度にphoto要素内の文字列の頭と末尾に改行が増えていってしまうので、
  # 美しくないけれどその処理をしておく……。
  doc.elements.each("photolist/photo") {|e| e.text.strip!}
  # 実行する度にphoto要素間に2つくらいずつ改行が増えていってしまうので、
  # 美しくないけれどその処理をしておく……。
  doc.elements.each("photolist") do |child|
    child.delete_if {|grandchild| grandchild.class==REXML::Text}
  end
  File.open(logfile,'w+') {|f| f.print "#{Uconv::u8toeuc(doc.to_s)}\n"}
end
このプログラムを見てとりあえず疑問に思ってほしいことは、、、、一杯ある。

プログラムの中身がどうという以前に、プログラム開発とデバッグの方法が基 本的に理解されていないように見えるのがまずは問題である。つまり、

スクリプトの中ではいじられていないデータ (すなわち読み込まれて何もされず に書き出されるデータ) にまで被害が及ぶので、 バグの原因はUconvまたは REXMLの一方もしくは両方 (つまり複合的な要因) にあると 思われる。
とかいって人のせいにする前に、どちらのせいかくらいは調べてほしい。これ を調べるのは別になにも難しいことではない。単に、 Uconv を使わないよう にプログラムを変更して、それでも同じ問題が起きるなら REXML (またはその 使いかた)のせいであり、そうでなければ Uconv に少なくとも原因の一部はあ るということになる。

さらに、「どこでおかしくなったか」も重要な情報である。つまり、このプロ グラムの場合なら、とりあえず読み込んで要素を追加する前にすぐに書いてみ て、それでも結果がおかしいなら REXML 自体の読み込みあるいは書き出しの メソッドに問題がある可能性があり、そうでなければ自分のプログラムが結果 を破壊しているわけである。 実際、 ruby 1.8.1 と適当なバージョンの rexml 等で上のスクリプトを適当 に補間したもの、具体的には

  # ここにメールを処理する部分が入るが省略。
の後に

  pm.text = ""
  
  mail.parts.each do |m|
    if m.main_type == 'text'
      pm.text += m.body
    else
      pm.filename =  File.basename(m.disposition_param('filename').gsub(/\\/, '/'))
      pm.savefile(decode64(m.body), $SAVE_DIR);
      
    end
  end
だけを追加したものを動かしてみると、(最後の妙な処理を消しても))何もおかしいことは起きない。このプログラムの作者の環境ではそれでも破壊されているという可能性はあるが、若干疑問な気がしてくるであろう。

もっとも、妙なことは起きない代わりに、

<?xml version='1.0'?><photolist><photo id='mpfoo.jpg' filename='.foo.jpg' maildate='2003-01-11T20:08:02+09:00'>sample text
</photo></photolist>
という具合に text 部分以外に全く改行がない XML ファイルが生成される。 これは DOM ライクな XML 処理エンジンの動作としては全く正当なものである が、人が読みやすいものではない。従って、元のプログラムではなんらかの形 で読みやすくするための処理をしていて、そのために書き出すたびに余計に改 行が増えているのではないかと想像される。

この辺りの、デバッグの方法の極めて基本的な部分については、「達人プログ ラマー」3章18節に簡潔にまとめられている。これは良い本なので読まないと 損である。

まあ、そんなことはともかく、問題はこのプログラムの構成原理である。目的 が、

<?xml version='1.0'?>
<photolist>
<photo id='mpfoo.jpg' filename='.foo.jpg' maildate='2003-01-11T20:08:02+09:00'>
sample text
</photo>
</photolist>
というファイルにエントリーを追加したいということであれば、「そういうプ ログラム」を書くことが自然であろう。しかも、このファイル自体が自分自身 の出力なので、XML をパースして DOM ライクなツリー構造を得る必要は全く ない。要するに、ファイルの最後にエントリーを追加すればいいのだから、
f = open(filename, "a")
f.print "<photo id=mp# "+@filename.gsub(%r|/|, '') + ...
というようなことをすればいいだけである。

もっとも、これでは </photolist> の後に要素が追加されてしまって、正し い XML ファイルにならない。これを回避するには、

  1. まず、最後の </photolist> の手前までをコピーした一時ファイルを 作っておき、それに追加した後で</photolist>を書いて一時ファイルを元 のファイル名にリネームする。
  2. xml ファイルの他に、最後の</photolist>だけを書いてないファイ ルを別に作っておき、 xml ファイルはそれをコピーして最後に </photolist>を追加して作る
  3. XML を使うのはあほらしいのでやめる
など、いくつかの対応が考えられる。 最初の案(必要以上に複雑であまり好ましいとは思えないが)の場合、おおむね 以下のような処理になろう。
#!/usr/bin/ruby

$SAVE_DIR = ENV["HOME"] + '/public_html/mp'

require 'time'
require 'tmail'
require 'base64'

logfile = $SAVE_DIR + "/photos.xml"
outfilename = $SAVE_DIR + "/photos" + Process.pid.to_s
outfile = open(outfilename, "w+")

if File.exist? logfile
  infile=File.open(logfile) 
  while s=infile.gets
    outfile.print s unless s =~ /<\/photolist>/
  end
else
  outfile.print "<?xml version='1.0' encoding='euc-jp'?>\n<photolist>"
end

mail = TMail::Mail.parse(gets(nil))

if mail.multipart? then
  text = ""
  mail.parts.each do |m|
    if m.main_type == 'text'
      text += m.body
    else
      mdate = mail.date
      if(%r|\[pic\:(\d\d\-\d\d\-\d\d)\_(\d\d)\-(\d\d)\]|.match(mail.subject))
	pdate = Time.parse("#$1 #$2:#$3")
      else
	pdate=nil
      end
      filename =  File.basename(m.disposition_param('filename').gsub(/\\/, '/'))
      file = File.open($SAVE_DIR + "/"+filename,'w+')
      file.chmod(0644)
      file.write(m.body)
      file.close()      
      outfile.print "<photo " 
      # ここで色々書く
      outfile.print "maildate='",mail.date.xmlschema,"'>\n"
      outfile.print text, "\n</photo>\n"
    end
  end
end  
outfile.print  "</photolist>\n"
File.rename(logfile, logfile + ".bak");
File.rename(outfilename, logfile);
細かいことだが、環境変数 HOME の値は ENV["HOME"] で得られることや、改行ごと入 力全部を一気に読むには単に gets(nil) でいいこと等は知っていてもばちは当たら ないと思う。

ここでの主張は、

ということである。元のプログラムは というようなことをしている(らしい)。これに対して、最終版のプログラムで は というふうになる。余計なことをしないことでプログラムが簡潔で明快になっ ている。とりあえず長さは随分短くなるし、妙な改行が入るとかいった問題は 起こりえない。

まあ、元のプログラムの作者の意図は REXML の勉強であって、そのためにわ ざわざややこしいことをしているということなのかもしれないが、クラスの使 い方を見る限りそれは疑わしい。(意図はそうかもしれないが勉強になっているかどうかが疑わしい) 例えばこのプログラムで定義されている PhotoMail なるクラスは、「何のためにそのクラスを使うのか」ということを 全く考えることなく作られているように見える。例えば、 @mdate や @pdate といった変数は、(ここで見る限り) XML 属性に書くのに一度だけつかわれるだけであ る。ならば、

  xml.attributes["maildate"] = m.date.xmlschema
といった具合に、 Mail オブジェクトから Element オブジェクトに直接渡す べきであろう。

極めて善意に解釈するなら、中間クラスを使うことで XML 以外の形式にも対 応するというような意図があるのかもしれない。しかし、それなら、 PhotoMail に Mail オブジェクトを受け取って処理するというメソッドがある べきであろう。つまり、トップレベルでは

mail = TMail::Mail.parse(gets(nil))
pm = PhotoMail.new(mail)
if xml = pm.to_xml then doc.root.add_element(xml)
というような形になっていて欲しいところである。 text、 filename、 origname 等の、外から読み書きする必要が全くなさそうなイン スタンス変数が読み書きできるようになっている辺りも、クラスによって処理 の独立性を高めるという機能をクラスが果たしていないことを表している。

つまり、いいたいことは、

ということである。もうちょっと一般的にいうと、このプログラムでは、 Ruby 言語の強力な機能や、 REXML という極めて汎用性の高い有用なツールが、 なんだかかわいそうな使われかたをしている。それは何故か?というのが問題 なわけだが、やはり、技倆というか知識というか応用する能力というかそうい うものがまだ不足であるからではないかと思われる。強力な道具を使えば誰で も強力になれるわけではなく、それを使いこなすにはそれなりの用意やら練習 やらがいるわけである。

といっても、自分では一生懸命練習しているつもりなのにそれが能力を伸ばす ことになっていないというケースが往々にしてあるわけで、何故そうなるかが 真の問題である。独学ではできないかといえば、できている人はいくらでもい るわけではあるが、しかし実際に独学でできていない人も結構いる。その違い はどこからくるのか?というのはなんだかとても難しい問題ではある。


2003/1/5 追記 ここに本人のコメントがある。 保存版

なかなか反応は素直ですが、問題は実践につながるかどうかですね。