DesktopHEのインデックスを作る
はじめに(2007.10.21)
DesktopHE(Hyper Estraierを使ったデスクトップ検索ツール)を使ってハードディスク内のファイルの全文検索を行っている.全文検索ではGoogleデスクトップが有名でとても便利なのだが,常駐するのがなんとなく嫌なので今は使っていない.Googleデスクトップを使ってpcが遅くなったと言うわけではなく,常駐するのがただなんとなく嫌というだけである.
そこで,常駐しないDesktopHEを使ってみる事にした.常駐しないと言うことは,インデックスは手動で作ると言うことだ.DesktopHEを動かして,インデックスを作ってと言う作業をいちいち手で行うのは面倒なので,プログラムを作って定期的に動かすことにした.
「そんなことをするぐらいだったら,Googleデスクトップを使えば良いんじゃないか」と思わなくもないが,まあプログラムを作ることにしよう.
言語はrubyにした.流行っているからだ.cygwinで動かす.インデックス作成に使うコマンドやインデックスの格納先を記述する設定ファイルはYAMLで作ることにした.XMLよりマイナーだからだ.
環境は以下の通り.
- DesktopHE 1.7.0
- WindowsXP SP2+月次パッチたくさん
- cygwin版ruby 1.8.5
起動方法を決める(2007.11.4)
スケジューラで動かすつもりなのでコマンドラインで動けば良い.ファイル名(コマンド名)はEst.rb
とした.引数に実行したいサブコマンドを指定して起動する.サブコマンドはDesktopHEのインデックスの操作(「ファイル」メニューの下の4個のコマンド)と合わせ
- gather:インデックス開始
- purge:削除したファイルの情報を除去
- extkeys:キーワード抽出
- optimize:インデックス最適化
の4種類だ.例えば,
$ Est.rb gather
とすればインデックスを開始する.空白で区切って複数のサブコマンドを実行できるようにしよう.例えば,
$ Est.rb gather purge
とすればインデックスを作った後で削除したファイルの情報を除去する.
設定ファイルを作る(2007.11.18)
YAMLの書式で設定ファイルを作る.記述するのは以下の4項目.
- インデックス作成に使うコマンド
- インデックスを格納するディレクトリ
- 検索対象のディレクトリ
- 実行結果の入ったログ
“インデックス作成に使うコマンド”は以下の手順で確認できる.
- DesktopHEのメニューバーから“設定”を選ぶ.
- プルダウンメニューの“上級者向けインデックス設定”を選ぶ.
以下のダイアログが出る.
テキストボックスの中に書いてあるのがインデックス作成に使うコマンドだ.
続いて,“インデックスを格納するディレクトリ”と“検索対象のディレクトリ”は以下の手順で確認する.
- DesktopHEのメニューバーから“設定”を選ぶ.
- プルダウンメニューの“インデックス設定”を選ぶ.
以下のダイアログが出る.
“インデックスデータ格納フォルダ”と“インデックス対象フォルダ”を確認しよう.
実行結果を入れるログの設定はDesktopHEにはない.どこか適当なディレクトリとファイル名を決めておこう.
これらが決まったら,YAMLファイルを作成する.ファイル名はconf.yaml
とした.
- command
- インデックス作成に使うコマンド
- index
- インデックスデータ格納フォルダ
- dir
- 検索対象のディレクトリ
- log
- 実行結果を入れるログ
である.特にひねりもなく,そのまま書いた.
#conf.yaml
command:
gather:
text: estcmd gather -il ja -sd -cm -pc CP932 -lf 10
office: estcmd gather -fx .pdf,.rtf,.doc,.xls,.ppt T@estxfilt -fz -ic CP932 -pc CP932 -sd -cm
purge: estcmd purge -pc CP932
extkeys: estcmd extkeys -um
optimize: estcmd optimize
index: C:/Documents and Settings/foo/Application Data/DesktopHE/index
dir:
- C:/Documents and Settings/foo/デスクトップ
- C:/Documents and Settings/foo/My Documents
log:
path: C:/Documents and Settings/foo/デスクトップ
gather:
text: est-gather.log
office: est-gather.log
purge: est-purge.log
extkeys: est-extkeys.log
optimize: est-optimize.log
ディレクトリの区切文字はUNIX風(“/”)でもWindows/DOS風(“\”)でもどちらでも良い.ただし,末尾が“\”で終る場合(例えば“f:\”)はエスケープ(“f:\\”)しないと正しくパスとして認識しない.
初版(2007.12.2)
とりあえず作ってみた初版.
gather以外のサブコマンド(purge, extkeys, optimize)の実行はexecuteメソッドで統一できたが,gatherだけは別個に実装している.その点が不完全.rubyのオプション(-Ks)で文字コードをシフトJISにしている.コマンドの実行にはなくても良いが,コマンドラインに結果を出力する際に文字化けを防ぐことができる.
#!/bin/ruby -Ks
require 'yaml'
class Est
def initialize file
@CONST=YAML.load File.open(file)
end
def gather
@CONST['estcmd']['gather'].each_value do |cmd|
@CONST['dirs'].each do |dir|
log=%[>>"#{@CONST['log']['path']}/#{@CONST['log']['gather']}"] unless @CONST['log']['gather'].nil?
`#{cmd} "#{@CONST['index']}" "#{dir}">>"#{@CONST['log']['path']}/#{@CONST['log']['gather']}"`
end
end
end
def purge
execute 'purge'
end
def extkeys
execute 'extkeys'
end
def optimize
execute 'optimize'
end
def execute command
`#{@CONST['estcmd'][command]} "#{@CONST['index']}">>"#{@CONST['log']['path']}/#{@CONST['log'][command]}"`
end
end
METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize} # 1. 引数(サブコマンド)と実行するメソッドの対応
est=Est.new 'conf.yaml' # 2. 設定ファイルの読込み
ARGV.each do |arg|
est.send METHODS[arg] # 3. 引数に対応するメソッドの実行
end
1で引数(サブコマンド)とメソッドとの対応表を作っている.もし,サブコマンドだけではなくてオプション(例えば-g
, -p
, -e
, -o
)でもインデックスを操作する場合には
METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize,
'-g'=>:gahter,'-p'=>:purge,'-e'=>:extkeys,'-o'=>:optimize}
とすればOK.
2で設定ファイルconf.yaml
を引数としてEstクラスのインスタンスを作っている.その結果,インスタンスestのインスタンス変数@CONST
は以下のような値となる.
{"command"=>{"purge"=>"estcmd purge -pc CP932",
"optimize"=>"estcmd optimize",
"gather"=>{"text"=>"estcmd gather -il ja -sd -cm -pc CP932 -lf 10",
"office"=>"estcmd gather -fx .pdf,.rtf,.doc,.xls,.ppt T@estxfilt -fz -ic CP932 -pc CP932 -sd -cm"},
"extkeys"=>"estcmd extkeys -um"},
"dir"=>["C:/Documents and Settings/foo/デスクトップ",
"C:/Documents and Settings/foo/My Documents"],
"log"=>{"optimize"=>"est-optimize.log",
"gather"=>{"text"=>"est-gather.log",
"office"=>"est-gather.log"},
"extkeys"=>"est-extkeys.log"},
"index"=>"C:/Documents and Settings/foo/Application Data/DesktopHE/index"}
3で引数に指定したサブコマンドに対応するメソッドを呼ぶ.
purge/extkeys/optimizeは引数が同じ(HyperEstraierのコマンド,インデックスデータ格納フォルダ,ログ)なので,実行はexecuteメソッドに統合した.
gatherは他のコマンドより引数が多い(HyperEstraierのコマンド,インデックスデータ格納フォルダ,検索対象のディレクトリ,ログ).また,テキスト用とオフィス用でコマンドが別れているので,executeメソッドに統合できていない.
gatherもexecuteに統一版(2007.12.15)
purge/extkeys/optimizeメソッドと同様に,gatherメソッドでもexecuteメソッドを呼ぶことにした.検索対象ディレクトリをexecuteメソッドの引数に追加,この引数を使うのはgatherメソッドだけで,purge/extkeys/optimizeメソッドではnilを設定している.
#!/bin/ruby -Ks
require 'yaml'
class Est
def initialize file
@CONST=YAML.load File.open(file)
end
def gather
@CONST['estcmd']['gather'].each do |operation,cmd|
@CONST['dirs'].each do |dir|
execute cmd,dir,@CONST['log'][operation]
end
end
end
def purge
execute @CONST['estcmd']['purge'],nil,@CONST['log']['purge']
end
def extkeys
execute @CONST['estcmd']['extkeys'],nil,@CONST['log']['extkeys']
end
def optimize
execute @CONST['estcmd']['optimize'],nil,@CONST['log']['optimize']
end
def execute cmd,dir=nil,log=nil
exec=%[#{cmd} "#{@CONST['index']}"]
exec<<%[ "#{dir}"] unless dir.nil?
exec<<%[>>"#{@CONST['log']['path']}/#{log}"] unless log.nil?
`#{exec}`
end
end
METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}
est=Est.new 'conf.yaml'
ARGV.each do |arg|
est.send METHODS[arg]
end
古いログを消す(2007.12.30)
ログが残ったままなので,コマンド実行前に削除することにした.サブコマンドに対応したログを選び出すremoveLogメソッドとログを消すremoveメソッドを追加.
removeメソッドの中にあるハッシュproc
(ソース中の1)を説明しよう.このハッシュはremoveメソッドの引数の型と実行する処理を対応付けたものである.
removeメソッドの引数がハッシュだった場合は,まだログファイル自体にたどり着いていない.したがって,引数がStringになるまでremoveメソッドを再帰的に呼ぶ.removeメソッドの引数が文字列だった場合は,その文字列のファイルを削除する.
gahterメソッドの場合を考えてみよう.まず@conf['log']['gather']
を引数にremoveメソッドを呼ぶ.この時@conf['log']['gather']
には以下のハッシュが入っている.gahterではtext用とoffice用の二つのログを出力するためだ.
@conf['log']['gather']={"text"=>"est-gather.log","office"=>"est-gather.log"}
削除対象のログはハッシュの中にあるので,ハッシュの値を引数にremoveを呼ぶ.したがってこのハッシュの場合,キー"text"の値"est-gather.log"と,キー"office"の値"est-gather.log"を引数にremoveメソッドを呼ぶ.
キーの型がStringの場合はその値をログファイルの名前としてファイルを削除する.ログファイルが存在しないとエラーが発生するが,無視する.
ログの削除をインデックス操作の前に実施する(ソースファイルの2).
#!/bin/ruby -Ks
require 'yaml'
class Est
attr_accessor :conf,:log,:dir
def initialize file
self.conf=YAML.load File.open(file)
end
def gather
@conf['dir'].each do |dir|
execute @conf['command']['gather'],@conf['log']['gather'],dir
end
end
def purge
execute @conf['command']['purge'],@conf['log']['purge']
end
def extkeys
execute @conf['command']['extkeys'],@conf['log']['extkeys']
end
def optimize
execute @conf['command']['optimize'],@conf['log']['optimize']
end
def execute cmd,log=nil,dir=nil
if cmd.class==String
exec=%[#{cmd} "#{@conf['index']}"]
exec<<%[ "#{dir}"] unless dir.nil?
exec<<%[>>"#{(@conf['log']['path']||=".")}/#{log}"] unless log.nil?
`#{exec}`
else
cmd.each do |key,cmd|
execute cmd,dir,log[key]
end
end
end
def removeLog cmds
cmds.each do |cmd|
remove @conf['log'][cmd]
end
end
def remove log
proc={Hash=>Proc.new {|log| log.each_value{|log| remove log}},
String=>Proc.new {|log| File.delete log rescue nil}} # 1. 引数logの型と実行する処理の対応付け
proc[log.class].call log unless proc[log.class].nil?
end
end
METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}
ARGV.uniq!
incorrectArg=ARGV-METHODS.keys
abort <<END unless incorrectArg.empty?
incorrect argument(s): #{incorrectArg.join ','}
usage: Est.rb [gather|purge|extkeys|optimize]...
END
est=Est.new 'conf.yaml'
est.removeLog ARGV # 2. コマンドの実行前にログを削除
ARGV.each do |arg|
est.send METHODS[arg]
end
コマンドオブジェクト導入(2008.1.13)
conf.yamlを見てみると,HyperEstraierのコマンドもログと同様に階層構造を持っている.つまり,ハッシュの値がハッシュだったら複数のコマンドがあり,文字列だったらそれが実行するコマンドと言うことだ.そこで,コマンドもログと同様にデータ型によって実行するメソッドを決めることにしよう.そのために,コマンドオブジェクトを定義した.
コマンドオブジェクトのサブクラスとしてEstCommand(HyperEstraier用のコマンド)とRmCommand(ログファイル削除用のコマンド)を定義(ソースコード中の1と3).それぞれの中に引数のデータ型に対応した処理を記述してハッシュprocを用意した(ソースコード中の2と4).
例えばHyperEstraierのgatherコマンドを実行する場合,@conf['command']['gahter']
を引数にEstCommandのインスタンスを生成する(ソースコード中の5).conf['command']['gahter']には以下のハッシュが入っている.
@conf['log']['gather']={"text"=>"est-gather.log","office"=>"est-gather.log"}
ハッシュなので,各要素の値を引数にもう一度EstCommandのインスタンスを生成する.そうすると引数が文字列となり,文字列に該当するHyperEstraierのコマンドを実行する.
RmCommandも同様で,ハッシュなら値を引数に再度RmCommandのインスタンスを生成する.引数が文字列ならログファイルを削除する.
#!/bin/ruby -Ks
require 'yaml'
class Command
def execute
proc[conf['handler'].class].call
end
end
class EstCommand<Command # 1. HyperEstraier用のコマンド
attr_accessor :conf,:proc,:dir
def initialize conf
self.conf=conf
# 2. 引数confのキー"handler"の値の型によって実行する処理を記述.
self.proc={
Hash=>Proc.new {
conf['handler'].each do |key,command|
EstCommand.new('handler'=>command,
'index'=>conf['index'],
'dir'=>conf['dir'],
'log'=>{'file'=>conf['log']['file'][key],
'path'=>conf['log']['path']}).execute
end
},
String=>Proc.new {
exec=%[#{conf['handler']} "#{conf['index']}"]
exec<<%[ "#{conf['dir']}"] unless conf['dir'].nil?
exec<<%[>>"#{(conf['log']['path']||=".")}/#{conf['log']['file']}"] unless conf['log']['file'].nil?
`#{exec}`
}
}
end
def dir=dir
self.conf['dir']=dir
end
end
class RmCommand<Command # 1. ログ削除用のコマンド
attr_accessor :conf,:proc
def initialize conf
self.conf=conf
# 2. 引数confのキー"handler"の値の型によって実行する処理を記述.
self.proc={
Hash=>Proc.new {
conf['handler'].each_value do |log|
RmCommand.new('handler'=>log).execute
end
},
String=>Proc.new {
File.delete conf['handler'] rescue nil unless conf['handler'].nil?
},
NilClass=>Proc.new {nil}
}
end
end
class Est
attr_accessor :conf
def initialize file
self.conf=YAML.load File.open(file)
end
def gather
command=prepare 'gather'
conf['dir'].each do |dir|
command.dir=dir
command.execute
end
end
def purge
prepare('purge').execute
end
def extkeys
prepare('extkeys').execute
end
def optimize
prepare('optimize').execute
end
def prepare command
RmCommand.new('handler'=>conf['log'][command]).execute
EstCommand.new 'handler'=>conf['command'][command],
'index'=>conf['index'],
'log'=>{'file'=>conf['log'][command],'path'=>conf['log']['path']}
# 5. EstCommandのインスタンス生成
end
end
METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}
ARGV.uniq!
incorrectArg=ARGV-METHODS.keys
abort <<END unless incorrectArg.empty?
incorrect argument(s): #{incorrectArg.join ','}
usage: Est.rb [gather|purge|extkeys|optimize]...
END
est=Est.new 'conf.yaml'
ARGV.each do |arg|
est.send METHODS[arg]
end