Expressions régulières

RegEx en Java

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Les expressions régulières (RegEx) sont très utiles dans le traitement des données textuelles. Grâce à elles on peut facilement extraire un fragment du texte ainsi que modifier un mot correspondant au modèle global défini.

Le principe des RegEx (Regular Expressions) reste le même, quelque soit le langage. Les développeurs PHP ne devrait pas avoir du mal à se retrouver dans le monde des expressions régulières en Java. Ils devront juste connaître quelques régles de base et des pièges à éviter.

En Java le package java.util.regex est chargé de manipuler les chaînes de caractères avec l'utilisation des expressions régulières. Il contient deux classes Pattern et Matcher. La première consiste à préparer le modèle de caractères à manipuler. La deuxième classe exécute les opérations sur les chaînes de caractères. Regardons cela sur un exemple :

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexExemple {
    public static void main(String[] args) {
        if (args.length >= 1) {
            System.out.println("Trying to match string with RegEx pattern : " + args[0]);
            String text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."+
	        "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).";
            Pattern model = Pattern.compile(args[0]);
            Matcher match = model.matcher(text);

            while (match.find()) {     
                System.out.println("Found text " + match.group() + " at position " + match.start() + " and " + match.end());
            }
        }       
    }
}

Pour l'appeler, on doit indiquer en paramètre le pattern qui se trouvera dans Pattern.compile(args[0]), par exemple :

- java RegexExemple Lorem donne :

Trying to match string with RegEx pattern : Lorem
Found text Lorem at position 0 and 5
Found text Lorem at position 75 and 80
Found text Lorem at position 446 and 451
Found text Lorem at position 562 and 567
Found text Lorem at position 718 and 723
Found text Lorem at position 942 and 947

- java RegexExemple "Lorem [a-zA-z]+" donne :

Trying to match string with RegEx pattern : Lorem [a-zA-z]+
Found text Lorem Ipsum at position 0 and 11
Found text Lorem Ipsum at position 75 and 86
Found text Lorem Ipsum at position 446 and 457
Found text Lorem Ipsum at position 562 and 573
Found text Lorem Ipsum at position 718 and 729
Found text Lorem Ipsum at position 942 and 953

Dans les expressions on peut également préciser comment elles doivent se comporter. Pour cela on utilisera les constantes de la classe Pattern :

- CANON_EQ : active la recherche par rapport à la représentation canonique des patterns. Si ce flag est présent, Java décompose chaque caractère inclus dans la chaîne analysée et le compare à sa plus petite représentation binaire. Cette opération exige beaucoup de ressources et peut être pénalisante pour les performances du programme. Cependant, sur cet exemple on ne le constatera pas :

String testCanonical = "a\u0308\u0061\u0308";
Pattern patternNotCan = Pattern.compile("ä");
Matcher matcherNotCan = patternNotCan.matcher(testCanonical);
Pattern patternCan = Pattern.compile("ä", Pattern.CANON_EQ);
Matcher matcherCan = patternCan.matcher(testCanonical);
System.out.println("Not canonical matcher result is " + matcherNotCan.find());
System.out.println("Canonical matcher result is " + matcherCan.find());

Et le résultat :

Not canonical matcher result is false
Canonical matcher result is true

- CASE_INSENSITIVE : active la recherche insensible à la différence entre majuscules et minuscules, comme sur cet exemple :

String testCase = "B";
Pattern patternSens = Pattern.compile("b");
Matcher matcherSens = patternSens.matcher(testCase);
Pattern patternNonSens = Pattern.compile("b", Pattern.CASE_INSENSITIVE);
Matcher matcherNonSens = patternNonSens.matcher(testCase);
System.out.println("Result without case insesitive flag " + matcherSens.find());
System.out.println("Result with case insesitive flag " + matcherNonSens.find());

Et le résultat :

Result without case insesitive flag false
Result with case insesitive flag true

- COMMENTS : active les commentaires dans le pattern. Regardons cela sur un exemple :

String testString = "a a bb cc 2 3 dd";
Pattern patternWithout = Pattern.compile("a#get all a letters from string");
Pattern patternWith = Pattern.compile("a#get all a letters from string", Pattern.COMMENTS);
Matcher matchWithout = patternWithout.matcher(testString);
Matcher matchWith = patternWith.matcher(testString);
System.out.println("Does pattern without Pattern.COMMENTS matche a letters ? " + matchWithout.find());
System.out.println("Does pattern with Pattern.COMMENTS matche a letters ? " + matchWith.find());

Voici le résultat qui explique tout :

Does pattern without Pattern.COMMENTS matche a letters ? false
Does pattern with Pattern.COMMENTS matche a letters ? true

- DOTALL : active le marquage des caractères prenant en compte les sauts de ligne (\n, \r\n). Voici un exemple :

String inputStr = "123\n456";
String patternStr = ".*3.+4.*";
Pattern pattern = Pattern.compile(patternStr, Pattern.DOTALL);
Pattern patternNormal = Pattern.compile(patternStr);
Matcher matcher = pattern.matcher(inputStr);
Matcher matcherNormal = patternNormal.matcher(inputStr);
boolean matchFound = matcher.matches();
boolean matchNormalFound = matcherNormal.matches();
System.out.println("Found matched value with Pattern.DOTALL " + matchFound);
System.out.println("Found matched value without Pattern.DOTALL " + matchNormalFound);

Et le résultat :

Found matched value with Pattern.DOTALL true
Found matched value without Pattern.DOTALL false

- LITERAL : enlève une signification spéciale à des meta caractères. Autrement dit, un \\d ne sera plus considéré comme un marqueur pour des chiffres. Il sera une simple chaîne de caractères. On peut le constater sur l'exemple ci-dessous :

String testLiteral = "2";
String regex = "\\d";
Pattern patternNoLiteral = Pattern.compile(regex);
Matcher matcherNoLiteral = patternNoLiteral.matcher(testLiteral);
Pattern patternLiteral = Pattern.compile(regex, Pattern.LITERAL);
Matcher matcherLiteral = patternLiteral.matcher(testLiteral);
System.out.println("Result without literal flag " + matcherNoLiteral.find());
System.out.println("Result with literal flag " + matcherLiteral.find());

Et le résultat l'illustrant :

Result without literal flag true
Result with literal flag false

- MULTILINE : active le mode de marquage sur plusieurs lignes, comme sur cet exemple :

String testMultiLines = "line1\nline2\nline3";
String regex = "^line2$";
Pattern patternSingleLine = Pattern.compile(regex);
Matcher matcherSingleLine = patternSingleLine.matcher(testMultiLines);
Pattern patternMultiLine = Pattern.compile(regex, Pattern.MULTILINE);
Matcher matcherMultiLine = patternMultiLine.matcher(testMultiLines);
System.out.println("Result without multiline flag " + matcherSingleLine.find());
System.out.println("Result with multiline flag " + matcherMultiLine.find());

Le résultat est :

Result without multiline flag false
Result with multiline flag true

- UNICODE_CASE : active le marquage des chaînes de caractères contenant les symboles non-anglais (comme par exemple lettres accentuées). Voici un exemple :

String testUnicode = "FRANÇAIS";
String regex = "français";
Pattern patternNormal = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcherNormal = patternNormal.matcher(testUnicode);
Pattern patternUnicode = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
Matcher matcherUnicode = patternUnicode.matcher(testUnicode);
System.out.println("Result without unicode flag " + matcherNormal.find());
System.out.println("Result with unicode flag " + matcherUnicode.find());

Le résultat est le suivant :

Result without unicode flag false
Result with unicode flag true

- UNIX_LINES : active uniquement le marquage des sauts de ligne Unix (\n). Normalement RegEx accepte les sauts de ligne de différents systèmes (\n, \r\n, \r). Avec ce flag, il va uniquement prendre en compte les sauts de ligne Unix, comme sur cet exemple :

String testLines = "Line-1\r\n"+"Line-2\r\n";
String testLinesUnix = "Line-1\n"+"Line-2\n";
String patternLines = "^Line-2$";
Pattern pattUnix = Pattern.compile(patternLines, Pattern.MULTILINE | Pattern.UNIX_LINES);
Pattern pattNotUnix = Pattern.compile(patternLines, Pattern.MULTILINE);
Matcher unixMatcher = pattUnix.matcher(testLines);
Matcher notUnixMatcher = pattNotUnix.matcher(testLines);
Matcher unixLine = pattUnix.matcher(testLinesUnix);
System.out.println("Does UNIX_LINES allow not Unix new lines ? " + unixMatcher.find());
System.out.println("Result without UNIX_LINES flag ? " + notUnixMatcher.find());
System.out.println("UNIX_LINES result on \\n string : " + unixLine.find());

Et le résultat qui explique la différence :

Does UNIX_LINES allow not Unix new lines ? false
Result without UNIX_LINES flag ? true
UNIX_LINES result on \n string : true
http://www.kodejava.org/examples/772.html http://easyprograming.com/index.php/java/55-using-regex-regular-expressions-in-java#unix-lines

Est-ce que les expressions régulières en Java sont thread safety ? Oui et non. Uniquement les instances de la classe Pattern sont thread safety. Elles représentent une classe immuable donc chaque fois on est obligés de créer une nouvelle instance. Par contre, les instances de la classe Matcher ne sont pas thread safety. Pour éviter des suprises, il vaut mieux synchroniser les opérations sur les chaînes de caractères avec RegEx.

Expression régulière négative

Il existe également ce qu'on appele une expression régulière négative. Son but est d'écarter tous les fragments qui ne correspondent pas à une ou plusieurs conditions. Elle est représentée par l'expression (?!X) où X signifie le caractère indésirable.

Imaginons que dans notre exemple on voudrait trouver tous les mots Lorem qui ne sont pas précédés par un mon se terminant par "g". Voici comment on devrait appeler notre classe java RegexExemple "\(?!g \)Lorem"

Astuces pour expressions régulières en Java

Voici quelques régles qui peuvent être utiles lors de définition des modèles des expressions régulières en Java :
- Les patterns devraient être testés séparément. Grâce à cela le débugage sera plus rapide.
- Il vaut mieux priviligégier plusieurs expressions à la place d'une seule grosse qui fait tout et correspond à l'abbréviation DEBE (Does Everything But Eat - un code qui fait tout mais est gourmand, soit en ressources, soit en complexité opérationnelle).
- Les expressions régulières sont destinées au traitement complexe des données textuelles. Avant de les utiliser il est conseillé de regarder du côté de la classe java.lang.String s'il n'y a pas de méthodes capables de faire ce qu'on souhaite. Certaines fonctions de cette classe sont plus rapides à l'exécution et moins compliquées à élaborer que RegEx.
- On peut capturer plusieurs groupes (par exemple deux chaînes de caractères différents, récupérés séparément). On verra cela sur notre classe d'exemple, appelée avec java RegexExemple "(Lorem) ([a-zA-z]+)" donnera :

Trying to match string with RegEx pattern : (Lorem) ([a-zA-z]+)
Found text Lorem at position 0 and 5
Found text Ipsum at position 6 and 11
Found text Lorem at position 75 and 80
Found text Ipsum at position 81 and 86
Found text Lorem at position 446 and 451
Found text Ipsum at position 452 and 457
Found text Lorem at position 562 and 567
Found text Ipsum at position 568 and 573
Found text Lorem at position 718 and 723
Found text Ipsum at position 724 and 729
Found text Lorem at position 942 and 947
Found text Ipsum at position 948 and 953

Le code a été modifié dans la boucle while () en ceci :

System.out.println("Found text " + match.group(1) + " at position " + match.start(1) + " and " + match.end(1));
System.out.println("Found text " + match.group(2) + " at position " + match.start(2) + " and " + match.end(2));

Par rapport aux patterns précédents, celui-ci se distingue grâce à des parenthèses qui englobent les expressions à trouver. Et ce sont ces lettres entre parenthèses qui définissent des groupes qu'on manipule plus loin dans le code en spécifiant les chiffres 1 et 2 (en occurence correspondant à des expressions (Lorem) et ([a-zA-z]+)).

Bartosz KONIECZNY Packages

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Un problème avec la définition des valeurs par défaut pour input type checkbox ?

Si vous rencontrez un problème avec la définition des valeurs par défaut pour un champ du type checkbox sous Symfony2, assurez-vous de la conformité des types de ces valeurs. Par exemple, le code suivant ne va pas fonctionner (le checkboxes ne seront pas sélectionnés pour les valeurs indiquées) :

  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = $val;
    }
    $this->preferedGifts = $vals;
  }
Par contre, le code suivant fonctionnera correctement (les checkboxes seront sélectionnés pour des valeurs passées dans la boucle foreach) :
  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = (int)$val;
    }
    $this->preferedGifts = $vals;
  }
Pour résumer, si le tableau avec les choix ($gifts dans l'exemple) contient les clés qui sont des integers, les valeurs par défaut doivent aussi être des integers.