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
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.
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.
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] ); } } } |
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 $>
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 ) ); } } } |
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.
?
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 ) ); } } } |
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 $>
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 ); } } } |
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 $>
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 } } |
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. ;-)
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 ) ); } } } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :