├── .gitignore ├── LICENSE ├── README.md ├── pptxgrep.d └── qiita.md /.gitignore: -------------------------------------------------------------------------------- 1 | pptxgrep 2 | pptxgrep.o 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 H. Watanabe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grep keyword in Microsoft Powerpoint (pptx) files 2 | 3 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 4 | 5 | ## Summary 6 | 7 | Find keywords in pptx files. It will search pptx files recursively from the current working directory. 8 | 9 | Descriptions in Japanese are available [here](qiita.md). 10 | 11 | ## Build 12 | 13 | dmd pptxgrep.d 14 | alias pptxgrep=$PWD/pptxgrep # or put them somewhere in $PATH 15 | 16 | ## Usage 17 | 18 | $ pptxgrep keyword 19 | Found "keyword" in hoge/hoge.pptx at slide 4 20 | Found "keyword" in hoge/hoge.pptx at slide 1 21 | Found "keyword" in test.pptx at slide 3 22 | -------------------------------------------------------------------------------- /pptxgrep.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.file; 3 | import std.regex; 4 | import std.zip; 5 | import std.string; 6 | import std.algorithm; 7 | import std.path; 8 | import std.xml; 9 | import std.conv; 10 | 11 | dstring extractText(string xmltext) 12 | { 13 | dstring dxml = xmltext.to!dstring(); 14 | dstring text; 15 | while(findSkip(dxml, "")){ 16 | auto e = indexOf(dxml,""); 17 | text ~= dxml[0..e]; 18 | } 19 | return text; 20 | } 21 | 22 | void search(string keyword, string filename) 23 | { 24 | ZipArchive zip; 25 | try{ 26 | zip = new ZipArchive(read(filename)); 27 | }catch(ZipException){ 28 | return; 29 | } 30 | foreach (name, am; zip.directory) 31 | { 32 | foreach(m; match(name, r"ppt/slides/slide([0-9]+).xml$")) 33 | { 34 | zip.expand(am); 35 | auto slidenum = m.captures[1]; 36 | char *cstr = cast(char*)am.expandedData; 37 | auto len = am.expandedData.length; 38 | string str = cast(string) cstr[0..len]; 39 | // This is XML version. It is too slow. 40 | /* 41 | auto xml = new DocumentParser(str); 42 | dstring text; 43 | xml.onText = (string s) 44 | { 45 | text ~= s.to!dstring; 46 | }; 47 | xml.parse(); 48 | */ 49 | dstring text = extractText(str); 50 | if(text.indexOf(keyword) !=-1) 51 | { 52 | auto rname = relativePath(filename); 53 | writefln("Found \"%s\" in %s at slide %s",keyword,rname, slidenum); 54 | } 55 | } 56 | } 57 | } 58 | 59 | void main(string[] args) 60 | { 61 | if(args.length <2) 62 | { 63 | writeln("Usage:"); 64 | writeln(" dgrep_pptx keyword"); 65 | return; 66 | } 67 | auto keyword = args[1]; 68 | auto cwd = std.file.getcwd(); 69 | auto d = dirEntries(cwd,"*.pptx",SpanMode.depth); 70 | string [] files; 71 | foreach(string filename; d){ 72 | files ~= filename; 73 | } 74 | files.sort!(); 75 | foreach(string filename; files){ 76 | search(keyword, filename); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /qiita.md: -------------------------------------------------------------------------------- 1 | # パワーポイント内のテキストをgrepする(D言語版) 2 | 3 | これは[Qiita](https://qiita.com/kaityo256/items/ab093cf5534752886ff8)に書いた記事を転載したものです。 4 | 5 | ## はじめに 6 | 7 | 大量のパワーポイント資産がある時、「あれ?この話どこでしたっけ?」とか「あの話をしたスライドどれだっけ?」と悩むことが多い。そんな時のために[pptxファイル内をgrepするRubyスクリプト](https://qiita.com/kaityo256/items/2977d53e70bbffd4d601)を書いた。これはこれで便利だったのだが、手抜き処理が多かったために動作が遅く、大量のpptxファイルの検索には向かなかった。Rubyスクリプトのまま高速化することもできるのだろうが、勉強を兼ねて別の言語で書き直したい。というわけでD言語版を作った。 8 | 9 | ファイルは 10 | https://github.com/kaityo256/pptxgrep 11 | においてある。 12 | 13 | 自分で言うのもなんだけどわりと便利なので、pptx資産が大量にある人は是非使って欲しい。 14 | 15 | ## 使い方 16 | 17 | とりあえずコンパイルして、aliasなりPATHの通ったところに置くなりする。 18 | 19 | ```shell 20 | $ dmd pptxgrep.d 21 | $ alias pptxgrep=$PWD/pptxgrep # or put them somewhere in $PATH 22 | ``` 23 | 24 | 実行する。引数は検索したいキーワードのみ。実行したディレクトリから再帰的にサブディレクトリを探しに行き、見つけた*.pptxファイルの中で、キーワードを含むスライド番号を出力する。 25 | 26 | ```shell 27 | $ pptxgrep keyword 28 | Found "keyword" in hoge/hoge.pptx at slide 4 29 | Found "keyword" in hoge/hoge.pptx at slide 1 30 | Found "keyword" in test.pptx at slide 3 31 | ``` 32 | 33 | なお、ファイルのパスについてはソートされているが、出力されるスライド番号についてはソートされないため、番号が前後することがある。 34 | 35 | ## 動作原理 36 | 37 | PPTXファイルはzip圧縮されたXMLファイルなので、unzipして出てきたXMLファイル内を検索すればよろしい。というわけで以下のような処理ができれば良い。 38 | 39 | 1. カレントディレクトリ以下の*.pptxを再帰的に検索する 40 | 1. pptxファイルを見つけたらその中身を調べる 41 | 1. `ppt/slides/slide([0-9]+).xml`にマッチするファイルがあればそれをメモリに展開する 42 | 1. 展開したデータを文字列として解釈し、検索ワードを含んでいたら、ファイル名とスライド番号を出力する 43 | 44 | 45 | ### カレントディレクトリからファイルを再帰的に検索する 46 | 47 | `std.file.dirEntries`という、そのものずばりの関数があるのでそれを使う。 48 | 49 | ```d 50 | auto cwd = std.file.getcwd(); 51 | auto d = dirEntries(cwd,"*.pptx",SpanMode.depth); 52 | ``` 53 | 54 | カレントディレクトリを`std.file.getcwd()`で取得し、それを`std.file.dirEntries`にわたす。検索方法は`SpanMode`で指定するが、あとでソートするのでなんでも良い。 55 | 56 | これで得られるファイルはソートされていないので、`string []`に変換してソートする。 57 | 58 | ```d 59 | string [] files; 60 | foreach(string filename; d){ 61 | files ~= filename; 62 | } 63 | files.sort!(); 64 | ``` 65 | 66 | `map`とかで一発で書けるんだろうけど気にしない。これでサブディレクトリ以下にある全ての*.pptxファイルを文字列配列として得ることができた。 67 | 68 | ### pptxファイルの中身を調べる 69 | 70 | pptxはzipファイルなので、その内容物を調べるのに`std.zip.ZipArchive`を使う。 71 | 72 | ```d 73 | auto zip = new ZipArchive(read(filename)); 74 | ``` 75 | 76 | zipファイルの中の`ppt/slides/slide([0-9]+).xml`にマッチするファイルがスライドなので、それを探す。 77 | 78 | ```d 79 | foreach (name, am; zip.directory) 80 | { 81 | foreach(m; match(name, r"ppt/slides/slide([0-9]+).xml$")) 82 | { 83 | // スライドファイルを見つけた 84 | } 85 | } 86 | ``` 87 | 88 | ちなみに、カッコで囲んだ部分がスライド番号なので、あとで使う。 89 | 90 | ### スライドファイルを展開し、文字列に変換する 91 | 92 | ZIPファイル名のエントリーは`ArchiveMember`として受け取れる。これを個別にメモリ上でunzipするには、`ZipArchive.expand`を使えば良い。 93 | 94 | ```d 95 | zip.expand(am); 96 | ``` 97 | 98 | すると、`expandedData`メンバに展開される。これは`ubyte []`なので、これを文字列に変換する。 99 | 100 | ```d 101 | char *cstr = cast(char*)am.expandedData; 102 | auto len = am.expandedData.length; 103 | string str = cast(string) cstr[0..len]; 104 | ``` 105 | 106 | これで`str`にスライド一枚のXMLファイルがテキスト形式で格納された。 107 | 108 | ### XMLからテキストを抽出 109 | 110 | XMLファイルがテキスト形式で`str`に格納されたので、あとは`match`なり`indexOf`なりでキーワードを含むか調べたくなるが、[前に書いた](https://qiita.com/kaityo256/items/2977d53e70bbffd4d601#%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%AE%E6%8E%A2%E3%81%97%E6%96%B9)とおり、スライド上では一続きの文字列に見えても、XMLではバラバラに格納されていることがある。例えば「平成30年」という言葉は、「平成」「30」「年」でバラバラになっており、このままだと「平成30年」でヒットしない。そのためにXMLのテキストノードをすべて抽出し、結合した文字列に対して検索をかける必要がある。 111 | 112 | 素直にD言語標準のXMLのパーサー`std.xml`を使うとこんな感じに書けるだろう。 113 | 114 | ```d 115 | auto xml = new DocumentParser(str); 116 | dstring text; 117 | xml.onText = (string s) 118 | { 119 | text ~= s.to!dstring; 120 | }; 121 | xml.parse(); 122 | ``` 123 | 124 | これは`DocumentParser`に「テキストノードを見つけた時」のイベントハンドラを登録しておいて、`xml.parse()`を呼び出すと、後は見つけるたびにテキストが追加されていく仕組み。こうして得られた`text`に対して`indexOf`をかければ良いのだが、残念ながらこのコードは遅い。おそらく`std.xml`のパーサーが遅いのだと思われる。仕方ないので、テキストノードを抽出するコードを自分で書こう。 125 | 126 | テキストは``と``に囲まれているので、それを抽出すれば良い。素直に書けばこんな感じになるだろうか。 127 | 128 | ```d 129 | dstring extractText(string xmltext) 130 | { 131 | dstring dxml = xmltext.to!dstring(); 132 | dstring text; 133 | while(findSkip(dxml, "")){ 134 | auto e = indexOf(dxml,""); 135 | text ~= dxml[0..e]; 136 | } 137 | return text; 138 | } 139 | ``` 140 | 141 | あとはこいつを 142 | 143 | ```d 144 | dstring text = extractText(str); 145 | ``` 146 | 147 | と呼び出せば、`text`にテキストノードが全て結合されたものが入る。 148 | 149 | ### スライドに検索ワードが含まれていたらファイル名とスライド番号を出力 150 | 151 | ```d 152 | if(text.indexOf(keyword) !=-1) 153 | { 154 | auto rname = relativePath(filename); 155 | writefln("Found \"%s\" in %s at slide %s",keyword,rname, slidenum); 156 | } 157 | ``` 158 | 159 | そのままなので難しいところは無いと思う。ただ、ファイル名が絶対パスになっているのが不便だったので、`relativePath`でカレントディレクトリからの相対パスを出力するようにしている。 160 | 161 | ### 速度 162 | 163 | 自分のすべてのスライド資産にたいして、キーワード「hoge」を検索するのにかかった時間をRuby版と比較してみる。何度か実行して、ファイル情報がキャッシュにのった状態で測定。 164 | 165 | ``` 166 | $ time ruby ~/github/grep_pptx/grep_pptx.rb hoge > /dev/null 167 | ruby ~/github/grep_pptx/grep_pptx.rb hoge > /dev/null 80.60s user 6.74s system 96% cpu 1:30.16 total 168 | 169 | $ time ~/github/pptxgrep/pptxgrep hoge > /dev/null 170 | ~/github/pptxgrep/pptxgrep hoge > /dev/null 2.13s user 0.62s system 92% cpu 2.974 total 171 | ``` 172 | 173 | Ruby版が90秒、D言語版は3秒ということで、30倍くらい早くなった。なお、Ruby版は 174 | 175 | * unzipをシェルで呼び出している 176 | * 必要ないファイルもすべて展開し、ファイルに吐いている 177 | * XPathを使ったXML解析をしている 178 | 179 | というハンデがあるので、これはフェアな比較になっていないことに注意。 180 | 181 | ちなみにD言語版でも、XMLでパースした場合は 182 | 183 | ``` 184 | $ time ~/github/pptxgrep/pptxgrep hoge > /dev/null 185 | ~/github/pptxgrep/pptxgrep hoge > /dev/null 19.15s user 0.85s system 95% cpu 20.937 total 186 | ``` 187 | 188 | と、自前パース版に比べて7倍くらい遅くなる。 189 | 190 | ## まとめ 191 | 192 | D言語でpptx内テキストのgrepコマンドを作った。数十行足らずでこれだけのことができるんだから大したもんだと思う。ただし、`std.xml`が遅いのはちょっと困る。これは開発コミュニティも把握しているっぽいが、`std.xml2`は[Abandoned](https://wiki.dlang.org/Review_Queue)になってますね・・・ --------------------------------------------------------------------------------