venerdì 25 febbraio 2011

Elaboriamo le stringhe con le Regular Expression


Ultimamente mi sono imbattuto nella elaborazione di dati statistici. Il problema principale è stato quello di riuscire a estrarre dai files di log di diverse applicazioni solo le informazioni che mi interessavano. Avevo due scelte: scrivere del codice specifico per ogni singola applicazione oppure scrivere del codice generico che utilizza le espressioni regolari per estrarre le informazioni.

Un esercizio molto bello è stato quello di riuscire a estrarre le informazioni contenute nel più famoso dei log, l'access.log di apache che generalmente usa il formato NSCA Log Format.
Un esempio è questo:
125.125.125.125 - dsmith [10/Oct/1999:21:15:05 +0500] "GET /index.html HTTP/1.0" 200 1043
Ho trovato questo documento dell'IBM che descrive i singoli componenti del log.


Con le espressioni regolari ho però trovato un grosso limite. Il raggruppamento è posizionale e non è possibile identificare un gruppo dal nome. Quindi se le posizioni delle informazioni all'interno dei vari log cambiano, sarà necessario sapere esattamente la posizione del campo che si vuole estrarre per ogni tipo di log.
Io ho esteso la sintassi delle regular expression in modo da dichiarare un nome per un dato gruppo. Visto che il gruppo si definisce con le parentesi tonde, io ho immaginato uno scenario del tipo:
...(...)...(...)...
vs.
...({gruppo1}...)...({gruppoN}...)...
In questo modo posso pensare di utilizzare un'etichetta che mi identifica la posizione del gruppo all'interno della regular expression.

Uno strumento molto utile per testare le mie espressioni regolari è stato questo: Regular Expression Hilighting. Io l'ho utilizzato come plugin di eclipse ma è disponibile anche la versione applet fruibile online.


Dopo tutto questo pensare alla fine è uscita questa classe Java molto semplice.

/**
 * @author Diego Vicentini (digolo) [digolo-gmail.com]
 */
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class RegExTester {

 // Pattern per il calcolo del numero dei gruppi
 private Pattern groupPattern = Pattern.compile("\\((.*?)\\)");
 
 // Pattern per l'estrazione del nome campo
 private Pattern fieldnamePattern = Pattern.compile("\\(\\{(.*?)\\}.*?\\)");

 // Pattern per l'estrazione dei flag che non sono da considerare gruppi
 // non è previsto il flag commento (?x)
 private Pattern flagPattern = Pattern.compile("(\\(\\?i\\)|\\(\\?d\\)|\\(\\?m\\)|\\(\\?s\\)|\\(\\?u\\))");

 private Map fieldMap = new HashMap();
 
 private Pattern pattern;
 
 public RegExTester(String rawPattern) {
  
  // Inizializzo il nuovo pattern
  StringBuffer cleanPattern = new StringBuffer();
  
  Matcher matcher = groupPattern.matcher(rawPattern);
  int i = 1;
  int start = 0;
  while (matcher.find()) {
   String matchGroup = matcher.group();
   Matcher flagMatcher = flagPattern.matcher(matchGroup);
   if (!flagMatcher.find()) {
    Matcher fieldNameMatcher = fieldnamePattern.matcher(matchGroup);
    if (fieldNameMatcher.find()) {
     // Aggiungo il campo alla mappa
     fieldMap.put(fieldNameMatcher.group(1), i);
     // Aggiungo il pattern senza il nome campo
     cleanPattern.append(rawPattern.substring(start, matcher.start()));
     cleanPattern.append("(");
     cleanPattern.append(matchGroup.substring(3 + fieldNameMatcher.group(1).length()));
     // Mi posiziono alla fine del match
     start = matcher.end();
    }
    i++;
   }
  }
  cleanPattern.append(rawPattern.substring(start));
  
  // Preparo il pattern applicativo
  pattern = Pattern.compile(cleanPattern.toString());
 }

 public Pattern getPattern() {
  return pattern;
 }
 
 public String group(Matcher matcher, String name) {
  return matcher.group(fieldMap.get(name));
 }
 
 public static void main(String[] args) {
  
  // La mia RegEx per estrarre IP e data dal log
  String ncsaPattern = "(?i)({ip}\\b(?:\\d{1,3}.){3}\\d{1,3}) - ({user}\\w+)* \\[({date}.*)\\] \"({method}.*?)\\s({url}.*) ({protocol}.*?)\" ({status}\\d{3}) ({bytes}\\d+)";
  
  String logLine = "125.125.125.125 - dsmith [10/Oct/1999:21:15:05 +0500] \"GET /index.html HTTP/1.0\" 200 1043";
 
  RegExTester tester = new RegExTester(ncsaPattern);
  
  Pattern ncsa = tester.getPattern();
  Matcher matcher = ncsa.matcher(logLine);
  while (matcher.find()) {
   System.out.println("IP       :" + tester.group(matcher, "ip"));
   System.out.println("USER     :" + tester.group(matcher, "user"));
   System.out.println("DATE     :" + tester.group(matcher, "date"));
   System.out.println("METHOD   :" + tester.group(matcher, "method"));
   System.out.println("URL      :" + tester.group(matcher, "url"));
   System.out.println("PROTOCOL :" + tester.group(matcher, "protocol"));
   System.out.println("STATUS   :" + tester.group(matcher, "status"));
   System.out.println("BYTES    :" + tester.group(matcher, "bytes"));
   
  }
 }
}
Il risultato ottenuto è:
IP       :125.125.125.125
USER     :dsmith
DATE     :10/Oct/1999:21:15:05 +0500
METHOD   :GET
URL      :/index.html
PROTOCOL :HTTP/1.0
STATUS   :200
BYTES    :1043
Spero che i commenti all'interno della classe siano chiari.

0 commenti: