Capítulo 4. Comparación de patrones con expresiones regulares

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

4.0 Introducción

Supón que llevas unos años en Internet y has sido fiel a la hora de guardar toda tu correspondencia, por si tú (o tus abogados, o la fiscalía) necesitáis una copia. El resultado es que tienes una partición de disco de 5 GB dedicada al correo guardado. Supongamos además que recuerdas que en algún lugar de ahí hay un mensaje de correo electrónico de alguien llamada Angie o Anjie. ¿O era Angy? Pero no recuerdas cómo lo llamaste ni dónde lo guardaste. Obviamente, tienes que buscarlo.

Pero mientras algunos de vosotros vais e intentáis abrir los 15.000.000 de documentos en un procesador de textos, yo lo encontraré con un simple comando. Cualquier sistema que admita expresiones regulares me permite buscar el patrón de varias formas. La más sencilla de entender es

Angie|Anjie|Angy

que probablemente puedas adivinar significa simplemente buscar cualquiera de las variaciones. Una forma más concisa (más pensar, menos teclear) es:

An[^ dn]

La sintaxis se irá aclarando a medida que avancemos en este capítulo. Brevemente, la "A" y la "n" coinciden entre sí, encontrando de hecho palabras que empiecen por "An", mientras que el críptico [^ dn] requiere que la "An" vaya seguida de un carácter que no sea(^ significa no en este contexto) un espacio (para eliminar la palabra inglesa muy común "an" al principio de una frase) o "d" (para eliminar la palabra común "and") o "n" (para eliminar "Anne", "Announcing", etc.). ¿Tu procesador de textos ya ha superado la pantalla de inicio? Bueno, no importa, porque ya he encontrado el archivo que faltaba. Para encontrar la respuesta, simplemente he tecleado este comando:

grep 'An[^ dn]' *

Lasexpresiones regulares, o regexes para abreviar, proporcionan una especificación concisa y precisa de patrones que deben coincidir en el texto. Una buena forma de pensar en las expresiones regulares es como un pequeño lenguaje para hacer coincidir patrones de caracteres en el texto contenido en cadenas. Una API de expresiones regulares es unintérpretepara hacer coincidir expresiones regulares.

Como otro ejemplo del poder de las expresiones regulares, considera el problema de la actualización masiva de cientos de archivos. Cuando empecé con Java, la sintaxis para declarar referencias a matrices era baseType arrayVariableName[]. Por ejemplo, un método con un argumento de matriz, como el método main de todo programa, solía escribirse así:

public static void main(String args[]) {

Pero con el paso del tiempo, a los administradores del lenguaje Java les quedó claro que sería mejor escribirlo como baseType[] arrayVariableName, así:

public static void main(String[] args) {

Esto es mejor al estilo de Java, porque asocia el "carácter de matriz" del tipo con el propio tipo, en lugar de con el nombre del argumento local, y el compilador sigue aceptando ambos modos. Quería cambiar todas las apariciones de main escritas de la forma antigua a la nueva. Utilicé el patrón main(String [a-z] con la utilidad grep descrita anteriormente para encontrar los nombres de todos los archivos que contenían declaraciones principales al estilo antiguo (es decir, main(Stringseguido de un espacio y un carácter de nombre en lugar de un corchete abierto). Luego utilicé otra herramienta de Unix basada en regex, el editor de secuencias sed, en un pequeño script de shell para cambiar todas las apariciones en esos archivos de main(String *([a-z][a-z]*)[] amain(String[] $1 (la sintaxis regex utilizada aquí se explica más adelante en este capítulo). De nuevo, el enfoque basado en regex fue órdenes de magnitud más rápido que hacerlo interactivamente, incluso utilizando un editor razonablemente potente como vi o emacs, por no hablar de intentar utilizar un procesador de textos gráfico.

Históricamente, la sintaxis de las expresiones regulares ha cambiado a medida que se incorporaban a más herramientas y lenguajes, por lo que la sintaxis exacta de los ejemplos anteriores no es exactamente la que utilizarías en Java, pero transmite la concisión y potencia del mecanismo de las expresiones regulares.1

Como tercer ejemplo, considera el análisis de un archivo de registro del servidor web Apache, en el que algunos campos están delimitados por comillas, otros por corchetes y otros por espacios. Escribir código ad hoc para analizar esto es complicado en cualquier lenguaje, pero una expresión regular bien elaborada puede dividir la línea en todos los campos que la componen en una sola operación (este ejemplo se desarrolla en la Receta 4.10).

Los desarrolladores de Java pueden obtener estas mismas ganancias de tiempo. El soporte de expresiones regulares ha estado en el tiempo de ejecución estándar de Java durante años y está bien integrado (por ejemplo, hay métodos regex en la clase estándar y en el nuevo paquete de E/S), hay métodos regex en la clase estándar java.lang.String y en el nuevo paquete E/S). Hay algunos otros paquetes regex para Java, y es posible que ocasionalmente encuentres código que los utilice, pero prácticamente todo el código de este siglo se puede esperar que utilice el paquete incorporado. La sintaxis de las propias expresiones regulares de Java se trata en la Receta4.1, y la sintaxis de la API de Java para utilizar expresiones regulares se describe en la Receta 4.2. Las recetas restantes muestran algunas aplicaciones de la tecnología regex en Java.

Ver también

Mastering Regular Expressions de Jeffrey Friedl (O'Reilly) es la guía definitiva sobre todos los detalles de las expresiones regulares. La mayoría de los libros introductorios sobre Unix y Perl incluyen algún debate sobre las expresiones regulares; Unix Power Tools de Mike Loukides, Tim O'Reilly, Jerry Peek y Shelley Powers (O'Reilly) les dedica un capítulo.

4.1 Sintaxis de las expresiones regulares

Problema

Necesitas aprender la sintaxis de las expresiones regulares de Java.

Solución

Consulta la Tabla 4-1 para obtener una lista de los caracteres de las expresiones regulares.

Debate

Estos caracteres de patrón te permiten especificar expresiones regulares de considerable potencia. Para crear patrones, puedes utilizar cualquier combinación de texto normal y los metacaracteres o caracteres especiales de la Tabla 4-1. Todos ellos pueden utilizarse en cualquier combinación que tenga sentido. Por ejemplo, a+ significa cualquier número de apariciones de la letra a, desde una hasta un millón o un gazillón. El patrón Mrs?\. coincide con Mr. oMrs. Y .* indica cualquier carácter, cualquier número de veces, y su significado es similar al que la mayoría de los intérpretes de línea de comandos dan a la letra \* sola. El patrón \d+ significa cualquier número de dígitos numéricos. \d{2,3} significa un número de dos o tres dígitos.

Tabla 4-1. Sintaxis de los metacaracteres de las expresiones regulares
Subexpresión Partidos Notas

General

\^

Inicio de línea/cadena

$

Fin de línea/cadena

\b

Límite de la palabra

\B

Ni una palabra límite

\A

Comienzo de toda la cadena

\z

Fin de toda la cadena

\Z

Fin de toda la cadena (excepto el final de línea permitido)

Ver Receta 4.9

.

Cualquier carácter (excepto el final de línea)

[…​]

"Clase de personaje"; un personaje cualquiera de los enumerados

[\^…​]

Cualquier personaje que no esté en la lista

Ver Receta 4.2

Alternancia y agrupación

(…​)

Agrupación (grupos de captura)

Ver Receta 4.3

|

Alternancia

(?:_re_ )

Paréntesis de no captura

\G

Fin del partido anterior

+\+n

Referencia retrospectiva para capturar el número de grupo n

Cuantificadores normales (codiciosos)

{m,n }

Cuantificador para de m hasta n repeticiones

Ver Receta 4.4

{ m ,}

Cuantificador para m o más repeticiones

{ m }

Cuantificador para exactamente m repeticiones

Ver Receta 4.10

{,n }

Cuantificador para 0 hasta n repeticiones

\*

Cuantificador para 0 o más repeticiones

Abreviatura de {0,}

+

Cuantificador para 1 o más repeticiones

Corto para {1,}; ver Receta 4.2

?

Cuantificador para 0 ó 1 repeticiones (es decir, presente exactamente una vez o ninguna)

Abreviatura de {0,1}

Cuantificadores reticentes (no reticentes)

{m,n }?

Cuantificador reticente para de m hasta n repeticiones

{ m ,}?

Cuantificador reticente para m o más repeticiones

{,n }?

Cuantificador reticente para 0 hasta n repeticiones

\*?

Cuantificador reticente: 0 o más

+?

Cuantificador reticente: 1 o más

Ver Receta 4.10

??

Cuantificador reticente: 0 ó 1 veces

Cuantificadores posesivos (muy golosos)

{m,n }+

Cuantificador posesivo para de m hasta n repeticiones

{ m ,}+

Cuantificador posesivo para m o más repeticiones

{,n }+

Cuantificador posesivo para 0 hasta n repeticiones

\*+

Cuantificador posesivo: 0 o más

++

Cuantificador posesivo: 1 o más

?+

Cuantificador posesivo: 0 ó 1 veces

Escapes y taquigrafías

\

Carácter de escape (comillas): desactiva la mayoría de los metacaracteres; convierte los caracteres alfabéticos subsiguientes en metacaracteres

\Q

Escapa (entrecomilla) todos los caracteres hasta \E

\E

Termina la cita iniciada con \Q

\t

Carácter de tabulación

\r

Carácter de retorno (retorno de carro)

\n

Carácter de nueva línea

Ver Receta 4.9

\f

Forma de alimentación

\w

Carácter en una palabra

Utiliza \w+ para una palabra; consulta la Receta 4.10

\W

Un carácter que no sea una palabra

\d

Dígito numérico

Utiliza \d+ para un número entero; consulta la Receta 4.2

\D

Un carácter no numérico

\s

Espacio en blanco

Espacio, tabulador, etc., según determine java.lang.Character.isWhitespace()

\S

Un carácter que no sea un espacio en blanco

Ver Receta 4.10

Bloques Unicode (muestras representativas)

\p{InGreek}

Un personaje del bloque griego

(Bloque simple)

\P{InGreek}

Cualquier carácter que no esté en el bloque griego

\p{Lu}

Una letra mayúscula

(Categoría simple)

\p{Sc}

Un símbolo monetario

Clases de caracteres estilo POSIX (definidas sólo para US-ASCII)

\p{Alnum}

Caracteres alfanuméricos

[A-Za-z0-9]

\p{Alpha}

Caracteres alfabéticos

[A-Za-z]

\p{ASCII}

Cualquier carácter ASCII

[\x00-\x7F]

\p{Blank}

Caracteres de espacio y tabulación

\p{Space}

Caracteres espaciales

[ \t\n\x0B\f\r]

\p{Cntrl}

Caracteres de control

[\x00-\x1F\x7F]

\p{Digit}

Caracteres numéricos

[0-9]

\p{Graph}

Caracteres imprimibles y visibles (no espacios ni caracteres de control)

\p{Print}

Caracteres imprimibles

Igual que \p{Graph}

\p{Punct}

Caracteres de puntuación

Uno de !"#$%&'()\*+,-./:;<=>?@[]\^_`{|}\~

\p{Lower}

Caracteres minúsculos

[a-z]

\p{Upper}

Caracteres en mayúsculas

[A-Z]

\p{XDigit}

Caracteres de dígitos hexadecimales

[0-9a-fA-F]

Las remezclas coinciden en cualquier lugar posible de la cadena. Los patrones seguidos de cuantificadores codiciosos (el único tipo que existía en las regex tradicionales de Unix) consumen (coinciden) todo lo posible sin comprometer las subexpresiones que les siguen. Los patrones seguidos de cuantificadores posesivos coinciden tanto como sea posible sin tener en cuenta las subexpresiones siguientes. Los patrones seguidos de cuantificadores reticentes consumen el menor número de caracteres posible para obtener una coincidencia.

Además, a diferencia de los paquetes regex de otros lenguajes, el paquete regex de Java se diseñó para manejar caracteres Unicode desde el principio. La secuencia de escape estándar de Java \u+nnnn se utiliza para especificar un carácter Unicode en el patrón. Utilizamos métodos de java.lang.Character para determinar las propiedades de los caracteres Unicode, como si un carácter dado es un espacio. De nuevo, ten en cuenta que la barra invertida debe duplicarse si está en una cadena Java que se está compilando porque, de lo contrario, el compilador la interpretaría como "barra invertida-u" seguida de algunos números.

Para ayudarte a aprender cómo funcionan los regex, te proporciono un pequeño programa llamado REDemo.2 El código de REDemo es demasiado largo para incluirlo en el libro; en el directorio en línea regex del repositorio darwinsys-api, encontrarás REDemo.java, que puedes ejecutar para explorar cómo funcionan las expresiones regulares.

En el cuadro de texto superior (ver Figura 4-1), escribe el patrón regex que quieras probar. Ten en cuenta que, a medida que escribes cada carácter, se comprueba la sintaxis del regex; si la sintaxis es correcta, verás una marca de verificación al lado. A continuación, puedes seleccionar Coincidir, Buscar o Buscar todo. Coincidir significa que toda la cadena debe coincidir con la expresión regular, y Buscar significa que la expresión regular debe encontrarse en algún lugar de la cadena (Buscar todo cuenta el número de ocurrencias que se encuentran). Debajo, escribe la cadena con la que debe coincidir la expresión regular. Experimenta hasta la saciedad. Cuando tengas la expresión regular tal y como quieres, puedes pegarla en tu programa Java. Tendrás que escapar (barra invertida) cualquier carácter que sea tratado de forma especial tanto por el compilador de Java como por el paquete de expresiones regulares de Java, como la propia barra invertida, las comillas dobles y otros. Una vez que tengas la expresión regular tal y como quieres, hay un botón Copiar (que no se muestra en estas capturas de pantalla) para exportar la expresión regular al portapapeles, con o sin doble barra invertida, dependiendo de cómo quieras utilizarla.

Consejo

Recuerda que, como una regex se introduce como una cadena que será compilada por un compilador Java, normalmente necesitas dos niveles de escape para cualquier carácter especial, incluidas la barra invertida y las comillas dobles. Por ejemplo, la regex (que incluye las comillas dobles):

"You said it\."

tiene que estar tipado así para ser un lenguaje Java válido en tiempo de compilación String:

String pattern = "\"You said it\\.\""

En Java 14+ también podrías utilizar un bloque de texto para evitar escapar las comillas:

String pattern = """
	"You said it\\.""""

No sabes cuántas veces he cometido el error de olvidarme de la barra invertida extra en \d+, \w+, ¡y sus similares!

En la Figura 4-1, escribí qu en la casilla Patrón del programa REDemo, que es un patrón regex sintácticamente válido: cualquier carácter ordinario es un regex por sí mismo, por lo que busca la letra q seguida de u. En la versión superior, sólo he escrito un q en la cadena, que no coincide. En la segunda, he escrito quack y el q de un segundo quack. Como he seleccionado Buscar todo, el recuento muestra una coincidencia. En cuanto escribo el segundo u, el recuento se actualiza a dos, como se muestra en la tercera versión.

Las expresiones regulares pueden hacer mucho más que coincidir caracteres. Por ejemplo, el regex de dos caracteres ^T coincidiría con el principio de línea (^) seguido inmediatamente de una T mayúscula, es decir, cualquier línea que empiece por T mayúscula. No importa si la línea empieza por "Tiny trumpets", "Titanic tubas" o "Triumphant twisted trombones", siempre que la T mayúscula esté presente en la primera posición.

Pero aquí no vamos muy adelantados. ¿Realmente hemos invertido todo este esfuerzo en la tecnología regex sólo para poder hacer lo que ya podíamos hacer con el método java.lang.String startsWith() ? Hmmm, puedo oír a algunos de vosotros inquietarse un poco. ¡Permaneced en vuestros asientos! ¿Y si quisieras emparejar no sólo una letra T en primera posición, sino también una vocal inmediatamente después de ella, seguida de cualquier número de letras de una palabra, seguida de un signo de exclamación? ¿Seguro que podrías hacerlo en Java comprobando startsWith("T") y charAt(1) == 'a' || charAt(1) == 'e', y así sucesivamente? Sí, pero para cuando lo hicieras, habrías escrito un montón de código muy especializado que no podrías utilizar en ninguna otra aplicación. Con las expresiones regulares, puedes simplemente dar el patrón ^T[aeiou]\w*!. Es decir, ^ y T como antes, seguidos de una clase de caracteres que enumere las vocales, seguidos de cualquier número de caracteres de palabra (\w*), seguidos del signo de exclamación.

jcb4 0401
Figura 4-1. REDemo con ejemplos sencillos

"Pero espera, ¡hay más!", como solía decir mi difunto y gran jefeYuri Rubinsky. ¿Y si quieres poder cambiar el patrón que buscas en tiempo de ejecución? ¿Recuerdas todo ese código Java que acabas de escribir para que coincida con T en la columna 1, más una vocal, algunos caracteres de palabra y un signo de exclamación? Pues ya es hora de tirarlo. Porque esta mañana necesitamos que coincida con Q, seguido de una letra distinta de u, seguido de un número de dígitos, seguido de un punto. Mientras algunos de vosotros os ponéis a escribir una nueva función para hacerlo, los demás nos limitaremos a ir al RegEx Bar & Grille, pedir un ^Q[^u]\d+\.. al camarero y seguir nuestro camino.

Vale, si quieres una explicación: el [^u] significa que coincide con cualquier carácter que no sea el carácter u. El \d+ significa uno o más dígitos numéricos. + es un cuantificador que significa una o más ocurrencias de lo que sigue, y \d es un dígito numérico cualquiera. Así que \d+ significa un número con uno, dos o más dígitos. Por último, el \.. Bueno, . por sí mismo es un metacarácter. La mayoría de los metacaracteres simples se desactivan precediéndolos de un carácter de escape. No la tecla Esc de tu teclado, por supuesto. El carácter de escape regex es la barra invertida. Preceder un metacarácter como . con este escape desactiva su significado especial, por lo que buscamos un punto literal en lugar de cualquier carácter. Preceder unos cuantos caracteres alfabéticos seleccionados (por ejemplo, n, r, t, s, w) con escape los convierte en metacaracteres. La Figura 4-2 muestra la regex ^Q[^u]\d+\..en acción. En el primer recuadro, he escrito parte de la expresión regular como ^Q[^u. Como hay un corchete sin cerrar, el indicador Sintaxis OK está desactivado; cuando termine la expresión regular, volverá a activarse. En el segundo fotograma, he terminado de escribir la expresión regular y he escrito la cadena de datos como QA577 (que debería coincidir con $$^Q[^u]\d+$$ pero no con el punto, ya que no lo he escrito). En el tercer fotograma, he escrito el punto para que el indicador Coincidencias esté en Sí.

jcb4 0402
Figura 4-2. Ejemplo de REDemo con "Q no seguida de u

Como hay que escapar las barras invertidas al pegar la expresión regular en código Java, la versión actual de REDemo tiene un botón Copy Pattern, que copia la expresión regular literalmente para utilizarla en la documentación y en comandos Unix, y un botón Copy Pattern Backslashed, que copia la expresión regular en el portapapeles con las barras invertidas dobladas, para pegarla en cadenas Java.

A estas alturas deberías tener al menos una idea básica de cómo funcionan las expresiones regulares en la práctica. En el resto de este capítulo se dan más ejemplos y se explican algunos de los temas más potentes, como los grupos de captura. En cuanto a cómo funcionan las expresiones regulares en teoría -y hay muchos detalles teóricos y diferencias entre los distintos tipos de expresiones regulares-, se remite al lector interesado a Dominar las expresiones regulares. Mientras tanto, empecemos a aprender a escribir programas Java que utilicen expresiones regulares.

4.2 Utilizar Regexes en Java: Prueba de un patrón

Problema

Ya estás listo para empezar a utilizar el procesamiento de expresiones regulares para reforzar tu código Java comprobando si un patrón dado puede coincidir en una cadena determinada.

Solución

Utiliza el paquete de expresiones regulares de Java, java.util.regex.

Debate

La buena noticia es que la API de Java para expresiones regulares es realmente fácil de usar. Si todo lo que necesitas es averiguar si un regex dado coincide con una cadena, puedes utilizar el práctico método boolean matches() de la clase String, que acepta como argumento un patrón regex en forma String:

if (inputString.matches(stringRegexPattern)) {
    // it matched... do something with it...
}

Sin embargo, se trata de una rutina cómoda, y la comodidad siempre tiene un precio. Si la expresión regular se va a utilizar más de una o dos veces en un programa, es más eficaz construir y utilizar un Pattern y su Matcher(s). Aquí se muestra un programa completo que construye un Pattern y lo utiliza para match:

public class RESimple {
    public static void main(String[] argv) {
        String pattern = "^Q[^u]\\d+\\.";
        String[] input = {
            "QA777. is the next flight. It is on time.",
            "Quack, Quack, Quack!"
        };

        Pattern p = Pattern.compile(pattern);

        for (String in : input) {
            boolean found = p.matcher(in).lookingAt();

            System.out.println("'" + pattern + "'" +
            (found ? " matches '" : " doesn't match '") + in + "'");
        }
    }
}

El paquete java.util.regex contiene dos clases, Pattern y Matcher, que proporcionan la API pública que se muestra en el Ejemplo 4-1.

Ejemplo 4-1. API pública de Regex
/**
 * The main public API of the java.util.regex package.
 */

package java.util.regex;

public final class Pattern {
    // Flags values ('or' together)
    public static final int
        UNIX_LINES, CASE_INSENSITIVE, COMMENTS, MULTILINE,
        DOTALL, UNICODE_CASE, CANON_EQ;
    // No public constructors; use these Factory methods
    public static Pattern compile(String patt);
    public static Pattern compile(String patt, int flags);
    // Method to get a Matcher for this Pattern
    public Matcher matcher(CharSequence input);
    // Information methods
    public String pattern();
    public int flags();
    // Convenience methods
    public static boolean matches(String pattern, CharSequence input);
    public String[] split(CharSequence input);
    public String[] split(CharSequence input, int max);
}

public final class Matcher {
    // Action: find or match methods
    public boolean matches();
    public boolean find();
    public boolean find(int start);
    public boolean lookingAt();
    // "Information about the previous match" methods
    public int start();
    public int start(int whichGroup);
    public int end();
    public int end(int whichGroup);
    public int groupCount();
    public String group();
    public String group(int whichGroup);
    // Reset methods
    public Matcher reset();
    public Matcher reset(CharSequence newInput);
    // Replacement methods
    public Matcher appendReplacement(StringBuffer where, String newText);
    public StringBuffer appendTail(StringBuffer where);
    public String replaceAll(String newText);
    public String replaceFirst(String newText);
    // information methods
    public Pattern pattern();
}

/* String, showing only the RE-related methods */
public final class String {
    public boolean matches(String regex);
    public String replaceFirst(String regex, String newStr);
    public String replaceAll(String regex, String newStr);
    public String[] split(String regex);
    public String[] split(String regex, int max);
}

Esta API es lo suficientemente grande como para requerir alguna explicación. Estos son los pasos normales para la concordancia regex en un programa de producción:

  1. Crea un Pattern llamando al método estático Pattern.compile() .

  2. Solicita un Matcher del patrón llamando a pattern.matcher(CharSequence) por cada String (u otro CharSequence) que desees consultar.

  3. Llama (una o varias veces) a uno de los métodos del buscador (analizados más adelante en esta sección) en el Matcher resultante.

La interfaz java.lang.CharSequence proporciona un acceso sencillo de sólo lectura a objetos que contienen una colección de caracteres. Las implementaciones estándar son String y StringBuffer/StringBuilder (descritas en el Capítulo 3), y la nueva clase de E/S java.nio.CharBuffer.

Por supuesto, puedes realizar la concordancia regex de otras formas, como utilizando los métodos de conveniencia de Pattern o incluso de java.lang.String, como ésta:

public class StringConvenience {
    public static void main(String[] argv) {

        String pattern = ".*Q[^u]\\d+\\..*";
        String line = "Order QT300. Now!";
        if (line.matches(pattern)) {
            System.out.println(line + " matches \"" + pattern + "\"");
        } else {
            System.out.println("NO MATCH");
        }
    }
}

Pero la lista de tres pasos es el patrón estándar para las coincidencias. Probablemente utilizarías la rutina de conveniencia String en un programa que sólo utilizara el regex una vez; si el regex se utilizara más de una vez, merece la pena tomarse el tiempo de compilarlo porque la versión compilada se ejecuta más rápido.

Además, Matcher tiene varios métodos de búsqueda, que proporcionan más flexibilidad que la rutina de convenienciaString match() . Estos son los métodos Matcher:

match()

Se utiliza para comparar toda la cadena con el patrón; es igual que la rutina de java.lang.String. Como coincide con todo String, tuve que poner .*antes y después del patrón.

lookingAt()

Se utiliza para hacer coincidir el patrón sólo al principio de la cadena.

find()

Se utiliza para hacer coincidir el patrón en la cadena (no necesariamente en el primer carácter de la cadena), empezando por el principio de la cadena o, si el método fue llamado previamente y tuvo éxito, en el primer carácter no coincidido por la coincidencia anterior.

Cada uno de estos métodos devuelve boolean, siendo true una coincidencia y false ninguna coincidencia. Para comprobar si una cadena dada coincide con un patrón determinado, sólo tienes que escribir algo como lo siguiente:

Matcher m = Pattern.compile(patt).matcher(line);
if (m.find( )) {
    System.out.println(line + " matches " + patt)
}

Pero también puedes querer extraer el texto que coincidió, que es el tema de la siguiente receta.

Las siguientes recetas cubren los usos de la API Matcher. Inicialmente, los ejemplos sólo utilizan argumentos del tipo String como fuente de entrada. El uso de otros tipos de CharSequence se trata en la Receta 4.5.

4.3 Encontrar el texto coincidente

Problema

Tienes que encontrar el texto con el que coincidió la expresión regular.

Solución

A veces necesitas saber algo más que si una expresión regular ha coincidido con una cadena. En los editores y muchas otras herramientas, quieres saber exactamente qué caracteres coincidieron. Recuerda que con cuantificadores como *, la longitud del texto coincidente puede no tener relación con la longitud del patrón que coincidió con él. No subestimes al poderoso.*, que coincide alegremente con miles o millones de caracteres si se le permite. Como has visto en la receta anterior, puedes averiguar si una determinada coincidencia tiene éxito simplemente utilizandofind() o matches() . Pero en otras aplicaciones, querrás obtener los caracteres con los que coincidió el patrón.

Tras una llamada satisfactoria a uno de los métodos anteriores, puedes utilizar estos métodos de información en la página Matcher para obtener información sobre la coincidencia:

start(), end()

Devuelve la posición en la cadena de los caracteres inicial y final que han coincidido.

groupCount()

Devuelve el número de grupos de captura entre paréntesis, si los hay; devuelve 0 si no se utilizó ningún grupo.

group(int i)

Devuelve los caracteres coincidentes por grupo i de la coincidencia actual, sii es mayor o igual que cero y menor o igual que el valor de retorno de groupCount(). El grupo 0 es toda la coincidencia, por lo que group(0) (o simplemente group()) devuelve toda la parte de la entrada que coincidió.

La noción de paréntesis, o grupos de captura, es fundamental para el procesamiento de regex. Las expresiones regulares pueden anidarse hasta cualquier nivel de complejidad. El método group(int) te permite recuperar los caracteres que coinciden con un grupo de paréntesis determinado. Si no has utilizado ningún paréntesis explícito, puedes tratar lo que haya coincidido como nivel cero. El Ejemplo 4-2 muestra parte de REMatch.java.

Ejemplo 4-2. Parte de main/src/main/java/regex/REMatch.java
public class REmatch {
    public static void main(String[] argv) {

        String patt = "Q[^u]\\d+\\.";
        Pattern r = Pattern.compile(patt);
        String line = "Order QT300. Now!";
        Matcher m = r.matcher(line);
        if (m.find()) {
            System.out.println(patt + " matches \"" +
                m.group(0) +
                "\" in \"" + line + "\"");
        } else {
            System.out.println("NO MATCH");
        }
    }
}

Cuando se ejecuta, se imprime:

Q[\^u]\d+\. matches "QT300." in "Order QT300. Now!"

Con el botón Match marcado, REDemo proporciona una visualización de todos los grupos de captura de una regex determinada; un ejemplo se muestra en laFigura 4-3.

jcb4 0403
Figura 4-3. REDemo en acción

También es posible obtener los índices inicial y final y la longitud del texto con el que coincidió el patrón (recuerda que los términos con cuantificadores, como el \d+ de este ejemplo, pueden coincidir con un número arbitrario de caracteres de la cadena). Puedes utilizarlos junto con los métodos String.substring() de la siguiente manera:

        String patt = "Q[^u]\\d+\\.";
        Pattern r = Pattern.compile(patt);
        String line = "Order QT300. Now!";
        Matcher m = r.matcher(line);
        if (m.find()) {
            System.out.println(patt + " matches \"" +
                line.substring(m.start(0), m.end(0)) +
                "\" in \"" + line + "\"");
        } else {
            System.out.println("NO MATCH");
        }

Supón que necesitas extraer varios elementos de una cadena. Si la entrada es

Smith, John
Adams, John Quincy

y quieres salir

John Smith
John Quincy Adams

utiliza lo siguiente:

public class REmatchTwoFields {
    public static void main(String[] args) {
        String inputLine = "Adams, John Quincy";
        // Construct an RE with parens to "grab" both field1 and field2
        Pattern r = Pattern.compile("(.*), (.*)");
        Matcher m = r.matcher(inputLine);
        if (!m.matches())
            throw new IllegalArgumentException("Bad input");
        System.out.println(m.group(2) + ' ' + m.group(1));
    }
}

4.4 Sustituir el texto coincidente

Problema

Una vez encontrado un texto mediante un Patrón, quieres sustituirlo por otro diferente, sin alterar el resto de la cadena.

Solución

Como vimos en la receta anterior, los patrones regex que incluyen cuantificadores pueden coincidir con muchos caracteres con muy pocos metacaracteres. Necesitamos una forma de sustituir el texto con el que coincidió la expresión regular sin cambiar otro texto anterior o posterior. Podríamos hacerlo manualmente utilizando el método String substring() . Sin embargo, como es un requisito tan común, la API de Expresiones Regulares de Java proporciona algunos métodos de sustitución.

Debate

La clase Matcher proporciona varios métodos para sustituir sólo el texto que coincida con el patrón. En todos estos métodos, pasas el texto de sustitución, o "lado derecho" de la sustitución (este término es histórico: en el comando de sustitución de un editor de texto de línea de comandos, el lado izquierdo es el patrón y el lado derecho es el texto de sustitución). Estos son los métodos de sustitución:

replaceAll(newString)

Sustituye todas las ocurrencias que coincidan por la nueva cadena

replaceFirst(newString)

Como arriba, pero sólo la primera vez

appendReplacement(StringBuffer, newString)

Copias hasta antes del primer partido, más las dadas newString

appendTail(StringBuffer)

Añade texto después de la última coincidencia (normalmente se utiliza después de appendReplacement)

A pesar de sus nombres, los métodos replace* se comportan de acuerdo con la inmutabilidad de Strings (véase "Intemporal, inmutable e inmutable"): crean un nuevo objeto String con la sustitución realizada; no modifican (de hecho, no podrían hacerlo) la cadena a la que se refiere el objeto Matcher.

El Ejemplo 4-3 muestra el uso de estos tres métodos.

Ejemplo 4-3. main/src/main/java/regex/ReplaceDemo.java
/**
 * Quick demo of RE substitution: correct U.S. 'favor'
 * to Canadian/British 'favour', but not in "favorite"
 * @author Ian F. Darwin, http://www.darwinsys.com/
 */
public class ReplaceDemo {
    public static void main(String[] argv) {

        // Make an RE pattern to match as a word only (\b=word boundary)
        String patt = "\\bfavor\\b";

        // A test input
        String input = "Do me a favor? Fetch my favorite.";
        System.out.println("Input: " + input);

        // Run it from a RE instance and see that it works
        Pattern r = Pattern.compile(patt);
        Matcher m = r.matcher(input);
        System.out.println("ReplaceAll: " + m.replaceAll("favour"));

        // Show the appendReplacement method
        m.reset();
        StringBuffer sb = new StringBuffer();
        System.out.print("Append methods: ");
        while (m.find()) {
            // Copy to before first match,
            // plus the word "favor"
            m.appendReplacement(sb, "favour");
        }
        m.appendTail(sb);        // copy remainder
        System.out.println(sb.toString());
    }
}

Efectivamente, cuando lo ejecutas, hace lo que esperamos:

Input: Do me a favor? Fetch my favorite.
ReplaceAll: Do me a favour? Fetch my favorite.
Append methods: Do me a favour? Fetch my favorite.

El método replaceAll() maneja el caso de hacer el mismo cambio en toda una cadena. Si quieres cambiar cada ocurrencia coincidente a un valor diferente, puedes utilizar replaceFirst() en un bucle, como en el Ejemplo 4-4. Aquí hacemos una pasada por toda la cadena, convirtiendo cada aparición de cat o dog en feline o canine. Esto es una simplificación de un ejemplo real que buscaba URLs de bit.ly y las sustituía por la URL real; el método computeReplacement utilizaba el código del cliente de red de la Receta 12.1.

Ejemplo 4-4. main/src/main/java/regex/ReplaceMulti.java
/**
 * To perform multiple distinct substitutions in the same String,
 * you need a loop, and must call reset() on the matcher.
 */
public class ReplaceMulti {
    public static void main(String[] args) {

        Pattern patt = Pattern.compile("cat|dog");
        String line = "The cat and the dog never got along well.";
        System.out.println("Input: " + line);
        Matcher matcher = patt.matcher(line);
        while (matcher.find()) {
            String found = matcher.group(0);
            String replacement = computeReplacement(found);
            line = matcher.replaceFirst(replacement);
            matcher.reset(line);
        }
        System.out.println("Final: " + line);
    }

    static String computeReplacement(String in) {
        switch(in) {
        case "cat": return "feline";
        case "dog": return "canine";
        default: return "animal";
        }
    }
}

Si necesitas referirte a partes de la ocurrencia que coinciden con la expresión regular, puedes marcarlas con paréntesis adicionales en el patrón y referirte a la parte coincidente con $1, $2, y así sucesivamente en la cadena de sustitución. El ejemplo 4-5 utiliza esto para intercambiar dos campos, en este caso, convertir nombres de la forma Firstname Lastname en Lastname, FirstName.

Ejemplo 4-5. main/src/main/java/regex/ReplaceDemo2.java
public class ReplaceDemo2 {
    public static void main(String[] argv) {

        // Make an RE pattern 
        String patt = "(\\w+)\\s+(\\w+)";

        // A test input
        String input = "Ian Darwin";
        System.out.println("Input: " + input);

        // Run it from a RE instance and see that it works
        Pattern r = Pattern.compile(patt);
        Matcher m = r.matcher(input);
        m.find();
        System.out.println("Replaced: " + m.replaceFirst("$2, $1"));
        
        // The short inline version:
        // System.out.println(input.replaceFirst("(\\w+)\\s+(\\w+)", "$2, $1"));
    }
}

4.5 Imprimir todas las ocurrencias de un patrón

Problema

Necesitas encontrar todas las cadenas que coincidan con una expresión regular determinada en uno o varios archivos u otras fuentes.

Solución

Este ejemplo lee un archivo línea a línea. Cada vez que se encuentra una coincidencia, la extraigo de line y la imprimo.

Este código toma los métodos group() de la Receta 4.3, el método substring de la interfaz CharacterIterator, y el método match() de la expresión regular y simplemente los pone todos juntos. Lo codifiqué para extraer todos los nombres de un archivo determinado; al ejecutar el programa por sí mismo, imprime las palabras import, java, until, regex, etc., cada una en su propia línea:

C:\> java ReaderIter.java ReaderIter.java
import
java
util
regex
import
java
io
Print
all
the
strings
that
match
given
pattern
from
file
public
...
C:\\>

Lo he interrumpido aquí para ahorrar papel. Esto se puede escribir de dos formas: un patrón línea a línea que se muestra en el Ejemplo 4-6 y una forma más compacta utilizando la nueva E/S que se muestra en el Ejemplo 4-7 (el nuevo paquete de E/S utilizado en ambos ejemplos se describe en el Capítulo 10).

Ejemplo 4-6. main/src/main/java/regex/ReaderIter.java
public class ReaderIter {
    public static void main(String[] args) throws IOException {
        // The RE pattern
        Pattern patt = Pattern.compile("[A-Za-z][a-z]+");
        // See the I/O chapter
        // For each line of input, try matching in it.
        Files.lines(Path.of(args[0])).forEach(line -> {
            // For each match in the line, extract and print it.
            Matcher m = patt.matcher(line);
            while (m.find()) {
                // Simplest method:
                // System.out.println(m.group(0));

                // Get the starting position of the text
                int start = m.start(0);
                // Get ending position
                int end = m.end(0);
                // Print whatever matched.
                // Use CharacterIterator.substring(offset, end);
                System.out.println(line.substring(start, end));
            }
        });
    }
}
Ejemplo 4-7. main/src/main/java/regex/GrepNIO.java
public class GrepNIO {
    public static void main(String[] args) throws IOException {

        if (args.length < 2) {
            System.err.println("Usage: GrepNIO patt file [...]");
            System.exit(1);
        }

        Pattern p=Pattern.compile(args[0]);
        for (int i=1; i<args.length; i++)
            process(p, args[i]);
    }

    static void process(Pattern pattern, String fileName) throws IOException {

        // Get a FileChannel from the given file
        FileInputStream fis = new FileInputStream(fileName);
        FileChannel fc = fis.getChannel();

        // Map the file's content
        ByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());

        // Decode ByteBuffer into CharBuffer
        CharBuffer cbuf =
            Charset.forName("ISO-8859-1").newDecoder().decode(buf);

        Matcher m = pattern.matcher(cbuf);
        while (m.find()) {
            System.out.println(m.group(0));
        }
        fis.close();
    }
}

La versión de E/S no bloqueante (NIO) que se muestra en el Ejemplo 4-7 se basa en el hecho de que un Buffer NIO puede utilizarse como CharSequence. Este programa es más general en el sentido de que el argumento del patrón se toma del argumento de la línea de comandos. Imprime la misma salida que el ejemplo anterior si se invoca con el argumento patrón del programa anterior en la línea de comandos:

java regex.GrepNIO "[A-Za-z][a-z]+"  ReaderIter.java

Puedes pensar en utilizar \w+ como patrón; la única diferencia es que mi patrón busca palabras en mayúsculas bien formadas, mientras que \w+ incluiría rarezas centradas en Java como theVariableName, que tienen mayúsculas en posiciones no estándar.

Ten en cuenta también que la versión NIO será probablemente más eficiente porque no reinicia Matcher a una nueva fuente de entrada en cada línea de entrada, como hace ReaderIter.

4.6 Imprimir líneas que contienen un patrón

Problema

Tienes que buscar líneas que coincidan con una expresión regular determinada en uno o varios archivos.

Solución

Escribe un programa sencillo similar a grep.

Debate

Como ya he mencionado, una vez que tengas un paquete regex, puedes escribir un programa similar a grep. Antes di un ejemplo del programa grep de Unix . grep se llama con algunos argumentos opcionales, seguidos de un patrón de expresión regular requerido, seguido de un número arbitrario de nombres de archivo. Imprime cualquier línea que contenga el patrón, a diferencia de la Receta 4.5, que sólo imprime el texto coincidente. Aquí tienes un ejemplo:

grep "[dD]arwin" *.txt 

El código busca líneas que contengan darwin o Darwin en cada línea de cada archivo cuyo nombre termine en .txt.3 El Ejemplo 4-8 es el código fuente de la primera versión de un programa para hacer esto, llamado Grep0. Lee líneas de la entrada estándar y no acepta argumentos opcionales, pero maneja todo el conjunto de expresiones regulares que implementa la clase Pattern (por tanto, no es idéntico a los programas Unix del mismo nombre). Aún no hemos cubierto el paquete java.io para entrada y salida (ver Capítulo 10), pero nuestro uso de él aquí es lo suficientemente sencillo como para que probablemente puedas intuirlo. La fuente en línea incluye Grep1, que hace lo mismo pero está mejor estructurado (y, por tanto, es más largo). Más adelante en este capítulo, la Receta 4.11 presenta un programa JGrep que analiza un conjunto de opciones de la línea de comandos.

Ejemplo 4-8. main/src/main/java/regex/Grep0.java
public class Grep0 {
    public static void main(String[] args) throws IOException {
        BufferedReader is =
            new BufferedReader(new InputStreamReader(System.in));
        if (args.length != 1) {
            System.err.println("Usage: MatchLines pattern");
            System.exit(1);
        }
        Pattern patt = Pattern.compile(args[0]);
        Matcher matcher = patt.matcher("");
        String line = null;
        while ((line = is.readLine()) != null) {
            matcher.reset(line);
            if (matcher.find()) {
                System.out.println("MATCH: " + line);
            }
        }
    }
}

4.7 Control de mayúsculas y minúsculas en expresiones regulares

Problema

Quieres encontrar el texto independientemente de las mayúsculas y minúsculas.

Solución

Compila el Pattern pasando el argumento flags Pattern.CASE_INSENSITIVE para indicar que la concordancia debe ser independiente de mayúsculas y minúsculas (es decir, que debe doblarse, ignorando las diferencias entre mayúsculas y minúsculas). Si tu código puede ejecutarse en diferentes configuraciones regionales (verReceta 3.12), entonces debes añadir Pattern.UNICODE_CASE. Sin estas opciones, el comportamiento por defecto es el normal, que distingue entre mayúsculas y minúsculas. Esta bandera (y otras) se pasan al método Pattern.compile() así:

// regex/CaseMatch.java
Pattern  reCaseInsens = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE |
    Pattern.UNICODE_CASE);
reCaseInsens.matches(input);        // will match case-insensitively

Esta bandera debe pasarse cuando crees el Pattern; como los objetos Pattern son inmutables, no pueden cambiarse una vez construidos.

El código fuente completo de este ejemplo está en línea como CaseMatch.java.

4.8 Emparejar caracteres acentuados o compuestos

Problema

Quieres que los caracteres coincidan independientemente de la forma en que se introduzcan.

Solución

Compila el Pattern con el argumento flags Pattern.CANON_EQ para la igualdad canónica.

Debate

Los caracteres compuestos pueden introducirse de varias formas. Considera, como ejemplo único, la letra e con acento agudo. Este carácter puede encontrarse de varias formas en el texto Unicode, como el carácter único é (carácter Unicode \u00e9) o la secuencia de dos caracteres (e seguida del acento agudo de combinación Unicode, \u0301). Para que puedas hacer coincidir dichos caracteres independientemente de cuál de las posibles múltiples formas totalmente descompuestas se utilice para introducirlos, el paquete regex tiene una opción para lacoincidencia canónica , que trata cualquiera de las formas como equivalente. Esta opción se activa pasando CANON_EQ como (una de) las banderas en el segundo argumento a Pattern.compile(). Este programa muestra CANON_EQ siendo utilizado para hacer coincidir varias formas:

public class CanonEqDemo {
    public static void main(String[] args) {
        String pattStr = "\u00e9gal"; // egal
        String[] input = {
                "\u00e9gal", // egal - this one had better match :-)
                "e\u0301gal", // e + "Combining acute accent"
                "e\u02cagal", // e + "modifier letter acute accent"
                "e'gal", // e + single quote
                "e\u00b4gal", // e + Latin-1 "acute"
        };
        Pattern pattern = Pattern.compile(pattStr, Pattern.CANON_EQ);
        for (int i = 0; i < input.length; i++) {
            if (pattern.matcher(input[i]).matches()) {
                System.out.println(
                    pattStr + " matches input " + input[i]);
            } else {
                System.out.println(
                    pattStr + " does not match input " + input[i]);
            }
        }
    }
}

Este programa empareja correctamente el acento de combinación y rechaza los demás caracteres, algunos de los cuales, por desgracia, se parecen al acento en una impresora, pero no se consideran caracteres de acento de combinación:

égal matches input égal
égal matches input e?gal
égal does not match input e?gal
égal does not match input e'gal
égal does not match input e´gal

Para más detalles, consulta las tablas de caracteres.

4.9 Hacer coincidir nuevas líneas en el texto

Problema

Tienes que hacer coincidir las nuevas líneas en el texto.

Solución

Utiliza \n o \r en tu patrón regex. Consulta también la constante Pattern.MULTILINE, que hace que las nuevas líneas coincidan como principio de línea y final de línea (\^ y $).

Debate

Aunque las herramientas orientadas a líneas de Unix, como sed y grep, emparejan las expresiones regulares línea a línea, no todas las herramientas lo hacen. El editor de texto sam de los Laboratorios Bell fue la primera herramienta interactiva que conozco que permitía expresiones regulares multilínea; el lenguaje de scripting Perl le siguió poco después. En la API de Java, el carácter de nueva línea no tiene por defecto ningún significado especial. El método BufferedReader readLine() elimina normalmente los caracteres de nueva línea que encuentra. Si lees montones de caracteres utilizando algún método distinto de readLine(), es posible que tengas un cierto número de secuencias \n, \r o \r\n en tu cadena de texto.4 Normalmente, todas ellas se tratan como equivalentes a \n. Si sólo quieres que coincidan con \n, utiliza la bandera UNIX_LINES en el método Pattern.compile().

En Unix, ^ y $ se utilizan habitualmente para coincidir con el principio o el final de una línea, respectivamente. En esta API, los metacaracteres regex \^ y $ ignoran los terminadores de línea y sólo coinciden al principio y al final, respectivamente, de toda la cadena. Sin embargo, si pasas la bandera MULTILINE a Pattern.compile() , estas expresiones coinciden justo después o justo antes, respectivamente, de un terminador de línea; $ también coincide con el final de la cadena. Como el final de línea es sólo un carácter ordinario, puedes hacerlo coincidir con . o expresiones similares; y, si quieres saber exactamente dónde está, \n o \r en el patrón también lo hacen coincidir. En otras palabras, para esta API, un carácter de nueva línea no es más que otro carácter sin significado especial. Consulta la barra lateral "Banderas de Patrón.compilar()". En el Ejemplo 4-9 se muestra un ejemplo de coincidencia con una nueva línea.

Ejemplo 4-9. main/src/main/java/regex/NLMatch.java
public class NLMatch {
    public static void main(String[] argv) {

        String input = "I dream of engines\nmore engines, all day long";
        System.out.println("INPUT: " + input);
        System.out.println();

        String[] patt = {
            "engines.more engines",
            "ines\nmore",
            "engines$"
        };

        for (int i = 0; i < patt.length; i++) {
            System.out.println("PATTERN " + patt[i]);

            boolean found;
            Pattern p1l = Pattern.compile(patt[i]);
            found = p1l.matcher(input).find();
            System.out.println("DEFAULT match " + found);

            Pattern pml = Pattern.compile(patt[i], 
                Pattern.DOTALL|Pattern.MULTILINE);
            found = pml.matcher(input).find();
            System.out.println("MultiLine match " + found);
            System.out.println();
        }
    }
}

Si ejecutas este código, el primer patrón (con el carácter comodín .) siempre coincide, mientras que el segundo patrón (con $) sólo coincide cuando MATCH_MULTILINE está activado:

> java regex.NLMatch
INPUT: I dream of engines
more engines, all day long

PATTERN engines
more engines
DEFAULT match true
MULTILINE match: true

PATTERN engines$
DEFAULT match false
MULTILINE match: true

4.10 Programa: Análisis de archivos de registro de Apache

El servidor web Apache es el principal servidor web del mundo y lo ha sido durante la mayor parte de la historia de la web. Es uno de los proyectos de código abierto más conocidos del mundo, y es el primero de muchos fomentados por la Fundación Apache. A menudo se afirma que el nombre Apache es un juego de palabras sobre los orígenes del servidor; sus desarrolladores empezaron con el servidor libre NCSA y siguieron pirateándolo, o parcheándolo, hasta que hizo lo que ellos querían. Cuando fue lo suficientemente diferente del original, se necesitó un nuevo nombre. Como ahora era un servidor parcheado, se eligió el nombre de Apache. Los funcionarios niegan la historia, pero de todos modos es simpática. Un lugar en el que sí se nota la irregularidad real es en el formato del archivo de registro. Mira el Ejemplo 4-10.

Ejemplo 4-10. Extracto del archivo de registro de Apache
123.45.67.89 - - [27/Oct/2000:09:27:09 -0400] "GET /java/javaResources.html
HTTP/1.0" 200 10450 "-" "Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)"

El formato del archivo se diseñó obviamente para la inspección humana, pero no para un análisis sintáctico fácil. El problema es que se utilizan distintos delimitadores: corchetes para la fecha, comillas para la línea de solicitud, y espacios salpicados por todas partes. Considera la posibilidad de intentar utilizar un StringTokenizer; puede que consigas que funcione, pero pasarías mucho tiempo jugueteando con él. En realidad, no, no funcionaría. Sin embargo, esta expresión regular algo retorcida5 facilita el análisis sintáctico (se trata de una única expresión regular del tamaño de Moby; tuvimos que dividirla en dos líneas para que se ajustara a los márgenes del libro):

\^([\d.]+) (\S+) (\S+) \[([\w:/]+\s[+\-]\d{4})\] "(.+?)" (\d{3}) (\d+)
  "([\^"]+)" "([\^"]+)"

Puede que te resulte informativo volver a consultar la Tabla 4-1 y revisar la sintaxis completa utilizada aquí. Fíjate en particular en el uso del cuantificador sin comillas +? en \"(.+?)\" para que coincida con una cadena entrecomillada; no puedes utilizar simplemente .+ porque eso coincidiría demasiado (hasta la comilla del final de la línea). El código para extraer los distintos campos, como la dirección IP, la solicitud, la URL de referencia y la versión del navegador, se muestra en el Ejemplo 4-11.

Ejemplo 4-11. main/src/main/java/regex/LogRegExp.java
public class LogRegExp {

    final static String logEntryPattern = 
            "^([\\d.]+) (\\S+) (\\S+) \\[([\\w:/]+\\s[+-]\\d{4})\\] " +
            "\"(.+?)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]+)\"";

    public static void main(String argv[]) {

        System.out.println("RE Pattern:");
        System.out.println(logEntryPattern);

        System.out.println("Input line is:");
        String logEntryLine = LogParseInfo.LOG_ENTRY_LINE;
        System.out.println(logEntryLine);

        Pattern p = Pattern.compile(logEntryPattern);
        Matcher matcher = p.matcher(logEntryLine);
        if (!matcher.matches() || 
            LogParseInfo.MIN_FIELDS > matcher.groupCount()) {
            System.err.println("Bad log entry (or problem with regex):");
            System.err.println(logEntryLine);
            return;
        }
        System.out.println("IP Address: " + matcher.group(1));
        System.out.println("UserName: " + matcher.group(3));
        System.out.println("Date/Time: " + matcher.group(4));
        System.out.println("Request: " + matcher.group(5));
        System.out.println("Response: " + matcher.group(6));
        System.out.println("Bytes Sent: " + matcher.group(7));
        if (!matcher.group(8).equals("-"))
            System.out.println("Referer: " + matcher.group(8));
        System.out.println("User-Agent: " + matcher.group(9));
    }
}

La cláusula implements es para una interfaz que sólo define la cadena de entrada; se utilizó en una demostración para comparar el modo de expresión regular con el uso de una StringTokenizer. El código fuente de ambas versiones está en el código fuente en línea de este capítulo. Si ejecutas el programa con la entrada de ejemplo del Ejemplo 4-10, obtendrás este resultado:

Using regex Pattern:
\^([\d.]+) (\S+) (\S+) \[([\w:/]+\s[+\-]\d{4})\] "(.+?)" (\d{3}) (\d+) "([\^"]+)"
"([\^"]+)"
Input line is:
123.45.67.89 - - [27/Oct/2000:09:27:09 -0400] "GET /java/javaResources.html
HTTP/1.0" 200 10450 "-" "Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)"
IP Address: 123.45.67.89
Date&Time: 27/Oct/2000:09:27:09 -0400
Request: GET /java/javaResources.html HTTP/1.0
Response: 200
Bytes Sent: 10450
Browser: Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)

El programa analizó con éxito toda la entrada de formato de archivo de registro con una sola llamada a matcher.matches() .

4.11 Programa: Grep completo

Ahora que hemos visto cómo funciona el paquete de expresiones regulares, es hora de escribir JGrep, una versión completa del programa de comparación de líneas con análisis sintáctico de opciones. La Tabla 4-2 enumera algunas opciones típicas de la línea de comandos que podría incluir una implementación Unix de grep. Para quienes no estén familiarizados con grep, se trata de una herramienta de línea de comandos que busca expresiones regulares en archivos de texto. Hay tres o cuatro programas de la familia grep estándar, y varios sustitutos más recientes como ripgrep, o rg. Este programa es mi adición a esta familia de programas.

Tabla 4-2. Opciones de la línea de comandos Grep
Opción Significado

-c

Sólo contar; no imprimir líneas, sólo contarlas

-C

Contexto; imprime algunas líneas por encima y por debajo de cada línea que coincida (no implementado en esta versión; se deja como ejercicio para el lector)

-f patrón

Toma el patrón del archivo cuyo nombre es -f en lugar de la línea de comandos

-h

Suprimir la impresión del nombre de archivo delante de las líneas

-i

Ignorar caso

-l

Listar sólo nombres de archivo: no imprimir líneas, sólo los nombres en los que se encuentran

-n

Imprime los números de línea antes de las líneas coincidentes

-s

Suprimir la impresión de determinados mensajes de error

-v

Invertir: imprime sólo las líneas que NO coinciden con el patrón

El mundo Unix de incluye varias rutinas de la biblioteca getopt para analizar los argumentos de la línea de comandos, así que he hecho una reimplementación de esto en Java. Como de costumbre, dado que main() se ejecuta en un contexto estático pero la línea principal de nuestra aplicación no, podríamos acabar pasando mucha información al constructor. Para ahorrar espacio, esta versión sólo utiliza variables globales para rastrear las opciones de la línea de comandos. A diferencia de la herramienta grep de Unix, ésta aún no maneja opciones combinadas, por lo que -l -r -i está bien, pero -lri fallará, debido a una limitación en el analizador sintáctico GetOpt utilizado.

Básicamente, el programa sólo lee las líneas, busca el patrón en ellas y, si encuentra una coincidencia (o no la encuentra, con -v), imprime la línea (y opcionalmente alguna otra cosa también). Dicho todo esto, el código se muestra en el Ejemplo 4-12.

Ejemplo 4-12. darwinsys-api/src/main/java/regex/JGrep.java
/** A command-line grep-like program. Accepts some command-line options,
 * and takes a pattern and a list of text files.
 * N.B. The current implementation of GetOpt does not allow combining short 
 * arguments, so put spaces e.g., "JGrep -l -r -i pattern file..." is OK, but
 * "JGrep -lri pattern file..." will fail. Getopt will hopefully be fixed soon.
 */
public class JGrep {
    private static final String USAGE =
        "Usage: JGrep pattern [-chilrsnv][-f pattfile][filename...]";
    /** The pattern we're looking for */
    protected Pattern pattern;
    /** The matcher for this pattern */
    protected Matcher matcher;
    private boolean debug;
    /** Are we to only count lines, instead of printing? */
    protected static boolean countOnly = false;
    /** Are we to ignore case? */
    protected static boolean ignoreCase = false;
    /** Are we to suppress printing of filenames? */
    protected static boolean dontPrintFileName = false;
    /** Are we to only list names of files that match? */
    protected static boolean listOnly = false;
    /** Are we to print line numbers? */
    protected static boolean numbered = false;
    /** Are we to be silent about errors? */
    protected static boolean silent = false;
    /** Are we to print only lines that DONT match? */
    protected static boolean inVert = false;
    /** Are we to process arguments recursively if directories? */
    protected static boolean recursive = false;

    /** Construct a Grep object for the pattern, and run it
     * on all input files listed in args.
     * Be aware that a few of the command-line options are not
     * acted upon in this version - left as an exercise for the reader!
     * @param args args
     */
    public static void main(String[] args) {

        if (args.length < 1) {
            System.err.println(USAGE);
            System.exit(1);
        }
        String patt = null;

        GetOpt go = new GetOpt("cf:hilnrRsv");

        char c;
        while ((c = go.getopt(args)) != 0) {
            switch(c) {
                case 'c':
                    countOnly = true;
                    break;
                case 'f':    /* External file contains the pattern */
                    try (BufferedReader b = 
                        new BufferedReader(new FileReader(go.optarg()))) {
                        patt = b.readLine();
                    } catch (IOException e) {
                        System.err.println(
                            "Can't read pattern file " + go.optarg());
                        System.exit(1);
                    }
                    break;
                case 'h':
                    dontPrintFileName = true;
                    break;
                case 'i':
                    ignoreCase = true;
                    break;
                case 'l':
                    listOnly = true;
                    break;
                case 'n':
                    numbered = true;
                    break;
                case 'r':
                case 'R':
                    recursive = true;
                    break;
                case 's':
                    silent = true;
                    break;
                case 'v':
                    inVert = true;
                    break;
                case '?':
                    System.err.println("Getopts was not happy!");
                    System.err.println(USAGE);
                    break;
            }
        }

        int ix = go.getOptInd();

        if (patt == null)
            patt = args[ix++];

        JGrep prog = null;
        try {
            prog = new JGrep(patt);
        } catch (PatternSyntaxException ex) {
            System.err.println("RE Syntax error in " + patt);
            return;
        }

        if (args.length == ix) {
            dontPrintFileName = true; // Don't print filenames if stdin
            if (recursive) {
                System.err.println("Warning: recursive search of stdin!");
            }
            prog.process(new InputStreamReader(System.in), null);
        } else {
            if (!dontPrintFileName)
                dontPrintFileName = ix == args.length - 1; // Nor if only one file
            if (recursive)
                dontPrintFileName = false;                // unless a directory!

            for (int i=ix; i<args.length; i++) { // note starting index
                try {
                    prog.process(new File(args[i]));
                } catch(Exception e) {
                    System.err.println(e);
                }
            }
        }
    }

    /**
     * Construct a JGrep object.
     * @param patt The regex to look for
     * @throws PatternSyntaxException if pattern is not a valid regex
     */
    public JGrep(String patt) throws PatternSyntaxException {
        if (debug) {
            System.err.printf("JGrep.JGrep(%s)%n", patt);
        }
        // compile the regular expression
        int caseMode = ignoreCase ?
            Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE :
            0;
        pattern = Pattern.compile(patt, caseMode);
        matcher = pattern.matcher("");
    }

    /** Process one command line argument (file or directory)
     * @param file The input File
     * @throws FileNotFoundException If the file doesn't exist
     */
    public void process(File file) throws FileNotFoundException {
        if (!file.exists() || !file.canRead()) {
            throw new FileNotFoundException(
                "Can't read file " + file.getAbsolutePath());
        }
        if (file.isFile()) {
            process(new BufferedReader(new FileReader(file)), 
                file.getAbsolutePath());
            return;
        }
        if (file.isDirectory()) {
            if (!recursive) {
                System.err.println(
                    "ERROR: -r not specified but directory given " + 
                    file.getAbsolutePath());
                return;
            }
            for (File nf : file.listFiles()) {
                process(nf);    // "Recursion, n.: See Recursion."
            }
            return;
        }
        System.err.println(
            "WEIRDNESS: neither file nor directory: " + file.getAbsolutePath());
    }

    /** Do the work of scanning one file
     * @param    ifile    Reader    Reader object already open
     * @param    fileName String    Name of the input file
     */
    public void process(Reader ifile, String fileName) {

        String inputLine;
        int matches = 0;

        try (BufferedReader reader = new BufferedReader(ifile)) {

            while ((inputLine = reader.readLine()) != null) {
                matcher.reset(inputLine);
                if (matcher.find()) {
                    if (listOnly) {
                        // -l, print filename on first match, and we're done
                        System.out.println(fileName);
                        return;
                    }
                    if (countOnly) {
                        matches++;
                    } else {
                        if (!dontPrintFileName) {
                            System.out.print(fileName + ": ");
                        }
                        System.out.println(inputLine);
                    }
                } else if (inVert) {
                    System.out.println(inputLine);
                }
            }
            if (countOnly)
                System.out.println(matches + " matches in " + fileName);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

1 Los no aficionados a Unix no deben temer, porque puedes utilizar herramientas como grep en sistemas Windows utilizando uno de varios paquetes. Uno es un paquete de código abierto llamado alternativamente CygWin (por Cygnus Software) o GnuWin32. Otro es el comando findstr de Microsoft para Windows. O puedes utilizar mi programa Grep de la Receta 4.6 si no tienes grep en tu sistema. Por cierto, el nombre grep proviene de un antiguo comando del editor de líneas Unix g/RE/p, el comando para encontrar la expresión regular globalmente en todas las líneas del búfer de edición e imprimir las líneas que coincidan, justo lo que el programa grep hace con las líneas de los archivos.

2 REDemo se inspiró en (pero no utiliza ningún código de) un programa similar proporcionado con el ahora retirado paquete Expresiones Regulares de Apache Jakarta.

3 En Unix, la shell o el intérprete de línea de comandos expande *.txt a todos los nombres de archivo coincidentes antes de ejecutar el programa, pero el intérprete normal de Java lo hace por ti en los sistemas en los que la shell no es lo suficientemente enérgica o brillante como para hacerlo.

4 O algunos caracteres Unicode relacionados, incluidos los caracteres de línea siguiente (\u0085), separador de línea (\u2028) y separador de párrafo (\u2029).

5 Podrías pensar que esto ostentaría algún tipo de récord mundial de complejidad en competiciones de expresiones regulares, pero estoy seguro de que ha sido superado muchas veces.

Get Libro de cocina de Java, 4ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.