mar.
2010
Au coeur du JDK : la classe Scanner
Une classe injustement méconnue du JDK est Scanner (java.util.Scanner
).
Elle offre pourtant des fonctionnalités très intéressantes pour parser des chaînes de caractères, et en extraire et convertir les composants.
Un Scanner peut se brancher sur à peu près n'importe quelle source : InputStream
, Readable
(et donc Reader
), File
... et bien sûr une simple String
.
Ensuite, deux options s'offrent à vous : utiliser les méthodes de type hasNext...()
/ next...()
, ou alors les méthodes de type find...()
/ match()
/ group()
.
Dans ce billet, nous verrons comment utiliser ces deux jeux d'instructions.
Première méthode : hasNext() / next()
Voyons une première façon d'utiliser un Scanner pour lire des flux de texte.
Elle se décompose en deux étapes :
- Premièrement, découper la chaîne de caractères en tokens grâce à un délimiteur ; il s'agit par défaut d'un caractère "blanc" (espace, tabulation, retour à la ligne...), mais il est évidemment possible de fournir sa propre expression via la méthode
useDelimiter(expression)
. - Ensuite, utiliser les méthodes de type
hasNext...()
etnext...()
pour parcourir, récupérer et convertir ces tokens.
Les méthodes de type hasNext...()
(hasNextInt()
, hasNextFloat()
...) fonctionnent sur le même principe qu'un Iterator
, et indiquent si le prochain token existe et s'il est bien du type spécifié. Une fois cette vérification effectuée, les méthodes de la forme next...()
(nextInt()
, nextFloat()
...) permettent de récupérer ledit token, directement converti dans le type adéquat.
A noter qu'existent également les méthodes simples hasNext()
et next()
, qui permettent de savoir s'il existe un token (de n'importe quel type) et de le récupérer sous forme de String.
A l'aide de ces méthodes, il est très facile de parser une chaîne dont vous maîtrisez parfaitement le format, par exemple un fichier .csv
:
String s = "Dalton;Joe;1.4\n" + "Dalton;Jack;1.6\n" + "Dalton;William;1.8\n" + "Dalton;Averell;2.0"; Scanner scan = new Scanner(s); scan.useDelimiter(";|\n"); scan.useLocale(Locale.US); // Pour les floats while(scan.hasNextLine()) { System.out.printf("%2$s %1$s : %3$.1f m %n", scan.next(), scan.next(), scan.nextFloat()); }
Joe Dalton : 1,4 m Jack Dalton : 1,6 m William Dalton : 1,8 m Averell Dalton : 2,0 m
Si au contraire le format de la chaîne vous est inconnu, par exemple si elle est saisie par l'utilisateur, il est plus prudent de recourir au pattern suivant, pour éviter les erreurs de conversion de type (InputMismatchException
) :
Scanner scan = new Scanner(System.in); System.out.println("Entrez un nombre entier à incrémenter :"); // Existe-t-il un token ? while(scan.hasNext()) { // Est-ce un int ? On l'incrémente if (scan.hasNextInt()) { int nb = scan.nextInt(); System.out.println(nb + " +1 = " + ++nb); } // Ce n'est pas un int, affichage d'un message d'erreur. else { System.out.println(scan.next() + " n'est pas un nombre."); } } scan.close();
Notez que la classe Scanner simplifie beaucoup votre code, car vous n'avez plus à convertir manuellement les tokens ni à gérer des exceptions de bas niveau.
A titre de comparaison, voici le code effectuant le même traitement sans utiliser la classe Scanner :
BufferedReader reader = null; String line = null; try { reader = new BufferedReader(new InputStreamReader(System.in)); while ((line=reader.readLine()) != null) { try { System.out.println(Integer.parseInt(line)+1); } catch (NumberFormatException e) { System.out.println(line + " n'est pas un nombre."); } } } catch (IOException e) { e.printStackTrace(); } finally { if (reader!=null) { try { reader.close(); } catch (IOException e) { } } }
Même pour une simple lecture de fichier ligne par ligne, Scanner remplace avantageusement le traditionnel BufferedReader
:
public static void readFileWithBufferedReader(String fileName) { BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(fileName)); String line = null; while ((line=reader.readLine())!=null) { System.out.println(line); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (reader!=null) { try { reader.close(); } catch (IOException e) { } } } } public static void readFileWithScanner(String fileName) { Scanner scan = null; try { scan = new Scanner(new File(fileName)); while(scan.hasNextLine()) { System.out.println(scan.nextLine()); } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (scan!=null) scan.close(); } }
Deuxième méthode : find() / match() / group()
La seconde utilisation possible d'un Scanner est la recherche d'éléments précis dans un texte, en s'appuyant sur des expressions régulières .
Cette fois encore, c'est très simple :
- Premièrement, il faut définir le format attendu de la ligne, à l'aide d'une expression régulière compatible avec la classe
java.util.regex.Pattern
. - Ensuite, il n'y a plus qu'à récupérer les informations qui nous intéressent en fonction de leur index, grâce à la méthode
group(index)
.
Par exemple, vous possédez un fichier répertoriant les plus grands délinquants du Far West, et vous souhaitez en extraire les nom, prénom et taille de ces individus.
Le bandit Joe Dalton mesure 1.40m. Le bandit Jack Dalton mesure 1.60m. Le bandit William Dalton mesure 1.80m. Le bandit Averell Dalton mesure 2m.
Chaque ligne répond visiblement au pattern suivant :
Le bandit (\\w+) (\\w+) mesure (\\d+(\\.\\d+)?)m\\.
Notez que le prénom est la première expression capturée (index 1), le nom la seconde (index 2), et la taille la troisième (index 3).
Utilisons Scanner pour extraire ces informations du fichier :
Pattern pattern = Pattern.compile("Le bandit (\\w+) (\\w+) mesure (\\d+(\\.\\d+)?)m\\."); Scanner scan = new Scanner(daltons); // Fichier, String... while (scan.findInLine(pattern) != null) { MatchResult match = scan.match(); System.out.printf("%s, %s : %.2fm%n", match.group(2), match.group(1), Float.valueOf(match.group(3))); if (scan.hasNextLine()) scan.nextLine(); // Avance à la ligne suivante, si elle existe. } scan.close();
Dalton, Joe : 1,40m Dalton, Jack : 1,60m Dalton, William : 1,80m Dalton, Averell : 2,00m
Conclusion
La classe Scanner, introduite avec Java 5.0, est injustement méconnue. Elle offre pourtant des fonctionnalités intéressantes, qui simplifient la lecture et l'interprétation de texte pouvant provenir de sources variées.
J'espère que ce billet vous aura donné envie de l'utiliser !
Commentaires
Quelle difference avec StringTokenizer ?
La partie next() y ressemble tellement que je me demande que privilégier entre Scanner et StringTokenizer.
Avec StringTokenizer, tous les tokens doivent être délimités par le même séparateur. Avec Scanner, on donne au contraire un template de ligne, duquel on extrait les informations intéressantes.
Par exemple, dans l'exemple des Daltons, on pourrait choisir de ramener le nom et le prénom comme un seul token, malgré le fait qu'il contienne des espaces :
"Le bandit (\\w+ \\w+) mesure (\\d+(\\.\\d+)?)m\\."
De plus, Scanner nous permet de récupérer la taille du Dalton malgré le "m" accolé ; avec StringTokenizer on serait obligé de le supprimer manuellement avant de convertir la taille en float.
Réponse : http://stackoverflow.com/questions/...
StringTokenizer est beaucoup plus rapide mais les délimiteur doivent être fixe au cours du parcours.