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(String
seguido 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
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.
Subexpresión | Partidos | Notas |
---|---|---|
General |
||
|
Inicio de línea/cadena |
|
|
Fin de línea/cadena |
|
|
Límite de la palabra |
|
|
Ni una palabra límite |
|
|
Comienzo de toda la cadena |
|
|
Fin de toda la cadena |
|
|
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 |
|
|
Paréntesis de no captura |
|
|
Fin del partido anterior |
|
+\+ |
Referencia retrospectiva para capturar el número de grupo |
|
Cuantificadores normales (codiciosos) |
||
|
Cuantificador para de |
Ver Receta 4.4 |
|
Cuantificador para |
|
|
Cuantificador para exactamente |
Ver Receta 4.10 |
|
Cuantificador para 0 hasta |
|
|
Cuantificador para 0 o más repeticiones |
Abreviatura de |
|
Cuantificador para 1 o más repeticiones |
Corto para |
|
Cuantificador para 0 ó 1 repeticiones (es decir, presente exactamente una vez o ninguna) |
Abreviatura de |
Cuantificadores reticentes (no reticentes) |
||
|
Cuantificador reticente para de |
|
|
Cuantificador reticente para |
|
|
Cuantificador reticente para 0 hasta |
|
|
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) |
||
|
Cuantificador posesivo para de |
|
|
Cuantificador posesivo para |
|
|
Cuantificador posesivo para 0 hasta |
|
|
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 |
|
|
Escapa (entrecomilla) todos los caracteres hasta |
|
|
Termina la cita iniciada con |
|
|
Carácter de tabulación |
|
|
Carácter de retorno (retorno de carro) |
|
|
Carácter de nueva línea |
Ver Receta 4.9 |
|
Forma de alimentación |
|
|
Carácter en una palabra |
Utiliza |
|
Un carácter que no sea una palabra |
|
|
Dígito numérico |
Utiliza |
|
Un carácter no numérico |
|
|
Espacio en blanco |
Espacio, tabulador, etc., según determine |
|
Un carácter que no sea un espacio en blanco |
Ver Receta 4.10 |
Bloques Unicode (muestras representativas) |
||
|
Un personaje del bloque griego |
(Bloque simple) |
|
Cualquier carácter que no esté en el bloque griego |
|
|
Una letra mayúscula |
(Categoría simple) |
|
Un símbolo monetario |
|
Clases de caracteres estilo POSIX (definidas sólo para US-ASCII) |
||
|
Caracteres alfanuméricos |
|
|
Caracteres alfabéticos |
|
|
Cualquier carácter ASCII |
|
|
Caracteres de espacio y tabulación |
|
|
Caracteres espaciales |
|
|
Caracteres de control |
|
|
Caracteres numéricos |
|
|
Caracteres imprimibles y visibles (no espacios ni caracteres de control) |
|
|
Caracteres imprimibles |
Igual que |
|
Caracteres de puntuación |
Uno de |
|
Caracteres minúsculos |
|
|
Caracteres en mayúsculas |
|
|
Caracteres de dígitos hexadecimales |
|
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.
"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í.
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
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:
-
Crea un
Pattern
llamando al método estáticoPattern.compile()
. -
Solicita un
Matcher
del patrón llamando apattern.matcher(CharSequence)
por cadaString
(u otroCharSequence
) que desees consultar. -
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 todoString
, 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
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 degroupCount()
. El grupo 0 es toda la coincidencia, por lo quegroup(0)
(o simplementegroup()
) 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.
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
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
.
(
"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
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
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
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
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´
(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
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.
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 |
-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
.
(
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.