Analyse syntaxique des pages MediaWiki

Il existe plusieurs initiatives de programmes permettant d'interpréter la syntaxe de MediaWiki. Il y a le code PHP utilisé par MediaWiki et qui sert de référence, mais également plusieurs autres initiatives notamment en Java.

La plupart des initiatives Java ont pour objectif de produire des version XHTML ou PDF des pages en syntaxe wiki et ne donnent pas accès à l'arbre syntaxique de la page. Un programme en C basé sur Flex/Bison et développé dans le cadre du projet MediaWiki offre cette possibilité.

Parmi les initiatives en Java qui semblent intéressantes pour le collection reader UIMA :

J'ai tout d'abord essayé Bliki pour traiter la syntaxe de MediaWiki. Ce dernier s'est toutefois avéré assez complexe, notamment au niveau de son architecture, de telle façon que je n'ai pas réussi à obtenir ce que je voulais. Je me suis alors tourné vers WikiModel qui offre un analyseur syntaxique (Wiki Event Model (WEM)) au fonctionnement proche de SAX.

WikiModel pour MediaWiki

Le gros avantage de WikModel est qu'il offre directement un parseur pour MediaWiki : org.wikimodel.wem.mediawiki.MediaWikiParser. Le parseur analyse du code Wiki brute et, comme un parseur SAX classique, lance des évènements à une instance implémentant l'interface org.wikimodel.wem.IWemListener. J'ai choisi de créer une classe MediawikiCasConverter implémentant cette interface et gérant elle même le lancement du parseur sur les textes brutes extraits des révisions.

  1. import org.wikimodel.wem.mediawiki.MediaWikiParser
  2. import org.wikimodel.wem.IWemListener;
  3. ...
  4. public class MediawikiCasConverter implements IWemListener {
  5.  
  6. public MediawikiCasConverter() {
  7. theParser = new MediaWikiParser();
  8. }
  9.  
  10. public void runParser(String rawWikiText) throws WikiParserException {
  11. // We use a string reader to parse the raw wiki texte
  12. StringReader reader = new StringReader(rawWikiText);
  13. // Parsing
  14. theParser.parse(reader, this);
  15. }
  16.  
  17. // IWemListener methods below
  18. ...
  19. }

Les méthodes du IWemListener sont de deux types. Les premières sont des méthodes one shot, ie elles sont appelées une seule fois lorsqu'un élément est rencontré. Ce sont notamment celles qui concernent le texte et leur mise en forme :

  • onEscape(String str)
  • onSpace(String str)
  • onSpecialSymbol(String str)
  • onWord(String str)
  • onReference(String str)

Les secondes sont des méthodes à la SAX en deux temps : begin et end. Ce sont notamment celles qui concernent la structuration du texte :

  • beginHeader(int headerLevel, WikiParameters params)
  • endHeader(int headerLevel, WikiParameters params)
  • beginSection(int docLevel, int headerLevel, WikiParameters params)
  • endSection(int docLevel, int headerLevel, WikiParameters params)
  • beginParagraph(WikiParameters params)
  • endParagraph(WikiParameters params)
  • beginList(WikiParameters params, boolean ordered)
  • endList(WikiParameters params, boolean ordered)

La plupart de ces méthodes ne font que des appels à une fonction de plus haut niveau qui collecte les données textuelles de la page et qui maintient l'index du CAS : addToContent. C'est notamment le cas pour la méthode onWord qui est appelée lorsque le parseur rencontre un nouveau mot :

  1. /**
  2.  * This method adds the string in parameter into the collected content
  3.  * and then increment the offset by the size of this string.
  4.  *
  5.  * @param str the string to be added to the content
  6.  */
  7. protected void addToContent(String str) {
  8. if ( str != null ) {
  9. theTextContent.append(str);
  10. theOffset += str.length();
  11. }
  12. }
  13. ...
  14. /** Called when a word is encountered */
  15. public void onWord(String str) {
  16. addToContent(str);
  17. }

J'ai choisi de transposer une partie des informations contenues dans les pages de Wikipedia sous la forme d'annotations. J'ai ainsi ajouté les types :

  • Header pour les titres qui possède un attribut Level indiquant le niveau de titre ;
  • Paragraph pour différencier les paragraphes ;
  • Section pour repérer les sections même si je ne suis pas certain d'avoir bien compris le principe de section dans MediaWiki. Ce type possède les attributs Level pour le niveau de section (cf. niveau de titre), Parent pointant sur la section parente et Title pointant sur le Header correspondant à ladite section ;
  • Link pour les liens divers et variés qui possède des attributs Label et Link pour respectivement l'étiquette du lien et son adresse.

En ce qui concerne les liens, j'ai choisi pour le moment d'ignorer les images qui me semblent apporter plus de bruit qu'autre chose.

La mise en place de ces annotations se fait assez facilement :

  1. Lors d'un appel à begin..., l'annotation est créée est stockée dans une liste, la plupart des informations excepté son index de fin est renseigné à ce moment ;
  2. Lors de l'appel correspondant à end..., la dernière annotation de la liste est récupérée et on fixe son index de fin (setEnd) ;
  3. Lorsque l'analyse est terminée, toutes les annotations sont accessibles par la méthode getAnnotations afin de les ajouter à l'index du CAS.

Cette approche permet de dédier toute la partie analyse à une classe. Si jamais l'on veut ignorer certaines annotations (ou toutes), il suffit de les filtrer au moment de leur récupérer par getAnnotations. L'extrait de code ci-dessous illustre ce fonctionnement pour les titres :

  1. /**
  2.  * When we encounter a new header, we create an annotation for it.
  3.  */
  4. public void beginHeader(int headerLevel, WikiParameters params) {
  5. // Jump a line
  6. addToContent(" ");
  7. // Create the annotation
  8. Header header = new Header(theCas);
  9. header.setLevel(headerLevel);
  10. header.setBegin( theOffset );
  11. // Add it to the list
  12. theHeadersAnnotations.add( header );
  13. // Add it as header of the last unclosed section
  14. if ( theUnclosedSections.size() > 0 ) {
  15. Section section = theUnclosedSections.get( theUnclosedSections.size()-1 );
  16. section.setTitle(header);
  17. }
  18. }
  19.  
  20. /**
  21.  * Add the ending value of the last started header.
  22.  */
  23. public void endHeader(int headerLevel, WikiParameters params) {
  24. // Retrieve the last header
  25. Header header = theHeadersAnnotations.get( theHeadersAnnotations.size()-1 );
  26. // Update its ending value
  27. header.setEnd( theOffset );
  28. // Jump a line
  29. addToContent(" ");
  30. }

Je me suis rendu compte que le parseur posait des problèmes (heap overflow) sur des pages de certains espaces de noms. J'ai donc choisi de n'appliquer l'analyse syntaxique qu'aux pages de l'espace de nom 0 (espace de nom principal). Ceci est toutefois facilement modifiable dans la méthode uima.wikipedia.mwdumper.ThreadedMWDumper.writeStartPage.

Il reste pas mal d'améliorations possibles : prendre en compte toutes les mises en formes/structurations, prendre en charge les macros, rendre l'analyse syntaxique configurable. Toutefois, le composant est dans l'état actuel aussi complet et performant que je le souhaitais à l'origine. Il ne reste plus qu'à le tester de manière intensive. J'ai d'ailleurs deux trois cobayes en tête pour ça.

Nouvelle version du composant

Pas de nouvelle version du composant pour l'instant. Étant donné que les dépendances se sont multipliées, et que le composant est désormais suffisamment fonctionnel pour se voir estampillé 1.0 ou quelque chose du genre, je vais travailler un peu plus son packaging.

Autres articles