Rechercher
 

Extraction de données par expressions régulières

Substitutions via des expressions régulières Utilisation de la classe java.util.Scanner



Accès rapide :
La vidéo
Extraction de données à partir d'une chaîne
Extraction de données via la méthode Pattern.split
Extraction de données via la méthode Matcher.find
Un exemple concret d'utilisation
Travaux pratiques
Le sujet
La correction

La vidéo

Cette vidéo vous montre comment reconnaître et extraire des parties d'une chaîne de caractères en utilisant des expressions régulières.


Extraction de données par expressions régulières

Extraction de données à partir d'une chaîne

Dans les chapitres précédents de ce cours, nous avons vu qu'il était possible de détecter la présence d'un motif (pattern matching, en anglais) et de remplacer les occurrences d'un motif via des expressions régulières. Nous allons maintenant voir qu'il est aussi possible d'extraire une chaîne de caractère correspondante à un motif, via une expression régulière.

Pour ce faire, nous allons continuer à utiliser les classes Pattern et Matcher. Deux principales manières de procéder sont possibles en fonction de la complexité de la chaîne de caractères et des informations à y extraire.

Extraction de données via la méthode Pattern.split

Cette manière de procéder n'est envisageable que dans des cas simples : il faut que la chaîne principale contienne des informations séparées entre elles pour un séparateur bien définit. Dans ce cas, il est possible de découper la chaîne à partir du séparateur et de récupérer les différentes données dans un tableau. Voici un exemple simple ou l'on récupère les informations de jour, de mois et d'années à partir d'une chaîne respectant le format jj/mm/aaaa ou le format jj-mm-aaaa.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
import java.util.regex.Pattern;

public class Extractions {

    public static void main(String[] args) {

        Pattern separatorRegExpr = Pattern.compile( "[/-]" );

        // --- Extract data with String.split ---
        String [] dates =  { "20-08-2010", "10/01/2017" };
        for (String date : dates) {
            // String [] dateParts = date.split( "[/-]" );      // Non optimisé
            String [] dateParts = separatorRegExpr.split( date );
            System.out.printf( "%s - %s - %s\n", dateParts[0], dateParts[1], dateParts[2] );
        }

    }
    
}
Exemple d'extraction de données via la méthode Pattern.split

Les données extraites sont stockées dans le tableau dateParts et sont, ensuite, utilisées durant l'affichage. Voici les résultats produits par ce programme.

$> java Extractions
20 - 08 - 2010
10 - 01 - 2017
$>

Extraction de données via la méthode Matcher.find

Cette seconde solution permet d'adresser des cas bien plus compliqués. Dans l'exemple suivant, nous utilisons une chaîne de caractères contenant un ensemble de tags XML : le but est d'en extraire les noms de tous les tags (ouvrants et fermants). Ce que je sous-entends, par en extraire les noms, c'est que je ne veux pas les caractères < et >. Pour y arriver, nous allons utiliser une paire de parenthèses afin de marquer la partie à extraire. Ensuite nous rechercherons toutes les occurrences via la méthode Matcher.find.

Le « matcher » va vous renvoyer un ensemble de résultats utilisable via les méthodes nommées group. Ce groupe de données contient, en première information (indice 0) la partie de la chaîne ayant été mise en correspondance avec l'expression régulière complète. En position 1, vous retrouverez la partie correspondante à la première paire de parenthèses utilisée pour extraire vos données. Si votre expression régulière possède plus d'une paire de parenthèse, vous récupérerez vos données avec les indices suivants.

Voici un code d'exemple : il prend soin d'afficher les informations du groupe de données du « matcher ». Regardez bien les données associées à htmlMatcher.group( 0 ) et à htmlMatcher.group( 0 ) (elles sont bien différentes). Mais ...

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Extractions {

    public static void main(String[] args) {

        // --- Extract data with Matcher class - Caution: regular expressions are greedy ---
        Pattern htmlPattern = Pattern.compile( "<(.+)>", Pattern.DOTALL );
        String htmlContent = "<html><head><title>Titre</title></head><body><h1>Titre visuel</h1></body></html>";
      
        Matcher htmlMatcher = htmlPattern.matcher( htmlContent );
        while ( htmlMatcher.find() ) {
            System.out.println( "Expression correspondante au motif: " + htmlMatcher.group( 0 ) );
            System.out.println( "Nom du tag: " + htmlMatcher.group( 1 ) );
        }

    }
    
}
Exemple d'extraction de données via la méthode Matcher.find

Le problème, c'est qu'on vient de se faire piéger : le programme ci-dessus ne fait pas ce qu'on a dit. Pour comprendre cela le mieux est de regarder ce qu'il produit.

$> java Extractions
Expression correspondante au motif: <html><head><title>Titre</title></head><body><h1>Titre visuel</h1></body></html>
Nom du tag: html><head><title>Titre</title></head><body><h1>Titre visuel</h1></body></html
$>

Le souci vient du fait qu'une expression régulière est dite « gourmande » (ou encore « gloutonne » ou « greedy » en anglais). Cela veut dire que l'expression régulière cherche à consommer un maximum de caractères. Dans notre cas, tout ce qui est contenu entre le premier < et le dernier > peut être consommé par .*.

Du coup, il faut « mettre l'expression régulière au régime » et lui demander de consommer le strict minimum, afin d'en laisser pour les autres. ;-) Cela se fait en rajoutant le caractère ? derrière le caractère de répétition * utilisé pour sélectionner le contenu d'un tag. En ajoutant un ? derrière un élément de répétition (*, +, {x,y}) on demande explicitement à ne pas être gourmand.

il ne faut pas confondre le caractère ? exprimant la facultativité avec celui exprimant la non-gourmandise (systématiquement placé après un élément de répétition).

Voici donc notre programme corrigé !

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Extractions {

    public static void main(String[] args) {

        // --- Extract data with Matcher class - Caution: regular expressions are greedy ---
        Pattern htmlPattern = Pattern.compile( "<(.+?)>", Pattern.DOTALL );
        String htmlContent = "<html><head><title>Titre</title></head><body><h1>Titre visuel</h1></body></html>";
      
        Matcher htmlMatcher = htmlPattern.matcher( htmlContent );
        while ( htmlMatcher.find() ) {
            System.out.println( "Expression correspondante au motif: " + htmlMatcher.group( 0 ) );
            System.out.println( "Nom du tag: " + htmlMatcher.group( 1 ) );
        }

    }
    
}
Exemple d'extraction de données via la méthode Matcher.find

Et voici les résultats produits par ce second exemple : c'est beaucoup mieux !

$> java Extractions
Expression correspondante au motif: <html>
Nom du tag: html
Expression correspondante au motif: <head>
Nom du tag: head
Expression correspondante au motif: <title>
Nom du tag: title
Expression correspondante au motif: </title>
Nom du tag: /title
Expression correspondante au motif: </head>
Nom du tag: /head
Expression correspondante au motif: <body>
Nom du tag: body
Expression correspondante au motif: <h1>
Nom du tag: h1
Expression correspondante au motif: </h1>
Nom du tag: /h1
Expression correspondante au motif: </body>
Nom du tag: /body
Expression correspondante au motif: </html>
Nom du tag: /html
$>

Un exemple concret d'utilisation

Voici un exemple qui pourra un jour vous servir : on cherche à extraire une information à partir des résultats produits par une commande du système d'exploitation utilisé. Une partie du code proposé est relatif à la récupération des résultats produits par la commande invoquée : je ne rentrerais pas, dans ce chapitre, dans les explications relatives à cette section de code.

Pour être plus précis, nous lançons la commande Linux ifconfig qui renvoi des informations sur les interfaces réseaux (sous Windows, vous pourriez utiliser la commande ipconfig). Cette commande produit plusieurs lignes d'informations : au milieu de ces lignes se trouve l'adresse IPv4 d'une carte réseau (wlp3s0). C'est cette information que nous souhaitons extraire.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Extractions {

    public static void main(String[] args) {
        
        // --- IP address extraction ---
        String result = execWithOutput( "ifconfig" );
        
        String interfaceName = "wlp3s0";
        String regExp = interfaceName + ":.+?inet (\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\s+netmask";
        Pattern pattern = Pattern.compile( regExp, Pattern.DOTALL );

        Matcher matcher = pattern.matcher( result );
        if ( matcher.find() ) {
            System.out.println( "Whole matched expression: " + matcher.group( 0 ) );
            System.out.println( "IP address: " + matcher.group( 1 ) );
        }
        
    }
    
    
    public static String execWithOutput( String programName ) {
        ProcessBuilder processBuilder = new ProcessBuilder( programName );
        processBuilder.redirectErrorStream(true);

        try {
            Process process = processBuilder.start();  
            try ( BufferedReader stream = new BufferedReader( new InputStreamReader( process.getInputStream() ) ) ) {
                StringBuilder builder = new StringBuilder();
                while ( true ) {
                    String line = stream.readLine();
                    if ( line == null ) break;
                    builder.append( line ).append( "\n" );
                }
                process.waitFor();
                return builder.toString();
            }
        } catch( Exception exception ) {
            throw new RuntimeException( "Cannot launch process " + programName );
        }
    }
    
}
Extraction de l'adresse IPv4 de l'interface wlp3s0
l'expression régulière utilisée pour extraire l'adresse IP (elle est placée entre la paire de parenthèses dans l'expression régulière) est beaucoup plus simple que celle étudiée dans le premier chapitre sur les expressions régulières : c'est normal ! Ici, nous ne cherchons pas à la valider et nous faisons confiance au système d'exploitation pour avoir produit une adresse IP correcte (il n'y aura donc pas de valeur supérieure à 255). Par contre, ce que l'on veut, c'est juste la localiser dans le texte pour l'extraire. Une version simplifiée sera parfaite du point de vue de la lisibilité du code.

Pour information, voici ce que renvoie la commande ifconfig sur ma machine.

$> ifconfig
enp4s0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether d8:50:e6:e0:3b:bd  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Boucle locale)
        RX packets 102215  bytes 17614550 (16.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 102215  bytes 17614550 (16.7 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlp3s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.27  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::5627:1eff:fe28:20bd  prefixlen 64  scopeid 0x20<link>
        ether 54:27:1e:28:20:bd  txqueuelen 1000  (Ethernet)
        RX packets 604057  bytes 504853419 (481.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 479704  bytes 196822093 (187.7 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
$>

Et maintenant voici le résultat produit par le programme sur ma machine.

$> java Extractions
Whole matched expression: wlp3s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.27  netmask
IP address: 192.168.1.27
$>

Travaux pratiques

Le sujet

Je vous demande de coder, via des expressions régulières, un mini parseur XML. Je vous fournis le code XML suivant :

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
public class XmlParser {

    public static void main(String[] args) {

        String data = "<data>\n"
                    + "    <subTag attr1='value' attr2='another value' />\n"
                    + "    <subTag2 attr3='value' attr4='another value' />\n"
                    + "</data>\n";
        
        // TODO: afficher les données comprises dans le texte XML sous ce format
        
        // data
        // subTag
        //     attr1: value
        //     attr2: anotherValue
        // subTag2
        //     attr3: value
        //     attr4: anotherValue
        
    }
    
}
Code de départ de l'exercice

Le but est d'afficher, les uns sous les autres, tous les tags ouvrant présents dans le document (sans les chevrons). Pour chaque tag, vous devez de plus afficher le nom de l'attribut ainsi que sa valeur, avec un retrait. Comme d'habitude, essayez de ne pas regarder la correction trop rapidement : je vous surveille. ;-)

La correction

Voici donc ma proposition de correction pour afficher, en respectant le format proposé, les données contenues dans la chaîne de caractères XML.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class XmlParser {

    public static void main(String[] args) {

        String data = "<data>\n"
                    + "    <subTag attr1='value' attr2='another value' />\n"
                    + "    <subTag2 attr3='value' attr4='another value' />\n"
                    + "</data>\n";
        
        // On veut au moins une première lettre (ou chiffre) pour éviter un tag fermant (</data>).
        // Et on essaye de ne pas prendre le caractère / des tags auto-fermants.
        String tagExtractionRegExp = "<([a-zA-Z_].*?)\\s*/?>";  
        Pattern tagPattern = Pattern.compile( tagExtractionRegExp, Pattern.DOTALL );
    
        // Le caractère : est autorisé pour le nom d'un tag (notion de namespace XML).
        String tagNameExtractionRegExp = "([a-zA-Z0-9_:-]+)";
        Pattern tagNamePattern = Pattern.compile( tagNameExtractionRegExp, Pattern.DOTALL );
        
        // On simplifie le problème en disant qu'une valeur d'attribut est constituée de lettres
        // et de chiffres. Notez la présence des deux paires de parenthèses pour sortir
        // le nom de l'attribut et sa valeur.
        String attributeExtractionRegExp = "([a-zA-Z0-9_:-]+)=['\"]([a-zA-Z0-9 ]*)['\"]";
        Pattern attributePattern = Pattern.compile( attributeExtractionRegExp, Pattern.DOTALL );
        
        
        Matcher htmlMatcher = tagPattern.matcher( data );
        while ( htmlMatcher.find() ) {
            // Récupération du contenu du tag
            String tagContent = htmlMatcher.group( 1 );
            
            // Affichage du nom du tag
            Matcher tagNameMatcher = tagNamePattern.matcher( tagContent );
            if ( tagNameMatcher.find() ) {
                System.out.println( tagNameMatcher.group( 1 ) );
            }
            
            // Affichage des attributs du tag courant
            Matcher attributeMatcher = attributePattern.matcher( tagContent );
            while( attributeMatcher.find() ) {
                System.out.printf( "    %s: %s\n", 
                                   attributeMatcher.group( 1 ),
                                   attributeMatcher.group( 2 ) );
            }
        }
        
    }
    
}
Mini moteur d'analyse de document XML


Substitutions via des expressions régulières Utilisation de la classe java.util.Scanner