はじめに
大量のパワーポイント資産がある時、「あれ?この話どこでしたっけ?」とか「あの話をしたスライドどれだっけ?」と悩むことが多い。そんな時のためにpptxファイル内をgrepするRubyスクリプトを書いた。これはこれで便利だったのだが、手抜き処理が多かったために動作が遅く、大量のpptxファイルの検索には向かなかった。Rubyスクリプトのまま高速化することもできるのだろうが、勉強を兼ねて別の言語で書き直したい。というわけでD言語版を作った。
ファイルは
https://github.com/kaityo256/pptxgrep
においてある。
自分で言うのもなんだけどわりと便利なので、pptx資産が大量にある人は是非使って欲しい。
使い方
とりあえずコンパイルして、aliasなりPATHの通ったところに置くなりする。
$ dmd pptxgrep.d
$ alias pptxgrep=$PWD/pptxgrep # or put them somewhere in $PATH
実行する。引数は検索したいキーワードのみ。実行したディレクトリから再帰的にサブディレクトリを探しに行き、見つけた*.pptxファイルの中で、キーワードを含むスライド番号を出力する。
$ pptxgrep keyword
Found "keyword" in hoge/hoge.pptx at slide 4
Found "keyword" in hoge/hoge.pptx at slide 1
Found "keyword" in test.pptx at slide 3
なお、ファイルのパスについてはソートされているが、出力されるスライド番号についてはソートされないため、番号が前後することがある。
動作原理
PPTXファイルはzip圧縮されたXMLファイルなので、unzipして出てきたXMLファイル内を検索すればよろしい。というわけで以下のような処理ができれば良い。
- カレントディレクトリ以下の*.pptxを再帰的に検索する
- pptxファイルを見つけたらその中身を調べる
-
ppt/slides/slide([0-9]+).xml
にマッチするファイルがあればそれをメモリに展開する - 展開したデータを文字列として解釈し、検索ワードを含んでいたら、ファイル名とスライド番号を出力する
カレントディレクトリからファイルを再帰的に検索する
std.file.dirEntries
という、そのものずばりの関数があるのでそれを使う。
auto cwd = std.file.getcwd();
auto d = dirEntries(cwd,"*.pptx",SpanMode.depth);
カレントディレクトリをstd.file.getcwd()
で取得し、それをstd.file.dirEntries
にわたす。検索方法はSpanMode
で指定するが、あとでソートするのでなんでも良い。
これで得られるファイルはソートされていないので、string []
に変換してソートする。
string [] files;
foreach(string filename; d){
files ~= filename;
}
files.sort!();
map
とかで一発で書けるんだろうけど気にしない。これでサブディレクトリ以下にある全ての*.pptxファイルを文字列配列として得ることができた。
pptxファイルの中身を調べる
pptxはzipファイルなので、その内容物を調べるのにstd.zip.ZipArchive
を使う。
auto zip = new ZipArchive(read(filename));
zipファイルの中のppt/slides/slide([0-9]+).xml
にマッチするファイルがスライドなので、それを探す。
foreach (name, am; zip.directory)
{
foreach(m; match(name, r"ppt/slides/slide([0-9]+).xml$"))
{
// スライドファイルを見つけた
}
}
ちなみに、カッコで囲んだ部分がスライド番号なので、あとで使う。
スライドファイルを展開し、文字列に変換する
ZIPファイル名のエントリーはArchiveMember
として受け取れる。これを個別にメモリ上でunzipするには、ZipArchive.expand
を使えば良い。
zip.expand(am);
すると、expandedData
メンバに展開される。これはubyte []
なので、これを文字列に変換する。
char *cstr = cast(char*)am.expandedData;
auto len = am.expandedData.length;
string str = cast(string) cstr[0..len];
これでstr
にスライド一枚のXMLファイルがテキスト形式で格納された。
XMLからテキストを抽出
XMLファイルがテキスト形式でstr
に格納されたので、あとはmatch
なりindexOf
なりでキーワードを含むか調べたくなるが、前に書いたとおり、スライド上では一続きの文字列に見えても、XMLではバラバラに格納されていることがある。例えば「平成30年」という言葉は、「平成」「30」「年」でバラバラになっており、このままだと「平成30年」でヒットしない。そのためにXMLのテキストノードをすべて抽出し、結合した文字列に対して検索をかける必要がある。
素直にD言語標準のXMLのパーサーstd.xml
を使うとこんな感じに書けるだろう。
auto xml = new DocumentParser(str);
dstring text;
xml.onText = (string s)
{
text ~= s.to!dstring;
};
xml.parse();
これはDocumentParser
に「テキストノードを見つけた時」のイベントハンドラを登録しておいて、xml.parse()
を呼び出すと、後は見つけるたびにテキストが追加されていく仕組み。こうして得られたtext
に対してindexOf
をかければ良いのだが、残念ながらこのコードは遅い。おそらくstd.xml
のパーサーが遅いのだと思われる。仕方ないので、テキストノードを抽出するコードを自分で書こう。
テキストは<a:t>
と</a:t>
に囲まれているので、それを抽出すれば良い。素直に書けばこんな感じになるだろうか。
dstring extractText(string xmltext)
{
dstring dxml = xmltext.to!dstring();
dstring text;
while(findSkip(dxml, "<a:t>")){
auto e = indexOf(dxml,"</a:t>");
text ~= dxml[0..e];
}
return text;
}
あとはこいつを
dstring text = extractText(str);
と呼び出せば、text
にテキストノードが全て結合されたものが入る。
スライドに検索ワードが含まれていたらファイル名とスライド番号を出力
if(text.indexOf(keyword) !=-1)
{
auto rname = relativePath(filename);
writefln("Found \"%s\" in %s at slide %s",keyword,rname, slidenum);
}
そのままなので難しいところは無いと思う。ただ、ファイル名が絶対パスになっているのが不便だったので、relativePath
でカレントディレクトリからの相対パスを出力するようにしている。
速度
自分のすべてのスライド資産にたいして、キーワード「hoge」を検索するのにかかった時間をRuby版と比較してみる。何度か実行して、ファイル情報がキャッシュにのった状態で測定。
$ time ruby ~/github/grep_pptx/grep_pptx.rb hoge > /dev/null
ruby ~/github/grep_pptx/grep_pptx.rb hoge > /dev/null 80.60s user 6.74s system 96% cpu 1:30.16 total
$ time ~/github/pptxgrep/pptxgrep hoge > /dev/null
~/github/pptxgrep/pptxgrep hoge > /dev/null 2.13s user 0.62s system 92% cpu 2.974 total
Ruby版が90秒、D言語版は3秒ということで、30倍くらい早くなった。なお、Ruby版は
- unzipをシェルで呼び出している
- 必要ないファイルもすべて展開し、ファイルに吐いている
- XPathを使ったXML解析をしている
というハンデがあるので、これはフェアな比較になっていないことに注意。
ちなみにD言語版でも、XMLでパースした場合は
$ time ~/github/pptxgrep/pptxgrep hoge > /dev/null
~/github/pptxgrep/pptxgrep hoge > /dev/null 19.15s user 0.85s system 95% cpu 20.937 total
と、自前パース版に比べて7倍くらい遅くなる。
まとめ
D言語でpptx内テキストのgrepコマンドを作った。数十行足らずでこれだけのことができるんだから大したもんだと思う。ただし、std.xml
が遅いのはちょっと困る。これは開発コミュニティも把握しているっぽいが、std.xml2
はAbandonedになってますね・・・