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 で作成したドキュメントで検証をおこなった。
内容は「こんにちは ○○ さん」というだけの単純なもの。○○の部分にプログラムから差しこみたい。
テンプレート文書の作成
差しこみフィールドの作成のやり方。
展開
$ 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"><hogehoge></text:placeholder> さん! </text:p>
単純明快。
Perl のワンライナーでざっくり置換してみた。XML を展開・再構築するほうが行儀いいんだろうけど,バリデーションを行うのでもなければ PCRE で必要十分な気がする。
$ perl -i -pe 's{<text:placeholder .*?> \s* <hogehoge> \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 で作成したドキュメントで検証をおこなった。
テンプレート文書の作成
差しこみフィールドの作成のやり方。
展開
$ 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>«hogehoge»</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 ではどうやるか調べていないが,同等の機能はたぶんあると思う。
- 「挿入」メニューから「フィールド」「その他」メニューを選択する
- 「変数」タブを選択
- 「フィールドタイプ」で「ユーザー欄」を選択
- 「名前」フィールドに「変数名」を,「値」フィールドにデフォルトの値を入力
- (書式は「テキスト」を選択しておくのが無難)
これでユーザ定義変数が定義される (「選択」ペインにユーザ変数一覧が表示される)。
あとは文書中の挿入したい場所にカーソルを移動して「フィールド」の「挿入」ダイアログをさきほどと同じように出し,「選択」ペインで作成したユーザ定義変数を選択し,「挿入」ボタンを押下すれば埋め込まれる。
このときの 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 の場合
- Java の場合
- そもそも UNO を使えば Java から OpenOffice.org, LibreOffice 等を操れるはず。
- Apache POI で OLE2 (旧来の MS Office document コンテナ) や OOXML のファイルを読み・書き・操作できるようだ。
- とはいえコアはあくまでコンテナの読み書きっぽい。各アプリケーション (Word など) の文書を操作する高次 API も用意されているようだが (例: Apache POI HWPF) クオリティについては不明。