Open XML WordprocessingML ドキュメントのテキストについて習得する
概要: Open XML WordprocessingML ドキュメントからテキストを確実に取得する方法について理解します。
適用対象: Office 2007 | Office 2010 | Open XML | Visual Studio Tools for Microsoft Office | Word 2007 | Word 2010
この記事の内容
概要
WordprocessingML のテキスト コンテンツについて
ベスト プラクティス: 処理の前に変更を反映する
WordprocessingML の抽象型について
階層のレベルが多いと処理の複雑さが増す
LogicalChildrenContent 軸メソッドの導入
DescendantsTrimmed 軸メソッドの実装
論理子の定義
LogicalChildrenContent 軸メソッドの使用
ExamineDocumentContent の例
段落のテキストの取得
LogicalChildrenContent 軸メソッドの 2 つの効果的なオーバーロード
LogicalChildrenContent メソッドで返された XML 要素の ID
ドキュメントのテキストの検索
まとめ
その他の技術情報
目次
概要
WordprocessingML のテキスト コンテンツについて
ベスト プラクティス: 処理の前に変更を反映する
WordprocessingML の抽象型について
階層のレベルが多いと処理の複雑さが増す
LogicalChildrenContent 軸メソッドの導入
DescendantsTrimmed 軸メソッドの実装
論理子の定義
LogicalChildrenContent 軸メソッドの使用
ExamineDocumentContent の例
段落のテキストの取得
LogicalChildrenContent 軸メソッドの 2 つの効果的なオーバーロード
LogicalChildrenContent メソッドで返された XML 要素の ID
ドキュメントのテキストの検索
まとめ
その他の技術情報
概要
Open XML ワープロ ドキュメントのテキストの処理は、最初、きわめて簡単なように見えます。ドキュメントの本体があり、本体には段落とテーブルがあり、テーブルには行とセルがあります。HTML と変わるところはありません。やがて、変更履歴、段落番号、箇条書き、コンテンツ コントロールのマークアップに加え、ブックマークやコメントのようにテキストに影響しないマークアップを目にして、作業が非常に困難に思えてきます。スタイルはテキストに関係ないように思われますが、段落番号や箇条書きがある場合は関係があります。実のところ、複雑さは中程度です。留意するものは多数ありますが、これらの機能の 1 つ 1 つを取り出してみれば、それほど困難なものではありません。
とはいえ、ワープロ マークアップについての理解を促進するいくつかの基本的な概念と抽象型があります。ワープロ マークアップの処理を、Open XML SDK 2.0 for Microsoft Office へようこそ と LINQ to XML を使用し、Open XML SDK 2.0 の厳密に型指定されたオブジェクト モデルを使用して行うか、または、Java、PHP など、他のプラットフォームを使用して行うかどうかにかかわらず、これらの抽象型は関係します。これらの抽象型を指定するコードを記述します。コードは、目的の要素を組織的で予測可能な方法で正確に表示できます。この記事では、LINQ to XML と、Open XML SDK 2.0 の厳密に型指定されたオブジェクト モデルを使用して記述した Microsoft Visual C# コードを示します。いくつかの有用なメソッドのセマンティクスは注意深く定義されているため、どのような言語やプラットフォームを使用していても簡単に実装できます。
WordprocessingML のテキスト コンテンツについて
ドキュメントのメイン本体パーツでは、すべてのテキストが段落に格納されます。段落は、本体要素の子 (w:body)、テーブル内のセルの子 (w:tc)、およびテキスト ボックス コンテンツの子 (w:txbxContent) として、3 つの場所に存在します。セル自体にテーブルを格納できます。メイン ドキュメント パーツには、この他にもテキストのインスタンスがあります。画像は代替テキストを含むことができ、SmartArt グラフィックはテキストを含みます。しかし、これらのテキストは別物です。複数の文字列のテキストを単一の文字列にまとめるという問題は、これらには該当しません。
テキスト コンテンツの興味深い関係の 1 つは、段落はランを格納でき、ランは図形を格納でき、図形はテキスト ボックスを格納でき、テキスト ボックスは段落を格納できるということです。これは、段落要素が別の段落要素の子孫として出現する、他にはない Open XML WordprocessingML マークアップの状態です。その詳細と問題については、後述します。
ベスト プラクティス: 処理の前に変更を反映する
WordprocessingML コンテンツを処理する方法を簡単化する上で、最初の最も重要なポイントは、すべての変更履歴を反映することです。変更履歴のセマンティクスの詳細については、「Accepting Revisions in Open XML Word-Processing Documents」を参照してください。また、変更履歴を反映する Microsoft Visual C# 3.0 コード サンプルは、CodePlex の PowerTools for Open XML (英語) プロジェクトで参照してください。[Downloads] タブをクリックし、 RevisionAccepter.zip をダウンロードします。
変更履歴を最初に反映すると良いのは、この後、コンテンツの処理を複雑にする 40 を越える要素を安全に無視できることです。その要素の多くは、複雑なセマンティクスを持ちます。そのため、それらを処理してからドキュメントのコンテンツを処理する方が適切です。その MSDN 記事を記述し、変更を反映するコードを作成するまで、すべての状態に対応することはできず、あまりに簡単な方法では段落のテキストが間違って取得されます。
ドキュメントを変更せずにクエリを実行する必要がある場合があります。ドキュメントをバイト配列に読み取り、バイト配列から可変サイズのメモリ ストリームを作成し、メモリ ストリームからドキュメントを開くという単純な手法を使用できます。この方法の詳細については、ブログ投稿「Simplifying Open XML WordprocessingML Queries by First Accepting Revisions (英語)」を参照してください。この例では、変更を反映し、ディスク上の実際のドキュメントにはアクセスせずにドキュメントのクエリを実行します。
WordprocessingML の抽象型について
WordprocessingML マークアップについての理解を深めるために、いくつかの抽象型を定義しましょう。
ブロックレベルのコンテンツ コンテナー
ブロックレベルのコンテンツ
ランレベルのコンテンツ コンテナー
ランレベルのコンテンツ
サブランレベルのコンテンツ
変更履歴を反映し、高度なシナリオでのみ適用されるいくつかの要素は無視することを決定すると、残された処理する要素の一覧は次のとおりです。
ブロックレベルのコンテンツ コンテナー
ブロックレベルのコンテンツ コンテナーは、段落、テーブルなど、ブロックレベルのコンテンツが格納される WordprocessingML 要素です。メイン ドキュメント パーツに出現するブロックレベルのコンテンツ コンテナー要素は、3 つだけです。
ブロックレベルのコンテンツ コンテナー要素
要素 |
要素名 |
Open XML SDK 2.0 クラス名 名前空間: DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
本体 |
w:body |
Body |
テーブル セル |
w:tc |
TableCell |
テキスト ボックス コンテンツ |
w:txbxContent |
TextBoxContent |
前述のように、WordprocessingML には、コメント パーツ内の w:comment 要素、見出しパーツ内の w:hdr 要素など、段落が格納されるブロックレベルのコンテンツ コンテナーが他にもあります。しかし、これらはメイン ドキュメント パーツにはありません。このため、同じ処理の問題を示すわけではありません。
ブロックレベルのコンテンツ
ブロックレベルのコンテンツ要素は、レイアウト面の幅全体を占めます。上部と下部にバインドされ、有効スペースの左から右への有効な幅を占めます。たとえば、ドキュメントの通常のレイアウトでは、同じ物理行に 2 つの段落は表示されず、段落とテーブルが並んで表示されることもありません。
この規則には例外があるように思われますが、実際、これらの見かけ上の例外は、本当の例外ではありません。段落が並んで表示される例は、複数列のページ レイアウトを使用しています。この例の場合、段落またはテーブルのレイアウトの有効幅は列であり、ページ全体ではありません。もう 1 つの例は、ページ上にテキスト ボックスがある場合ですが、この例の場合、ブロックレベルのコンテンツのレイアウトの有効幅に、テキスト ボックス用に予約されているスペースは含まれません。さらに、テキスト ボックス自体が自身のレイアウト面を持ちます。
変更の反映後にあるブロックレベルのコンテンツ要素は 2 つだけです。
ブロックレベルのコンテンツ要素
要素 |
要素名 |
Open XML SDK 2.0 クラス名 名前空間: DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
段落 |
w:p |
Paragraph |
テーブル |
w:tbl |
Table |
この記事では説明しませんが、この他に、数式用のブロックレベルのコンテンツ要素が 2 つあります。MathML テキスト コンテンツの処理は、一般的には必要ありません。(段落で行うように) 数式のテキストを収集し、それを単一の文字列にまとめることは、それほど求められていません。数式内のテキストは、数式のコンテキストで処理される必要があります。この記事では、MathML 数式については説明しません。
ランレベルのコンテンツ コンテナー
変更の反映後にあるランレベルのコンテンツ コンテナー要素は、段落 (w:p) 要素の 1 つだけです。ランレベルのコンテンツ コンテナーは、ランレベルのコンテンツが左から右へと配置されるスペース、または場合によっては右から左へと配置されるスペースを定義します。たとえば、段落内の複数のテキスト ランは、必要に応じてそれぞれのフォントで、横方向に折り返して配置されます。段落は、ブロックレベルのコンテンツ要素であると同時にランレベルのコンテンツ コンテナー要素であるのに対して、テーブルは、ブロックレベルのコンテンツ要素のみであり、ランレベルのコンテンツ コンテナー要素ではないことに注意してください。
ランレベルのコンテンツ コンテナー要素
要素 |
要素名 |
Open XML SDK 2.0 クラス名 名前空間: DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
段落 |
w:p |
Paragraph |
ランレベルのコンテンツ
ランレベルのコンテンツは、段落のサブセクション固有の書式設定を持つ、段落内のコンテンツです。たとえば、ランは固有のフォントを持ちます。変更の反映後にあるランレベルのコンテンツ要素は 3 つだけです。
ランレベルのコンテンツ要素
要素 |
要素名 |
Open XML SDK 2.0 クラス名 名前空間: DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
テキスト ラン |
w:r |
Run |
VML 描画 |
w:pict |
Picture |
DrawingML オブジェクト |
w:drawing |
Drawing |
この要素の一覧では分かりにくいのですが、Vector Markup Language (VML) 描画オブジェクトまたは DrawingML オブジェクトは、ランレベルのコンテンツまたはサブランレベルのコンテンツです。両方とも、ブロックレベルのコンテンツ コンテナーである w:txbxContent 要素を子孫として格納できます。
サブランレベルのコンテンツ
サブランレベルのコンテンツは、ランの一部である WordprocessingML 要素で構成されます。たとえば、ランは複数のテキスト要素 (w:t) を格納できます。
サブランレベルのコンテンツ要素
要素 |
要素名 |
Open XML SDK 2.0 クラス名 名前空間: DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
改行 |
w:br |
Break |
復帰 |
w:cr |
CarriageReturnPicture |
日付ブロック – 長い日付書式 |
w:daylong |
DayLong |
日付ブロック – 長い日付書式 |
w:daylong |
DayLong |
日付ブロック – 短い日付書式 |
w:dayShort |
DayShort |
DrawingML オブジェクト |
w:drawing |
Drawing |
日付ブロック – 長い月書式 |
w:monthLong |
MonthLong |
日付ブロック – 短い月書式 |
w:monthShort |
MonthShort |
改行をしないハイフン文字 |
w:noBreakHyphen |
NoBreakHyphen |
ページ番号ブロック |
w:pgNum |
PageNumber |
VML 描画 |
w:pict |
Drawing |
絶対位置タブ文字 |
w:pTab |
PositionalTab |
任意指定のハイフン文字 |
w:softHyphen |
SoftHyphen |
記号 |
w:sym |
SymbolChar |
テキスト |
w:t |
Text |
タブ文字 |
w:tab |
TabChar |
日付ブロック – 長い年書式 |
w:yearlong |
YearLong |
日付ブロック – 短い年書式 |
w:yearShort |
YearShort |
このリストには VML 描画オブジェクトと DrawingML オブジェクトも含まれており、これらは w:txbxContent 要素 (ブロックレベルのコンテンツ コンテナー) を子孫として格納できます。
階層のレベルが多いと処理の複雑さが増す
単純な例で、解決を試みている問題を示します。次のドキュメントの第 1 段落には、コンテンツ コントロールとテキスト ボックスがあります。
図 1. コンテンツ コントロールとテキスト ボックスを含むドキュメント
次のコード例は、この段落のマークアップを示しています。このマークアップの詳細については、「ISO/IEC 29500-1:2008 (英語)」または「Standard ECMA-376 Office Open XML File Formats, Second Edition (ECMA-376 2nd edition) (英語)」を参照してください。
注意
問題を理解しやすいように、余分なマークアップは省略されています。
<w:p>
<w:pPr>
<w:ind w:right="3600"/>
</w:pPr>
<w:r>
<w:rPr>
<w:noProof/>
</w:rPr>
<mc:AlternateContent>
<mc:Choice Requires="wps">
<w:drawing>
<!-- . . . -->
<wps:txbx>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</wps:txbx>
<!-- . . . -->
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
<!-- . . . -->
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
<!-- . . . -->
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
<w:sdt>
<w:sdtContent>
<w:r>
<w:t>Text in content control.</w:t>
</w:r>
</w:sdtContent>
</w:sdt>
<w:r>
<w:t xml:space="preserve"> Text following the content control.</w:t>
</w:r>
</w:p>
この例では、テキスト ボックスのテキストは、コンテンツ コントロール内のテキストと同じ段落にあります。これは、コンテンツ コントロールの外部のテキストとも同じ段落にあります。コンテンツ コントロールによって、テキスト要素が階層の異なるレベルに発生します。階層内のこの相違を指定するコードを記述する必要があります。これは、問題を示している 1 つの例です。異なるインデント レベルでのテキスト コンテンツの発生を引き起こす WordprocessingML 抽象型は、数多く存在します。このため、この問題に対する汎用的なソリューションを開発する必要があります。
注意
Value プロパティを使用して段落 (w:p) 要素のテキストを取得するのは正しくありません。
using (WordprocessingDocument doc = WordprocessingDocument.Open("Test.docx", false))
{
XElement root = doc.MainDocumentPart.GetXDocument().Root;
XElement paragraph = root.Descendants(W.p).First();
Console.WriteLine(paragraph.Value);
}
返されたテキストは間違っています。
図 2. 段落値を使用した場合の間違った結果
テキスト ボックスのコンテンツが 2 回表示されることが問題ではありません。すべてが表示されることが問題なのです。テキスト ボックスのテキストは、本当は、段落の一部ではありません。それ自体で存在するものです。
コンテンツ コントロールによって、マークアップの階層内の異なるレベルにテキスト ランが発生するため、段落の子ランを反復処理することはできません。
using (WordprocessingDocument doc = WordprocessingDocument.Open("Test.docx", false))
{
XElement root = doc.MainDocumentPart.GetXDocument().Root;
XElement paragraph = root.Descendants(W.p).First();
StringBuilder sb = new StringBuilder();
foreach (XElement t in paragraph.Elements(W.r).Elements(W.t))
sb.Append((string)t);
Console.WriteLine(sb.ToString());
}
これには、コンテンツ コントロール内のテキストが含まれません。
図 3. 段落の子ランを連結した場合の間違った結果
これを特別な場合として処理するコードを記述できます。しかし、テキスト コンテンツを階層の異なるレベルに発生させる他のコンストラクトが原因で、正しい結果を返すことはできません。このため、ドキュメントのテキスト コンテンツの処理を促進する、汎用的な抽象型が必要です。
「Standard ECMA-376: Office Open XML File Formats, First Edition (ECMA-376) (英語)」には、XML 階層内のコンテンツの位置に関する同じ問題が記載されています。テキスト ボックスを子孫として格納する要素は、段落内の他のサブランレベルのコンテンツの兄弟要素です。この記事に記載されている抽象型は、ECMA-376 マークアップに同じように適用できます。
<w:p>
<w:pPr>
<w:ind w:right="3600"/>
</w:pPr>
<w:r>
<w:pict>
<v:shape . . .>
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
</v:shape>
</w:pict>
</w:r>
<w:sdt>
<w:sdtContent>
<w:r w:rsidR="00C578DC">
<w:t>Text in content control.</w:t>
</w:r>
</w:sdtContent>
</w:sdt>
<w:r>
<w:t xml:space="preserve"> Text following the content control.</w:t>
</w:r>
</w:p>
LogicalChildrenContent 軸メソッドの導入
この問題を解決するために、要素の論理子のコンテンツを返す軸メソッドを作成しました。論理子は、コンテンツ コントロールなど、コンテンツの階層のレベルを増やす他の要素に格納されているコンテンツを含みます。このため、この論理子コンテンツの軸は、LINQ to XML (または XPath) 子の軸とは異なります。階層のレベルを増やす実際の要素 (w:sdt、w:fldsimple、w:hyperlink) は、返されるコレクションには含まれません。必要なのは、コンテンツを格納している他の要素ではなく、実際のコンテンツです。
ヒント
軸メソッドという用語は、LINQ to XML の用語です。XML ドキュメントのコンテキストで軸とは、どのような要素にも関連要素の特定のセットがあるという概念で、軸メソッドはその関連要素のコレクションを返します。たとえば、ある XML 要素は、子要素の特定のセット、子孫の特定のセット、および祖先の特定のセットを持ちます。子孫、子要素、および祖先は、LINQ to XML 軸メソッドの基本です。
以下では、本体要素の論理子コンテンツを取得した場合に返されるコレクション内にある要素に注目しています。テキスト ボックス内の段落要素は、論理子には含まれません。これは、段落が、段落を格納しているテキスト ボックス コンテンツ要素 (w:txbxContent) の論理子だからです。テキスト ボックス コンテンツ要素は VML 画像要素 (w:pict) の論理子であり、VML 画像要素はこれを格納しているランの論理子孫です。
<w:body>
<w:sdt>
<w:sdtPr>
<w:id w:val="172579038"/>
<w:placeholder>
<w:docPart w:val="DefaultPlaceholder_22675703"/>
</w:placeholder>
</w:sdtPr>
<w:sdtEndPr/>
<w:sdtContent>
<w:p>
<w:r>
<w:t>Paragraph in content control.</w:t>
</w:r>
</w:p>
</w:sdtContent>
</w:sdt>
<w:p>
<w:pPr>
<w:ind w:right="3600"/>
</w:pPr>
<w:r>
<w:rPr>
<w:noProof/>
</w:rPr>
<mc:AlternateContent>
<mc:Choice Requires="wps">
<w:drawing>
. . .
<wps:txbx>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</wps:txbx>
. . .
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
. . .
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
. . .
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
<w:sdt>
<w:sdtContent>
<w:r>
<w:t>Text in content control.</w:t>
</w:r>
</w:sdtContent>
</w:sdt>
<w:r>
<w:t xml:space="preserve"> Text following the content control.</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Text in a following paragraph.</w:t>
</w:r>
</w:p>
</w:body>
以下では、第 2 段落の論理子コンテンツに注目しています。論理子に、最初のランの子孫は含まれません。
. . .
<w:p>
<w:pPr>
<w:ind w:right="3600"/>
</w:pPr>
<w:r>
<w:rPr>
<w:noProof/>
</w:rPr>
<mc:AlternateContent>
<mc:Choice Requires="wps">
<w:drawing>
. . .
<wps:txbx>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</wps:txbx>
. . .
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
. . .
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
. . .
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
<w:sdt>
<w:sdtContent>
<w:r>
<w:t>Text in content control.</w:t>
</w:r>
</w:sdtContent>
</w:sdt>
<w:r>
<w:t xml:space="preserve"> Text following the content control.</w:t>
</w:r>
</w:p>
この段落の最初のランの論理子要素は、mc:AlternateContent 要素です。
<w:r>
<w:rPr>
<w:noProof/>
</w:rPr>
<mc:AlternateContent>
<mc:Choice Requires="wps">
<w:drawing>
. . .
<wps:txbx>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</wps:txbx>
. . .
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
. . .
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
. . .
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
mc:AlternateContent は、コンテンツを処理する代替方法に関する情報を格納できるため、これを論理子コンテンツ要素の 1 つとして持つと役立ちます。mc:AlternateContent 要素の論理子は、自身に格納されている図形です。
<w:r>
<w:rPr>
<w:noProof/>
</w:rPr>
<mc:AlternateContent>
<mc:Choice Requires="wps">
<w:drawing>
. . .
<wps:txbx>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</wps:txbx>
. . .
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
. . .
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>Text in text box</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
<w10:wrap type="square"/>
. . .
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
DrawingML オブジェクトの論理子は、テキスト ボックス コンテンツ (w:txbxContents) です。その子は、囲まれた段落です。このように論理子の軸を定義することにより、段落のテキストを正確に作成しやすくなります。
DescendantsTrimmed 軸メソッドの実装
論理子の軸メソッドを実装するには、最初に、子孫がトリミングされた子孫要素のコレクションを返すメソッドを実装します。指定されたタグの子孫である要素は、返されるコレクションに含まれません。DescendantsTrimmed メソッドのもう 1 つのオーバーロードは、デリゲートを引数として使用します。このため、述語としてラムダ式を指定でき、各種タグに基づいてトリミングを実行できます。このメソッドのセマンティクスは、返されるコレクションにトリミングされた要素自体が含まれるように定義されています。
次のコード例は、DescendantsTrimmed 軸メソッドのセマンティクスを示しています。この軸メソッドでは、txbxContent 要素の子孫である要素がトリミングされます。各要素の要素名を表示するコード例は、祖先をカウントして、要素名を正しくインデントします。
XElement doc = XElement.Parse(
@"<body>
<p>
<r>
<t>Text before the text box.</t>
</r>
<r>
<pict>
<txbxContent>
<p>
<r>
<t>Text in a text box.</t>
</r>
</p>
</txbxContent>
</pict>
</r>
<r>
<t>Text after the text box.</t>
</r>
</p>
</body>");
foreach (XElement c in doc.DescendantsTrimmed("txbxContent"))
Console.WriteLine("{0}{1}", "".PadRight(c.Ancestors().Count() * 2), c.Name);
この例では、返されたコレクション内の各要素の名前を、インデントされたリストで表示します。
p
r
t
r
pict
txbxContent
r
t
論理子の定義
DescendantsTrimmed 軸メソッドを使用すると、特定の要素セットの論理子のみを返す軸メソッドを実装できます。論理子の定義は、次のとおりです。
w:document 要素の唯一の論理子は w:body 要素です。
ブロックレベルのコンテンツ コンテナー (w:body、w:tc、w:txbxContent) の論理子は、ブロックレベルのコンテンツ (w:p、w:tbl) です。
テーブル (w:tbl) の論理子は、その行 (w:tr) です。
行 (w:tr) の論理子は、そのセル (w:tc) です。
段落 (w:p) の論理子は、そのラン (w:r) です。
ラン (w:r) の論理子は、サブランレベルのコンテンツ (w:t、w:pict、w:drawing など) です。この記事の前述の一覧を参照してください。また、Office 2010 と ISO/IEC 29500 に対応するため、mc:AlternateContent 要素もランの子です。付属のコードは、ECMA-376 1st Edition と ISO/IEC 29500 (ECMA-376 2nd Edition) の両方で動作するように実装されています。
代替コンテンツ要素の論理子は、mc:Choice 要素内の画像です。mc.Fallback 要素ではなく、mc:Choice 要素のコンテンツを処理する必要があります。
VML 描画オブジェクト (w:pict) または DrawingML オブジェクト (w:drawing) の論理子は、格納されているテキスト ボックス コンテンツ要素 (w:txbxContent) です。VML オブジェクトまたは DrawingML オブジェクトの他の特定の部分を処理する必要があるシナリオでは、LogicalChildrenContent メソッドを再定義して、処理する必要がある要素を返されるコレクションに含めることができます。
LogicalChildrenContent 軸メソッドの使用
LogicalChildrenContent メソッドの実装を調べる前に、その用途を確認しておくとよいでしょう。
次の図は、課題のサンプル ドキュメントを示しています。
図 4. コンテンツ コントロールとテキスト ボックスが含まれるドキュメント
ExamineDocumentContent の例
この最初の例では、ドキュメント内のすべての論理コンテンツを再帰的に反復処理して、各要素の名前を正しいインデントで表示します。要素がテキスト要素 (w:t) の場合は、要素のテキスト コンテンツが出力されます。
この例は、RevisionAccepter.AcceptRevisions メソッドを呼び出すことにより、最初に変更を反映します。この例では、ドキュメントをバイト配列に読み取り、バイト配列から可変サイズのメモリ ストリームを初期化することでワープロ ドキュメントを開くという方法が使用されています。この方法により、編集可能なパラメーター を true に設定してドキュメントを開き、変更を反映できます。たとえばドキュメントを直接開いて編集する場合、変更を反映することで既存のドキュメントが変更されますが、思わぬ結果となる場合があります。たとえば読み取り専用モードでドキュメントを開いた場合、変更の反映は失敗します (例外がスローされます)。
static void IterateContent(XElement element, int depth)
{
if (element.Name == W.t)
Console.WriteLine("{0}{1} >{2}<", "".PadRight(depth * 2), element.Name.LocalName,
(string)element);
else
Console.WriteLine("{0}{1}", "".PadRight(depth * 2), element.Name.LocalName);
foreach (XElement item in element.LogicalChildrenContent())
IterateContent(item, depth + 1);
}
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
IterateContent(doc.MainDocumentPart.GetXDocument().Root, 0);
}
}
}
問題のドキュメントに対してこの例を実行すると、次のように表示されます。
document
body
p
r
t >Paragraph in <
r
t >content control.<
p
r
AlternateContent
drawing
txbxContent
p
r
t >Text in text box<
r
t >Text in content control. <
r
t >Text following the content control.<
p
r
t >Text in a following<
r
t > paragraph.<
さまざまな編集セッションを通じて、各種ランが複数のランに分割されて表示されています。テキスト ボックスとそのコンテンツは、該当の位置に表示されています。
Open XML SDK 2.0 for Microsoft Office へようこそ の厳密に型指定されたオブジェクト モデルを使用して、同じ軸メソッドを実装できます。論理コンテンツ軸を使用するコードは、次のようになります。
static void IterateContent(OpenXmlElement element, int depth)
{
if (element.GetType() == typeof(Text))
Console.WriteLine("{0}{1} >{2}<", "".PadRight(depth * 2),
element.GetType().Name, ((Text)element).Text);
else
Console.WriteLine("{0}{1}", "".PadRight(depth * 2),
element.GetType().Name);
foreach (var item in element.LogicalChildrenContent())
IterateContent(item, depth + 1);
}
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test7.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
IterateContent(doc.MainDocumentPart.Document, 0);
}
}
}
問題のドキュメントに対してこの例を実行すると、次のように表示されます。
Document
Body
Paragraph
Run
Text >Paragraph in <
Run
Text >content control.<
Paragraph
Run
AlternateContent
Drawing
TextBoxContent
Paragraph
Run
Text >Text in text box<
Run
Text >Text in content control. <
Run
Text >Text following the content control.<
Paragraph
Run
Text >Text in a following<
Run
Text > paragraph.<
段落のテキストの取得
ドキュメントを処理し、1 回の操作ですべての段落、各段落の下のすべてのラン、および各ランのすべてのテキスト要素を取得し、各段落の関連テキストを作成する必要がある場合があります。
これをできるだけ簡単に行うために、LogicalChildrenContent メソッドの別のオーバーロードを作成します。これは、コンテンツ要素のコレクションを引数として使用し、ソース コレクション内の各要素の論理子要素のセットをコレクションとして返す拡張メソッドとして作成すると便利です。この拡張メソッドは、ソース コレクション内の各要素のすべての子要素を返す、LINQ to XML の Elements 拡張メソッドに相当します。拡張メソッドは非常に簡単に実装できます。
public static IEnumerable<XElement> LogicalChildrenContent(this IEnumerable<XElement> source)
{
foreach (XElement e1 in source)
foreach (XElement e2 in e1.LogicalChildrenContent())
yield return e2;
}
Open XML SDK 2.0 for Microsoft Office へようこそ の厳密に型指定されたオブジェクト モデルを使用して実装される同じ軸メソッドは、次のようになります。
public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
this IEnumerable<OpenXmlElement> source)
{
foreach (OpenXmlElement e1 in source)
foreach (OpenXmlElement e2 in e1.LogicalChildrenContent())
yield return e2;
}
StringConcatenate メソッドという別の拡張メソッドの使用も役立ちます。これは、文字列の結合処理です。
public static string StringConcatenate(this IEnumerable<string> source)
{
StringBuilder sb = new StringBuilder();
foreach (string s in source)
sb.Append(s);
return sb.ToString();
}
本体要素のすべての子段落を取得し、各段落のテキストを取得する短いプログラムを作成します。RevisionAccepter メソッドと LogicalChildrenContent 軸を使用することで、各段落のテキストを正しく取得できます。
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
XElement root = doc.MainDocumentPart.GetXDocument().Root;
XElement body = root.LogicalChildrenContent().First();
foreach (XElement blockLevelContentElement in body.LogicalChildrenContent())
{
if (blockLevelContentElement.Name == W.p)
{
var text = blockLevelContentElement
.LogicalChildrenContent()
.Where(e => e.Name == W.r)
.LogicalChildrenContent()
.Where(e => e.Name == W.t)
.Select(t => (string)t)
.StringConcatenate();
Console.WriteLine("Paragraph text >{0}<", text);
continue;
}
// If element is not a paragraph, it must be a table.
Console.WriteLine("Table");
}
}
}
}
問題のドキュメントに対してこの例を実行すると、次のように表示されます。
Paragraph text >Paragraph in content control.<
Paragraph text >Text in content control. Text following the content control.<
Paragraph text >Text in a following paragraph.<
Open XML SDK 2.0 for Microsoft Office へようこそ を使用する例は、次のようになります。
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test7.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
OpenXmlElement root = doc.MainDocumentPart.Document;
Body body = (Body)root.LogicalChildrenContent().First();
foreach (OpenXmlElement blockLevelContentElement in
body.LogicalChildrenContent())
{
if (blockLevelContentElement is Paragraph)
{
var text = blockLevelContentElement
.LogicalChildrenContent()
.OfType<Run>()
.Cast<OpenXmlElement>()
.LogicalChildrenContent()
.OfType<Text>()
.Select(t => t.Text)
.StringConcatenate();
Console.WriteLine("Paragraph text >{0}<", text);
continue;
}
// If element is not a paragraph, it must be a table.
Console.WriteLine("Table");
}
}
}
}
この例では、子孫のブロックレベルのコンテンツ コンテナーのランは調べていないので、例は設計されたとおり、テキスト ボックスのテキストを表示しません。
LogicalChildrenContent 軸メソッドの 2 つの効果的なオーバーロード
LogicalChildrenContent 軸メソッドの 2 つのオーバーロードをさらに定義することで、最後の例を簡単化できます。段落のすべてのランを取得し、ランのすべてのテキスト要素を取得することが、共通の操作です。このため、指定のタグ名でフィルター処理を行う 2 つの拡張メソッドを定義すると、コードがさらに簡単化されます。
public static IEnumerable<XElement> LogicalChildrenContent(this XElement element,
XName name)
{
return element.LogicalChildrenContent().Where(e => e.Name == name);
}
public static IEnumerable<XElement> LogicalChildrenContent(
this IEnumerable<XElement> source, XName name)
{
foreach (XElement e1 in source)
foreach (XElement e2 in e1.LogicalChildrenContent(name))
yield return e2;
}
この拡張メソッドを使用すると、クエリは次のように簡単化されます。
var text = blockLevelContentElement
.LogicalChildrenContent(W.r)
.LogicalChildrenContent(W.t)
.Select(t => (string)t)
.StringConcatenate();
このクエリの出力は、前の例の出力と同じです。
Open XML SDK 2.0 for Microsoft Office へようこそ に実装されているこの追加の拡張メソッドは、次のとおりです。
public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
this OpenXmlElement element, System.Type typeName)
{
return element.LogicalChildrenContent().Where(e => e.GetType() == typeName);
}
public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
this IEnumerable<OpenXmlElement> source, Type typeName)
{
foreach (OpenXmlElement e1 in source)
foreach (OpenXmlElement e2 in e1.LogicalChildrenContent(typeName))
yield return e2;
}
簡単化されたクエリは、次のようになります。
var text = blockLevelContentElement
.LogicalChildrenContent(typeof(Run))
.LogicalChildrenContent(typeof(Text))
.OfType<Text>()
.Select(t => t.Text)
.StringConcatenate();
LogicalChildrenContent メソッドで返された XML 要素の ID
LogicalChildrenContent メソッドで返される要素について、重要なことが 1 つあります。要素は、WordprocessingML ドキュメント内の実際の要素であり、コピーやクローンではありません。つまり、たとえば、スタイルの各種プロパティについて追加でフィルター処理を行う必要がある場合、この操作を簡単に行うことができます。
ドキュメントのテキストの検索
ドキュメント内の特定の文字列を検索する例を作成します。この例は、ドキュメントに変更履歴、コンテンツ コントロール、ハイパーリンク、または、段落のテキストを作成するときに問題となる他のどのような要素が含まれていても、正しく動作します。さらに、ブロックレベルのコンテンツ コンテナーにまたがるテキストも正しく検索します。
static void IterateContentAndSearch(XElement element, string searchString)
{
if (element.Name == W.p)
{
string paragraphText = element
.LogicalChildrenContent(W.r)
.LogicalChildrenContent(W.t)
.Select(s => (string)s)
.StringConcatenate();
if (paragraphText.Contains(searchString))
Console.WriteLine("Found {0}, paragraph: >{1}<", searchString, paragraphText);
}
foreach (XElement item in element.LogicalChildrenContent())
IterateContentAndSearch(item, searchString);
}
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
IterateContentAndSearch(doc.MainDocumentPart.GetXDocument().Root, "control");
}
}
}
Open XML SDK 2.0 for Microsoft Office へようこそ を使用する同じ例は、次のようになります。
static void IterateContentAndSearch(OpenXmlElement element, string searchString)
{
if (element is Paragraph)
{
string paragraphText = element
.LogicalChildrenContent(typeof(Run))
.LogicalChildrenContent(typeof(Text))
.OfType<Text>()
.Select(s => s.Text)
.StringConcatenate();
if (paragraphText.Contains(searchString))
Console.WriteLine("Found {0}, paragraph: >{1}<", searchString, paragraphText);
}
foreach (OpenXmlElement item in element.LogicalChildrenContent())
IterateContentAndSearch(item, searchString);
}
static void Main(string[] args)
{
byte[] docByteArray = File.ReadAllBytes("Test.docx");
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(docByteArray, 0, docByteArray.Length);
using (WordprocessingDocument doc =
WordprocessingDocument.Open(memoryStream, true))
{
RevisionAccepter.AcceptRevisions(doc);
IterateContentAndSearch(doc.MainDocumentPart.Document, "control");
}
}
}
まとめ
Open XML WordprocessingML を処理するプログラムを開発する場合は、ドキュメントの実際のコンテンツのみを考慮すると効果的です。この記事では、ドキュメントの論理コンテンツを格納すると考えられる要素を定義しています。また、軸メソッド LogicalChildrenContent の 4 つのオーバーロードも定義しています。
Open XML WordprocessingML ドキュメントの単純で堅牢な処理にとって、変更履歴を反映することが重要です。これにより、変更履歴で使用される 40 を越える要素と属性 (複雑なセマンティクスを持つものを含む) を無視できます。変更履歴の反映に加えてこれらの軸メソッドを使用することで、Open XML WordprocessingML ドキュメントからコンテンツを確実に抽出する短いプログラムを記述できます。
その他の技術情報
MSDN の「Open XML Developer Center (英語)」を参照してください。記事、ハウツー ビデオ、さまざまなブログ投稿へのリンクなど、多数のコンテンツが掲載されています。次のリンクからは Open XML SDK 2.0 の使用に関する重要な情報にアクセスできます。