Maîtrise du texte dans les documents WordprocessingML Open XML

Résumé :   découvrez comme extraire du texte à partir de documents WordprocessingML Open XML de manière fiable.

Dernière modification : lundi 9 mars 2015

S’applique à : Office 2007 | Office 2010 | Open XML | Visual Studio Tools for Microsoft Office | Word 2007 | Word 2010

Dans cet article
Introduction
Présentation du contenu de texte en WordprocessingML
Meilleure pratique : accepter les révisions avant le traitement
Présentation des abstractions WordprocessingML
Augmentation de la complexité de traitement en fonction du niveau de hiérarchie
Introduction à la méthode d’axe LogicalChildrenContent
Implémentation de la méthode d’axe DescendantsTrimmed
Définition des enfants logiques
Utilisation de la méthode d’axe LogicalChildrenContent
Exemple ExamineDocumentContent
Extraction du texte des paragraphes
Deux surcharges utiles de la méthode d’axe LogicalChildrenContent
Identité des éléments XML renvoyés par la méthode LogicalChildrenContent
Recherche de texte dans des documents
Conclusion
Ressources supplémentaires

Sommaire

  • Introduction

  • Présentation du contenu de texte en WordprocessingML

  • Meilleure pratique : accepter les révisions avant le traitement

  • Présentation des abstractions WordprocessingML

  • Augmentation de la complexité de traitement en fonction du niveau de hiérarchie

  • Introduction à la méthode d’axe LogicalChildrenContent

  • Implémentation de la méthode d’axe DescendantsTrimmed

  • Définition des enfants logiques

  • Utilisation de la méthode d’axe LogicalChildrenContent

  • Exemple ExamineDocumentContent

  • Extraction du texte des paragraphes

  • Deux surcharges utiles de la méthode d’axe LogicalChildrenContent

  • Identité des éléments XML renvoyés par la méthode LogicalChildrenContent

  • Recherche de texte dans des documents

  • Conclusion

  • Ressources supplémentaires

Cliquez pour récupérer le code  Télécharger le code (éventuellement en anglais)

Introduction

Le traitement de texte dans les documents Open XML semble à première vue très simple : vous avez le corps du document, des paragraphes et des tableaux dans le corps, ainsi que des lignes et des cellules dans les tableaux, exactement comme en HTML. Mais les choses sont plus compliquées qu’il n’y paraît : on voit le balisage du suivi des révisions, des listes numérotées et à puces, des contrôles de contenu et du balisage qui n’affecte pas le texte, comme des signets et des commentaires. Les styles semblent ne pas affecter le texte, mais ils ont tout de même un impact s’il y a des listes numérotées et à puces. Il y a de nombreux éléments à prendre en considération, mais chacun de ces éléments, pris individuellement, ne pose pas trop de difficultés.

Ceci dit, certaines idées et abstractions fondamentales peuvent simplifier notre point de vue vis-à-vis du balisage de traitement de texte. Ces abstractions sont pertinentes dans tous les cas, que vous travailliez avec du balisage de traitement de texte à l’aide du modèle objet Kit de développement Open XML SDK 2.0 fortement typé à l’aide du kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office avec LINQ to XML, ou que vous utilisiez une autre plateforme, telle que Java ou PHP. Nous pouvons écrire du code qui gère ces abstractions. Le code peut exposer exactement les éléments qui nous intéressent, d’une manière organisée et prévisible. Cet article contient du code Microsoft Visual C# écrit avec LINQ to XML et avec le modèle objet Kit de développement Open XML SDK 2.0 fortement typé. La sémantique de certaines méthodes étant définie soigneusement, elle est facile à implémenter dans le langage et la plateforme de votre choix.

Présentation du contenu de texte en WordprocessingML

Dans le corps principal d’un document, tout le texte est contenu dans des paragraphes. Ceux-ci se trouvent à trois emplacements : en tant qu’enfant de l’élément de corps (w:body), en tant qu’enfant d’une cellule dans un tableau (w:tc) et en tant qu’enfant du contenu de zone de texte (w:txbxContent). Une cellule peut elle-même contenir un tableau. Il existe d’autres instances de texte dans la partie de document principale. Les images peuvent contenir du texte de remplacement et les graphiques SmartArt contiennent du texte. Cependant, ces éléments de texte sont plus isolés. Les problèmes liés à l’assemblage du texte de plusieurs chaînes en une chaîne unique ne s’appliquent pas à eux.

L’une des dynamiques intéressantes du contenu de texte est le fait qu’un paragraphe peut contenir une séquence, la séquence peut contenir un dessin, un dessin peut contenir une zone de texte, qui peut à son tour contenir des paragraphes. Il s’agit de l’unique circonstance où vous trouverez, dans du balisage WordprocessingML Open XML, un élément de paragraphe comme descendant d’un autre élément de paragraphe. Nous reparlerons de ce cas de figure et des problèmes qu’il pose un peu plus loin.

Meilleure pratique : accepter les révisions avant le traitement

Lorsqu’il s’agit de simplifier le traitement du contenu WordprocessingML, il convient avant toute chose d’accepter toutes les marques de révision. Pour plus d’informations sur la sémantique du suivi des révisions, voir Accepting Revisions in Open XML Word-Processing Documents. Vous pouvez également consulter l’exemple de code Microsoft Visual C# 3.0 qui porte sur l’acceptation des marques de révision dans le projet PowerTools for Open XML (éventuellement en anglais) sur CodePlex. Cliquez sur l’onglet Downloads, puis téléchargez RevisionAccepter.zip.

Le principal avantage offert par l’acceptation des marques de révision est qu’après cela, vous pouvez sans risque ignorer plus de 40 éléments qui compliquent le traitement du contenu. Une grande partie de ces éléments ont une sémantique complexe. Il est par conséquent préférable de les traiter en premier, puis de traiter le contenu du document. Jusqu’à ce que j’écrive cet article MSDN et que je rédige le code d’acceptation des révisions, je ne m’étais pas rendu compte du nombre de cas dans lesquels des approches plus simplistes entraînent l’extraction du texte incorrect pour un paragraphe.

Dans de nombreux cas, on souhaite interroger un document sans le modifier. On peut recourir à une technique simple qui consiste à lire le document dans un tableau d’octets, à créer un flux de mémoire redimensionnable à partir de ce tableau d’octets, puis à ouvrir le document à partir du flux de mémoire. Pour plus d’informations sur la manière de procéder, voir le billet de blog intitulé Simplification des requêtes WordprocessingML Open XML par l’acceptation préalable des révisions (éventuellement en anglais). Cet exemple vous permet d’accepter les révisions et d’interroger le document sans toucher réellement au document sur disque.

Présentation des abstractions WordprocessingML

Pour mieux comprendre le balisage WordprocessingML, définissons quelques abstractions :

  • conteneur de contenu au niveau du bloc ;

  • contenu au niveau du bloc ;

  • conteneur de contenu au niveau de la séquence ;

  • contenu au niveau de la séquence ;

  • contenu au niveau de la sous-séquence.

Après avoir accepté les marques de révision et décidé d’ignorer certains éléments qui ne s’appliquent qu’aux scénarios avancés, il nous reste les éléments suivants à traiter.

Conteneurs de contenu au niveau du bloc

Les conteneurs de contenu au niveau du bloc sont les éléments WordprocessingML qui renferment du contenu au niveau du bloc, tel que des paragraphes ou des tableaux. Seuls trois éléments conteneurs de contenu au niveau du bloc figurent dans la partie de document principale :

Éléments conteneurs de contenu au niveau du bloc

Élément

Nom de l’élément

Nom de la classe Kit de développement Open XML SDK 2.0

Espace de noms : DocumentFormat.OpenXml.Wordprocessing

Corps

w:body

Body

Cellule de tableau

w:tc

TableCell

Contenu de zone de texte

w:txbxContent

TextBoxContent

Comme mentionné précédemment, il existe d’autres conteneurs de contenu au niveau du bloc en WordprocessingML qui renferment des paragraphes, tels que l’élément w:comment dans la partie commentaires et l’élément w:hdr dans la partie en-tête. Cependant, ils ne se trouvent pas dans la partie de document principale et ne présentent donc pas les mêmes défis en matière de traitement.

Contenu au niveau du bloc

Les éléments de contenu au niveau du bloc sont les éléments WordprocessingML qui occupent toute la largeur de l’aire de dessin. Ils sont limités en haut et en bas et occupent la largeur disponible de gauche à droite. En guise d’exemple, durant la mise en page ordinaire d’un document, on ne voit pas deux paragraphes sur la même ligne physique, ni un paragraphe et un tableau côte à côte.

Il peut sembler y avoir des exceptions à cette règle, mais en réalité ces exceptions apparentes n’en sont pas. On peut par exemple voir des paragraphes côte à côte en cas d’utilisation d’une mise en page avec colonnes multiples. Dans ce cas, la largeur disponible pour la mise en page du paragraphe ou du tableau est la colonne, et non la page entière. Une zone de texte sur la page constitue un autre exemple, mais dans ce cas la largeur disponible pour la mise en page du contenu au niveau du bloc n’inclut pas l’espace réservé pour la zone de texte. Par ailleurs, la zone de texte proprement dite possède sa propre aire de dessin.

Après l’acceptation des révisions, il ne reste que deux éléments de contenu au niveau du bloc.

Éléments de contenu au niveau du bloc

Élément

Nom de l’élément

Nom de la classe Kit de développement Open XML SDK 2.0

Espace de noms : DocumentFormat.OpenXml.Wordprocessing

Paragraphe

w:p

Paragraph

Tableau

w:tbl

Table

Il existe deux autres éléments de contenu au niveau du bloc dont je ne discuterai pas dans cet article : ceux relatifs aux formules mathématiques. Le traitement de contenu de texte MathML n’est pas un besoin courant. En effet, il est rare qu’il faille recueillir le texte d’une formule et l’agréger dans une chaîne unique (comme on le fait pour un paragraphe). Au lieu de cela, le texte d’une formule doit être pris dans le contexte de la formule. Cet article n’aborde pas le traitement des formules MathML.

Conteneurs de contenu au niveau de la séquence

Après l’acceptation des révisions, il existe un seul élément qui est un conteneur de contenu au niveau de la séquence, l’élément de paragraphe (w:p). Le conteneur de contenu au niveau de la séquence définit l’espace dans lequel le contenu au niveau de la séquence est disposé de gauche à droite ou, selon le cas, de droite à gauche. En guise d’exemple, les différentes séquences de texte dans un paragraphe sont disposées horizontalement avec un habillage, dans leur police respective. Notez qu’un paragraphe est à la fois un élément de contenu au niveau du bloc et un élément conteneur de contenu au niveau de la séquence, tandis qu’un tableau est uniquement un élément de contenu au niveau du bloc, et non un élément conteneur de contenu au niveau de la séquence.

Éléments conteneurs de contenu au niveau de la séquence

Élément

Nom de l’élément

Nom de la classe Kit de développement Open XML SDK 2.0

Espace de noms : DocumentFormat.OpenXml.Wordprocessing

Paragraphe

w:p

Paragraph

Contenu au niveau de la séquence

Le contenu au niveau de la séquence est le contenu situé à l’intérieur d’un paragraphe dont la mise en forme est spécifique à une sous-section du paragraphe. Par exemple, une séquence est dans une police spécifique. Après l’acceptation des révisions, il ne reste que trois éléments de contenu au niveau de la séquence.

Éléments de contenu au niveau de la séquence

Élément

Nom de l’élément

Nom de la classe Kit de développement Open XML SDK 2.0

Espace de nom : DocumentFormat.OpenXml.Wordprocessing

Séquence de texte

w:r

Run

Dessin VML

w:pict

Picture

Objet DrawingML

w:drawing

Drawing

L’un des aspects non intuitifs de cette liste d’éléments est le fait qu’un objet de dessin VML (Vector Markup Language) ou un objet DrawingML est un contenu au niveau de la séquence ou un contenu au niveau de la sous-séquence. Tous deux peuvent également contenir comme descendant l’élément w:txbxContent, qui est un conteneur de contenu au niveau du bloc.

Contenu au niveau de la sous-séquence

Le contenu au niveau de la sous-séquence est constitué des éléments WordprocessingML qui font partie d’une séquence. Une séquence peut par exemple contenir plusieurs éléments de texte (w:t).

Éléments de contenu au niveau de la sous-séquence

Élément

Nom de l’élément

Nom de la classe Kit de développement Open XML SDK 2.0

Espace de noms : DocumentFormat.OpenXml.Wordprocessing

Saut

w:br

Break

Retour chariot

w:cr

CarriageReturnPicture

Bloc de date – Format de jour long

w:daylong

DayLong

Bloc de date – Format de jour long

w:daylong

DayLong

Bloc de date – Format de jour court

w:dayShort

DayShort

Objet DrawingML

w:drawing

Drawing

Bloc de date – Format de mois long

w:monthLong

MonthLong

Bloc de date – Format de mois court

w:monthShort

MonthShort

Caractère de trait d’union insécable

w:noBreakHyphen

NoBreakHyphen

Bloc de numéro de page

w:pgNum

PageNumber

Dessin VML

w:pict

Drawing

Caractère de tabulation de position absolue

w:pTab

PositionalTab

Caractère de trait d’union conditionnel

w:softHyphen

SoftHyphen

Caractère de symbole

w:sym

SymbolChar

Texte

w:t

Text

Caractère de tabulation

w:tab

TabChar

Bloc de date – Format d’année long

w:yearlong

YearLong

Bloc de date – Format d’année court

w:yearShort

YearShort

Cette liste contient également les objets de dessin VML et DrawingML, qui peuvent contenir un élément w:txbxContent (un conteneur de contenu au niveau du bloc) comme descendant.

Augmentation de la complexité de traitement en fonction du niveau de hiérarchie

Un exemple simple peut illustrer le problème que nous essayons de résoudre. Le premier paragraphe du document suivant comporte un contrôle de contenu et une zone de texte :

Figure 1. Document avec un contrôle de contenu et une zone de texte

Document avec contrôle de contenu et zone de texte

L’exemple de code suivant montre le balisage de ce paragraphe. Pour plus d’informations sur ce balisage, voir ISO/IEC 29500-1:2008 (éventuellement en anglais) ou Norme ECMA-376 sur les formats de fichiers Office Open XML, Deuxième Édition (ECMA-376 deuxième édition) (éventuellement en anglais).

Notes

Le balisage superflu est omis afin de mieux illustrer le problème.

<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>

Dans cet exemple, le texte de la zone de texte se trouve dans le même paragraphe que celui qui se trouve dans le contrôle de contenu. Il est également dans le même paragraphe que le texte qui se trouve en dehors du contrôle de contenu. Ce dernier fait en sorte que les éléments de texte soient à différents niveaux de la hiérarchie. Vous devez écrire du code qui gère cette différence hiérarchique. Cet exemple fournit une illustration du problème. Plusieurs abstractions WordprocessingML sont susceptibles de faire en sorte que du contenu de texte se trouve à différents niveaux de retrait. Il faut par conséquent développer une solution généralisée à ce problème.

Notes

L’extraction du texte de l’élément de paragraphe (w:p) à l’aide de la propriété Value n’est pas correct.

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);
}

Le texte renvoyé est incorrect.

Figure 2. Résultats incorrects de l’utilisation de la valeur de paragraphe

Résultats incorrects

Le problème n’est pas que le contenu de la zone de texte s’affiche deux fois, mais qu’il s’affiche tout court. Le texte de la zone de texte ne fait pas vraiment partie du paragraphe. Il est autonome.

Vous ne pouvez pas itérer les séquences enfants du paragraphe car le contrôle de contenu fait en sorte que les séquences de texte figurent à différents niveaux dans la hiérarchie du balisage.

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());
}

Ce code n’inclut pas le texte du contrôle de contenu.

Figure 3. Résultats incorrects de la concaténation des séquences enfants d’un paragraphe

Résultats incorrects

On pourrait écrire du code qui gère ce problème en tant que cas particulier. Toutefois, cela ne renvoie pas les résultats corrects pour les autres constructions qui font en sorte que du contenu de texte figure à différents niveaux hiérarchiques. Ce qu'il nous faut, ce sont plutôt des abstractions généralisées qui facilitent le traitement du contenu de texte des documents.

Le document Norme ECMA-376 : formats de fichiers Office Open XML, première édition (ECMA-376) (éventuellement en anglais) présente les mêmes problèmes lorsque du contenu se trouve dans la hiérarchie XML. L’élément qui contient la zone de texte en tant que descendant est un frère de l’autre contenu au niveau de la sous-séquence dans le paragraphe. Les abstractions décrites dans cet article s’appliquent également au balisage 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>

Introduction à la méthode d’axe LogicalChildrenContent

Pour résoudre ce problème, j’ai écrit une méthode d’axe qui renvoie le contenu des enfants logiques d’un élément. Les enfants logiques comprennent le contenu qui se trouve dans d’autres éléments qui augmentent le niveau de hiérarchie du contenu, tel qu’un contrôle de contenu. Par conséquent, cet axe de contenu d’enfants logiques diffère de l’axe d’enfants LINQ to XML (ou XPath). Les éléments qui augmentent réellement le niveau hiérarchique (w:sdt, w:fldsimple et w:hyperlink) ne sont pas inclus dans la collection renvoyée. Ce que nous voulons, c’est le contenu réel, et non les autres éléments qui renferment du contenu.

Conseil

J’emprunte le terme méthode d’axe à LINQ to XML. Le terme « axe », dans le contexte des documents XML, renvoie au fait que, pour tout élément donné, il existe un ensemble spécifique d’éléments associés, et une méthode d’axe renvoie une collection de ces éléments associés. Par exemple, pour un élément XML donné, il existe un ensemble spécifique d’éléments enfants, un ensemble spécifique de descendants et un ensemble spécifique d’ancêtres. Les descendants, éléments enfants et ancêtres forment la base de certaines méthodes d’axe LINQ to XML.

La liste suivante indique les éléments qui figurent dans la collection renvoyée si vous extrayez le contenu d’enfants logiques de l’élément de corps. L’élément de paragraphe à l’intérieur de la zone de texte n’est pas inclus dans les enfants logiques, car ce paragraphe est un enfant logique de l’élément de contenu de zone de texte (w:txbxContent) qui le contient. L’élément de contenu de zone de texte est un enfant logique de l’élément d’image VML (w:pict), qui est un descendant logique de la séquence qui le contient.

<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>

La liste suivante indique le contenu d’enfants logiques du second paragraphe. Aucun des descendants de la première séquence ne figure dans les enfants logiques.

  . . .
 <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>

L’élément enfant logique de la première séquence dans ce paragraphe est l’élément 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>

Il est utile que mc:AlternateContent soit l’un des éléments de contenu d’enfants logiques, car il contient des informations sur les autres approches possibles pour le traitement du contenu. L’enfant logique de l’élément mc:AlternateContent est son dessin contenu :

    <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>

L’enfant logique de l’objet DrawingML est le contenu de zone de texte (w:txbxContents). Son enfant est le paragraphe encadré. En définissant l’axe d’enfants logiques de cette manière, il est facile d’assembler le texte de manière exacte pour n’importe quel paragraphe.

Implémentation de la méthode d’axe DescendantsTrimmed

La première étape dans l’implémentation de la méthode d’axe d’enfants logiques consiste à implémenter une méthode qui renvoie une collection d’éléments descendants où les descendants sont tronqués. Tout élément descendant d’une balise spécifique n’est pas inclus dans la collection renvoyée. Une autre surcharge de la méthode DescendantsTrimmed prend un délégué comme argument. Elle vous permet de spécifier une expression lambda comme prédicat de sorte que vous puissiez effectuer une troncation en fonction de plusieurs balises. Je définis la sémantique de cette méthode de telle façon que les éléments tronqués proprement dits soient inclus dans la collection renvoyée.

L’exemple de code suivant illustre la sémantique de la méthode d’axe DescendantsTrimmed. Dans celle-ci, les éléments qui sont des descendants de l’élément txbxContent sont tronqués. L’exemple de code qui affiche le nom de chaque élément compte les ancêtres afin de mettre correctement en retrait les noms des éléments.

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);

Cet exemple affiche une liste avec mise en retrait du nom de chaque élément figurant dans la collection renvoyée.

  p
    r
      t
    r
      pict
        txbxContent
    r
      t

Définition des enfants logiques

La méthode DescendantsTrimmed vous permet d’implémenter une méthode d’axe qui renvoie uniquement les enfants logiques d’un ensemble spécifique d’éléments. Voici comment je définis les enfants logiques :

  • Le seul enfant logique de l’élément w:document est l’élément w:body.

  • Les enfants logiques d’un conteneur de contenu au niveau du bloc (w:body, w:tc et w:txbxContent) sont le contenu au niveau du bloc (w:p, w:tbl).

  • Les enfants logiques d’un tableau (w:tbl) sont ses lignes (w:tr).

  • Les enfants logiques d’une ligne (w:tc) sont ses cellules (w:tr).

  • Les enfants logiques d’un paragraphe (w:p) sont ses séquences (w:r).

  • Les enfants logiques d’une séquence (w:r) sont le contenu au niveau de la sous-séquence (w:t, w:pict, w:drawing, et ainsi de suite.) Voir la liste plus haut dans cet article. De plus, afin de satisfaire aux spécifications d’Office 2010 et d’ISO/IEC 29500, l’élément mc:AlternateContent est également un enfant d’une séquence. J’ai implémenté le code associé de sorte qu’il fonctionne avec ECMA-376 première édition et avec ISO/IEC 29500 (ECMA-376 deuxième édition).

  • L’enfant logique d’un élément de contenu de remplacement est un dessin ou une image dans l’élément mc:Choice. On souhaite traiter le contenu de l’élément mc:Choice, et non l’élément mc.Fallback.

  • Les enfants logiques d’un objet de dessin VML (w:pict) ou d’un objet DrawingML (w:drawing) sont tous les éléments de contenu des zones de texte contenues (w:txbxContent). Si dans votre scénario vous devez traiter d’autres parties spécifiques d’un objet VML ou DrawingML, vous pouvez redéfinir la méthode LogicalChildrenContent de façon à inclure les éléments que vous devez traiter dans la collection renvoyée.

Utilisation de la méthode d’axe LogicalChildrenContent

Avant d’examiner l’implémentation de la méthode LogicalChildrenContent, portons notre attention sur son utilisation.

La figure suivante montre l’exemple de document qui présentait quelques défis.

Figure 4. Document avec un contrôle de contenu et une zone de texte

Document avec contrôle de contenu et zone de texte

Exemple ExamineDocumentContent

Ce premier exemple itère de manière récursive tout le contenu logique d’un document et affiche le nom de chaque élément avec une mise en retrait correcte. S’il s’agit d’un élément de texte (w:t), la fonction imprime le contenu de texte de l’élément.

Notez que cet exemple accepte en premier lieu les révisions en appelant la méthode RevisionAccepter.AcceptRevisions. Il ouvre le document de traitement de texte en lisant d’abord le document dans un tableau d’octets, puis il initialise un flux de mémoire redimensionnable à partir du tableau d’octets. Cela lui permet d’ouvrir le document avec le paramètre de modification défini sur la valeur « true », et par conséquent d’accepter les révisions. Si l’exemple ouvrait directement le document pour modification, il modifierait le document existant en acceptant les révisions, ce qui risquerait de constituer un effet secondaire indésirable. S’il ouvrait le document en mode lecture seule, l’acceptation des révisions échouerait (elle lèverait une exception).

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);
        }
    }
}

Lorsque j’exécute cet exemple pour le document à problème, j’obtiens ce qui suit.

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.<

On constate que suite à diverses sessions de modifications, diverses séquences ont été fractionnées en séquences multiples. On peut observer la zone de texte et son contenu à l’emplacement approprié.

Nous pouvons implémenter les mêmes méthodes d’axes à l’aide du modèle objet fortement typé du kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office. Le code d’utilisation de l’axe de contenu logique ressemble à ce qui suit.

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);
        }
    }
}

Lorsque j’exécute cet exemple pour le document à problème, j’obtiens ce qui suit.

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.<

Extraction du texte des paragraphes

Il arrive souvent que l’on souhaite traiter un document et, en une même opération, extraire tous les paragraphes, toutes les séquences sous chaque paragraphe et tous les éléments de texte de chaque séquence, puis assembler le texte associé de chaque paragraphe.

Pour simplifier le plus possible cette opération, je vais écrire une autre surcharge de la méthode LogicalChildrenContent. Il est utile de l’écrire en tant que méthode d’extension qui prend comme argument une collection d’éléments de contenu et renvoie comme collection l’ensemble d’éléments enfants logiques de chaque élément dans la collection source. Cette méthode d’extension est comparable aux méthodes d’extension en LINQ to XML qui renvoient tous les éléments enfants de chaque élément d’une collection source. Cette méthode d’extension est très simple à implémenter.Elements

public static IEnumerable<XElement> LogicalChildrenContent(this IEnumerable<XElement> source)
{
    foreach (XElement e1 in source)
        foreach (XElement e2 in e1.LogicalChildrenContent())
            yield return e2;
}

La même méthode d’axe, implémentée à l’aide du modèle objet fortement typé du kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office, ressemble à ce qui suit.

public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
    this IEnumerable<OpenXmlElement> source)
{
    foreach (OpenXmlElement e1 in source)
        foreach (OpenXmlElement e2 in e1.LogicalChildrenContent())
            yield return e2;
}

Il est également utile d’utiliser une autre méthode d’extension, la méthode StringConcatenate, qui est une opération d’agrégation de chaînes.

public static string StringConcatenate(this IEnumerable<string> source)
{
    StringBuilder sb = new StringBuilder();
    foreach (string s in source)
        sb.Append(s);
    return sb.ToString();
}

Nous pouvons maintenant écrire un petit programme pour extraire tous les paragraphes enfants de l’élément de corps et extraire le texte de chaque paragraphe. En faisant appel à la méthode RevisionAccepter et aux axes LogicalChildrenContent, nous sommes certains de pouvoir extraire correctement le texte de chaque paragraphe.

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");
            }
        }
    }
}

Lorsque j’exécute ce programme pour le document à problème, j’obtiens ce qui suit.

Paragraph text >Paragraph in content control.<
Paragraph text >Text in content control. Text following the content control.<
Paragraph text >Text in a following paragraph.<

L’exemple qui utilise le kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office ressemble à ce qui suit.

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");
            }
        }
    }
}

Cet exemple n’examinant pas les séquences pour les conteneurs de contenu au niveau du bloc, il n’affiche pas le texte de la zone de texte.

Deux surcharges utiles de la méthode d’axe LogicalChildrenContent

Il est possible de simplifier le dernier exemple en définissant deux surcharges supplémentaires de la méthode d’axe LogicalChildrenContent. Une opération courante consiste à extraire toutes les séquences d’un paragraphe et tous les éléments de texte d’une séquence. Par conséquent, si nous définissons deux méthodes d’extension supplémentaires qui filtrent selon un nom de balise spécifié, le code se verra simplifié encore davantage.

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;
}

Lors de l’utilisation de ces méthodes d’extension, la requête est simplifiée comme suit.

var text = blockLevelContentElement
   .LogicalChildrenContent(W.r)
   .LogicalChildrenContent(W.t)
    .Select(t => (string)t)
    .StringConcatenate();

Cette requête génère la même sortie que l’exemple précédent.

Ces méthodes d’extension supplémentaires implémentées dans le kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office sont les suivantes.

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;
}

La requête simplifiée ressemble à ce qui suit.

var text = blockLevelContentElement
   .LogicalChildrenContent(typeof(Run))
   .LogicalChildrenContent(typeof(Text))
   .OfType<Text>()
    .Select(t => t.Text)
    .StringConcatenate();

Identité des éléments XML renvoyés par la méthode LogicalChildrenContent

Il y a une chose importante à noter concernant les éléments renvoyés par la méthode LogicalChildrenContent : il s’agit réellement des éléments du document WordprocessingML, et non de copies ou de clones. Cela signifie entre autres qu’il est très facile d’effectuer un filtrage supplémentaire sur les différentes propriétés de style.

Recherche de texte dans des documents

Nous pouvons maintenant écrire un exemple qui recherche une chaîne spécifique dans un document. Cet exemple fonctionne correctement si le document contient des marques de révision, des contrôles de contenu, des liens hypertexte ou tout autre élément qui pose un défi lors de l’assemblage du texte des paragraphes. Par ailleurs, il trouve correctement le texte qui chevauche plusieurs conteneurs de contenu au niveau du bloc.

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");
        }
    }
}

Le même exemple avec le kit Bienvenue dans le Kit de développement Open XML SDK 2.0 pour Microsoft Office ressemble à ce qui suit.

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");
        }
    }
}

Conclusion

Lorsque l’on développe un programme qui traite du WordprocessingML Open XML, il est souvent utile de prendre uniquement en compte le contenu réel du document. Cet article définit les éléments que j’estime renfermer le contenu logique d’un document. Je définis également quatre surcharges d’une méthode d’axe, LogicalChildrenContent.

Il est essentiel qu’un traitement simple et robuste des documents WordprocessingML Open XML accepte les marques de révision. Cela nous permet d’écrire du code qui ignore plus de 40 éléments et attributs (certains à la sémantique complexe) servant à effectuer le suivi des révisions. L’utilisation de ces méthodes d’axes, combinée à l’acceptation des marques de révision, nous permet d’écrire de petits programmes qui extraient de manière fiable le contenu de documents WordprocessingML Open XML.

Ressources supplémentaires

Cliquez pour récupérer le code  Télécharger le code (éventuellement en anglais)

Pour obtenir des articles, des vidéos de procédures et des liens vers de nombreux billets de blogs, voir le Centre de développement Open XML sur MSDN (éventuellement en anglais). Les liens suivants fournissent des informations importantes pour la prise en main du Kit de développement Open XML SDK 2.0 :

-
Téléchargement : Kit de développement Open XML SDK 2.0 (éventuellement en anglais)

-
Article : Creating Documents by Using the Open XML Format SDK 2.0 (Part 1 of 3)

-
Article : Creating Documents by Using the Open XML Format SDK 2.0 (Part 2 of 3)

-
Article : Creating Documents by Using the Open XML Format SDK 2.0 (Part 3 of 3)