Microsoft Word で LL 言語から差し込み文書作成

タイトルは釣り。
簡単な帳票 (Excel 系じゃなくて Word 系の文書) をプログラムから作成する必要がでてきたので調べた。テンプレートとなる文書にちょっとした変数の差し込みを行う必要があるケース。
ただし

  • 旧来の Microsoft Office Document (OLE2) はサポートしない
  • COM や UNO は使わない
  • (ODF や OOXML の) 仕様書は読まない

LibreOffice (OpenOffice.org) や最近の Microsoft Office だと,XML ファイルを ZIP で固めているだけなので単純な差し込みだけならわりと簡単にできそう。
注意: ODF にしても OOXML にしても XML namespace を使っているので,厳密なことをいうと以下の例のようにタグ名を決め打ちで置換していてはうまくいかないことがありうる。とはいえそうそう変更しないだろうからこのままでうまくいくだろうし,気になるなら対象タグの xmlns の部分だけバリデーション (というかサポートするターゲットかどうか判別) すればいいんじゃないかと思う。

OpenDocument Format (ODF) 編

LibreOffice 3.3.2 Writer on Ubuntu 11.04 で作成したドキュメントで検証をおこなった。
内容は「こんにちは ○○ さん」というだけの単純なもの。○○の部分にプログラムから差しこみたい。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「機能」タブを選択
  3. 「フィールドタイプ」で「プレースホルダ」を選択,「書式」は「テキスト」を選択
  4. プレースホルダ」に何らかのプレースホルダ名 (テンプレート変数的なもの) をいれる
  5. 「挿入」ボタンを押下
展開
$ unzip ../hello.odt

Archive:  ../hello.odt
 extracting: mimetype                
  inflating: content.xml             
  inflating: manifest.rdf            
  inflating: styles.xml              
 extracting: meta.xml                
 extracting: Thumbnails/thumbnail.png  
  inflating: Configurations2/accelerator/current.xml  
   creating: Configurations2/progressbar/
   creating: Configurations2/floater/
   creating: Configurations2/popupmenu/
   creating: Configurations2/toolpanel/
   creating: Configurations2/menubar/
   creating: Configurations2/toolbar/
   creating: Configurations2/images/Bitmaps/
   creating: Configurations2/statusbar/
  inflating: settings.xml            
  inflating: META-INF/manifest.xml   

無駄に空フォルダが多いが,実態は追える範囲。

解析と置換

文書の内容はおもに content.xml に記述されている。
今回の主眼となる部分の抜粋 (改行やインデントは編集してある; 元は全部1行*1 )。

<text:p text:style-name="Standard">
  こんにちは
  <text:placeholder text:placeholder-type="text">&lt;hogehoge&gt;</text:placeholder>
  さん!
</text:p>

単純明快。
Perlワンライナーでざっくり置換してみた。XML を展開・再構築するほうが行儀いいんだろうけど,バリデーションを行うのでもなければ PCRE で必要十分な気がする。

$ perl -i -pe 's{<text:placeholder .*?>
                 \s* &lt;hogehoge&gt; \s*
                 </text:placeholder>      }{dayflower}gxms' content.xml

これで一応完成。だけど,文書の統計情報 (何文字含まれているかとか何ページあるかとか) も文書に含まれている。基本的に文書ファイルのプロパティで表示するためだけのものなので間違っていても構わないんだけど,なんか気持ち悪いので削除しておく。
統計情報の実態は meta.xml というファイルの <meta:document-statistic/> タグに存在する。以下は例。

<meta:document-statistic
   meta:table-count="0"
   meta:image-count="0"
   meta:object-count="0"
   meta:page-count="1"
   meta:paragraph-count="1"
   meta:word-count="3"
   meta:character-count="20"/>

なので meta.xml ファイルから該当する XML タグを削除しておけばよい。

再構築

普通に再圧縮するだけ。

$ zip -r9 ../new.odt *

updating: Configurations2/ (stored 0%)
updating: META-INF/ (stored 0%)
......

作成された文書を再度 LibreOffice で開いてみたところ,きちんと埋め込みされていた。

Office Open XML (OOXML) 編

Microsoft Office 2010 Word で作成したドキュメントで検証をおこなった。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニュータブにおいて「クイックパーツ」を選択し「フィールド」メニューを選択
  2. 「フィールドの選択」で「差し込み印刷」フィルタを選び*2,「フィールドの名前」から「MergeField」を選択
  3. 「フィールドプロパティ」の「フィールド名」に適宜名前 (テンプレート変数的なもの) をつける
展開
$ unzip ../hello.docx

Archive:  ../hello.docx
  inflating: [Content_Types].xml     
  inflating: _rels/.rels             
  inflating: word/_rels/document.xml.rels  
  inflating: word/document.xml       
  inflating: word/theme/theme1.xml   
  inflating: word/settings.xml       
  inflating: word/webSettings.xml    
  inflating: word/stylesWithEffects.xml  
  inflating: docProps/core.xml       
  inflating: word/styles.xml         
  inflating: word/fontTable.xml      
  inflating: docProps/app.xml        

無駄な空フォルダは少ない。結構多くのファイルに分散しているのは ODF と同等。

解析と置換

文書の内容はおもに word/document.xml に記載されている。
今回のターゲットとなる部位の抜粋。

<w:p w:rsidR="00332438" w:rsidRDefault="00792A30">
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>こんにちは</w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:fldSimple w:instr=" MERGEFIELD  hogehoge  \* MERGEFORMAT ">
    <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
      <w:t>&#171;hogehoge&#187;</w:t>
    </w:r>
  </w:fldSimple>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>さん!</w:t>
  </w:r>
  <w:bookmarkStart w:id="0" w:name="_GoBack"/>
  <w:bookmarkEnd w:id="0"/>
</w:p>

埋め込みを行う立場から見ると,ちょっとめんどい (あくまで ODF に比べての話だけど)。MergeField じゃないもっと適切なフィールドタイプがあるのかもしれない。

フィールド埋め込みについては Perlワンライナーで書くなら以下のような感じ。

$ perl -pe 's{<w:fldSimple .*?>
                (.*? <w:t>)
                  .*?
                (</w:t> .*?)
              </w:fldSimple>    }{$1dayflower$2}gxms' document.xml

さきほどの ODF の例とは置換内容がちょっと違う (全埋め込みフィールド対象になっちゃってる) けど,ちゃんとやるなら置換後のほうにコード仕込んでとかやるかな。
OOXML でもやはり文書の統計情報が存在する。実態は docProps/app.xml のいくつかのタグ。以下抜粋。

<Pages>1</Pages>
<Words>9</Words>
<Characters>52</Characters>
<Lines>1</Lines>
<Paragraphs>1</Paragraphs>
再構築
$ zip -r9 ../new2.docx *
  adding: [Content_Types].xml (deflated 75%)
  adding: _rels/ (stored 0%)
......

作成された文書を再度 Microsoft Word 2010 で開いてみたところ,きちんと埋め込みされていた。

同じ値を複数ヶ所に同時に埋め込みたい場合

もちろん全プレースホルダを上記のようにプログラムから書き換えてもいいんだけど,もっとスマートな解決策もある。OpenDocument などにはユーザ定義変数のようなものがあって,それを文書中に埋め込むことができる。この変数の値の部分だけ書き換えれば,文書中のそのユーザ変数フィールドは全部置き換わることになる。
以下は OpenDocument での例。面倒なので Microsoft Word ではどうやるか調べていないが,同等の機能はたぶんあると思う。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「変数」タブを選択
  3. 「フィールドタイプ」で「ユーザー欄」を選択
  4. 「名前」フィールドに「変数名」を,「値」フィールドにデフォルトの値を入力
  5. (書式は「テキスト」を選択しておくのが無難)

これでユーザ定義変数が定義される (「選択」ペインにユーザ変数一覧が表示される)。
あとは文書中の挿入したい場所にカーソルを移動して「フィールド」の「挿入」ダイアログをさきほどと同じように出し,「選択」ペインで作成したユーザ定義変数を選択し,「挿入」ボタンを押下すれば埋め込まれる。
このときの content.xml の内容は以下の通り。

<text:user-field-decls>
  <text:user-field-decl office:value-type="string" office:string-value="hogehoge" text:name="holder"/>
</text:user-field-decls>
<text:p text:style-name="Standard">
  こんにちは
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん!
</text:p>

さきほどと差しこみフィールドの実現のされ方が結構違っている。
変数部分 (<text:user-field-decl>) のほうだけ書き換えればいいので,適当なワンライナーだけど (ほんとうなら text:name のほうでチェックするべき)

$ perl -i -pe 's{string-value="hogehoge"}{string-value="dayflower"}gxms' content.xml

こんな感じで。
実態のドキュメント部分 (<text:user-field-get>) のほうは置き換えてないけど,LibreOffice で再度開いた際には変数の埋めこまれた部分は自動的に変更されていた。ファイル自体はちょっと気持ち悪い状態だけど。

メモ

その他既存のアプローチについて (COM 以外)。

  • Perl の場合
    • OpenOffice-OODoc を使えば OpenDocument を読み・書き・操作できるみたい?そこそこメンテナンスされているようだ。
  • Ruby の場合
    • OOo4R という OpenDocumento を読み書きするプロジェクトがあるようだが 2004 年あたりで停滞しているようだ。
    • roo というプロジェクトがあるが表計算系 (OpenDocument ods, Excel xls, xlsx, Google Spreadsheet) の読み込みしかできないようだ。
    • UFOOAR というプロジェクトがあり Ruby 用の UNO ブリッジを目指しているようだ。2008 年あたりで停滞か?
  • Java の場合
    • そもそも UNO を使えば Java から OpenOffice.org, LibreOffice 等を操れるはず。
    • Apache POI で OLE2 (旧来の MS Office document コンテナ) や OOXML のファイルを読み・書き・操作できるようだ。
      • とはいえコアはあくまでコンテナの読み書きっぽい。各アプリケーション (Word など) の文書を操作する高次 API も用意されているようだが (例: Apache POI HWPF) クオリティについては不明。

*1:このため vim で編集しようとしたら半ばフリーズ気味になってしまった。

*2:もちろんフィルタリングせず全フィールドタイプから選択してもいい。