Ruby
XML
pptx

パワーポイント内のテキストをgrepする

はじめに

発表スライドを多数含むディレクトリを前に「あれ?これどこで話したっけ?」「あの話をしたスライドどれだっけ?」と悩むことが多い。そんな時、パワーポイント内のテキストを検索したくなるが、ちょっと検索した範囲ではあまり便利なものがなかったので作った。Mac上で、Mac版PowerPointに対してしか動作確認をしていない。

ファイルは
https://github.com/kaityo256/grep_pptx
においてある。

(2018年9月21日追記) より高速なD言語版を作ったので、そちらもどうぞ。

使い方

https://github.com/kaityo256/grep_pptxのgrep_pptx.rbを使って、探したいディレクトリで

$ ruby grep_pptx.rb keyword
find "keyword" in ./hoge/test2.pptx at slide 5
find "keyword" in ./test.pptx at slide 1
find "keyword" in ./test2.pptx at slide 5

とキーワードを指定すると、その場所から再帰的にpptxファイルを探し、そのpptxの何番目のスライドにそのキーワードがあるかを報告する。端末設定にも依ると思われるが、少なくとも筆者の環境では日本語も検索できる。

動作原理

カレントディレクトリ以下のpptxファイルを探す

これはDir.globを使えば一発。

Dir.glob './**/*.pptx' do |file|
  find_in_file(file, keyword)
end

unzipとディレクトリ構造

pptxファイルはOffice Open XMLになっており、unzipすれば中身がxmlファイルとして出てくる。

適当なファイルをunzipしてみると、こんなディレクトリ構造になる。

.
├── [Content_Types].xml
├── _rels
├── docProps
│   ├── app.xml
│   ├── core.xml
│   └── thumbnail.jpeg
└── ppt
    ├── _rels
    │   └── presentation.xml.rels
    ├── presProps.xml
    ├── presentation.xml
    ├── slideLayouts
    │   ├── _rels
    │   │   ├── slideLayout1.xml.rels
    │   │   ├── slideLayout10.xml.rels
    │   │   ├── slideLayout11.xml.rels
    │   │   ├── slideLayout2.xml.rels
    │   │   ├── slideLayout3.xml.rels
    │   │   ├── slideLayout4.xml.rels
    │   │   ├── slideLayout5.xml.rels
    │   │   ├── slideLayout6.xml.rels
    │   │   ├── slideLayout7.xml.rels
    │   │   ├── slideLayout8.xml.rels
    │   │   └── slideLayout9.xml.rels
    │   ├── slideLayout1.xml
    │   ├── slideLayout10.xml
    │   ├── slideLayout11.xml
    │   ├── slideLayout2.xml
    │   ├── slideLayout3.xml
    │   ├── slideLayout4.xml
    │   ├── slideLayout5.xml
    │   ├── slideLayout6.xml
    │   ├── slideLayout7.xml
    │   ├── slideLayout8.xml
    │   └── slideLayout9.xml
    ├── slideMasters
    │   ├── _rels
    │   │   └── slideMaster1.xml.rels
    │   └── slideMaster1.xml
    ├── slides
    │   ├── _rels
    │   │   ├── slide1.xml.rels
    │   │   ├── slide2.xml.rels
    │   │   ├── slide3.xml.rels
    │   │   ├── slide4.xml.rels
    │   │   └── slide5.xml.rels
    │   ├── slide1.xml
    │   ├── slide2.xml
    │   ├── slide3.xml
    │   ├── slide4.xml
    │   └── slide5.xml
    ├── tableStyles.xml
    ├── theme
    │   └── theme1.xml
    └── viewProps.xml

えらくごちゃごちゃしているんだけど、大事なのは/ppt/slidesにスライドの中身があって、それぞれslide1.xmlslide2.xml..となっている。このXMLをパースして、どこにどんなテキストがあるかを調べればよろしい。

とりあえず現在のディレクトリにテンポラリディレクトリを掘って、そこに展開してしまう。真面目にRubyからunzipするのは面倒なので外部コマンドを使ってしまおう。

def find_in_file(inputfile, keyword)
  Dir.mktmpdir(nil,'./') do |dir|
    `cd #{dir};unzip ../#{inputfile}`
    mygrep(dir, keyword, inputfile)
  end
end

あとはこのmygrepを実装すればよろしい。

テキストの探し方

ここで面倒なのが、スライドでは一連のテキストに見えるものが実際には複数のXMLエレメントに分離していることだ。

例えば「平成27年」というテキストを書いてみる。パワーポイント上ではこう見える(見やすいように枠をつけてある)。

image.png

これが、対応するXMLではこんな感じにバラされてしまっている。

<a:p>
  <a:r>
    <a:rPr kumimoji='1' lang='ja-JP' altLang='en-US'/>
    <a:t>
      平成
    </a:t>
  </a:r>
  <a:r>
    <a:rPr kumimoji='1' lang='en-US' altLang='ja-JP'/>
    <a:t>
      27
    </a:t>
  </a:r>
  <a:r>
    <a:rPr kumimoji='1' lang='ja-JP' altLang='en-US'/>
    <a:t></a:t>
  </a:r>
</a:p>

このまま検索をかけるのは不便なので、一度XMLに存在するテキストを一括してjoinしてしまう。スライドに対応するREXML::Documentオブジェクトがdocだとすると、

doc = REXML::Document.new(f.gets(nil))
n =  REXML::XPath.first(doc.root,'/p:sld/p:cSld')
text = REXML::XPath.match(n, './/text()').join

とすればスライドに含まれるテキストが全部くっついた文字列が生成されるので、あとはtext.include?(keyword)で調べれば良い。ファイル名はslide7.xmlみたいになっているので、スライド番号はそこから取れる。

ソース

さして長くないのでソース全体も掲載しておく。

require 'tmpdir'
require 'rexml/document'

if ARGV.size < 1
  puts "usage: ruby grep_pptx.rb keyword"
  exit
end
keyword = ARGV[0]

def mygrep(dir, keyword, inputfile)
  Dir.glob(dir+"/ppt/slides/*.xml") do |file|
    slidenum = 0
    if file=~/slide([0-9]+).xml/
      slidenum = $1.to_i
    end
    f = open(file)
    doc = REXML::Document.new(f.gets(nil))
    n =  REXML::XPath.first(doc.root,'/p:sld/p:cSld')
    text = REXML::XPath.match(n, './/text()').join
    if text.include?(keyword)
      puts "find \"#{keyword}\" in #{inputfile} at slide #{slidenum}"
    end
  end
end

def find_in_file(inputfile, keyword)
  Dir.mktmpdir(nil,'./') do |dir|
    `cd #{dir};unzip ../#{inputfile}`
    mygrep(dir, keyword, inputfile)
  end
end

Dir.glob './**/*.pptx' do |file|
  find_in_file(file, keyword)
end

まとめ

カレントディレクトリ以下にあるpptx全部にgrepをかけて、見つけたファイル名とスライド番号を報告する手抜きスクリプトを作った。問題点や要望としては

  • 毎回ファイルをunzipしてはREXMLで解析しているので遅い
  • 全部のテキストを結合してからinclude?で探しているので遅い
  • 正規表現に対応していない
  • 前後のテキストを表示する機能がない
  • 標準のgrepと連携したい

などがあるが、とりあえず今の僕の目的には十分なのでよしとする。スクリプトをMITライセンスで置いておくので、好きなように改造してください。

参考