I. Introducción
Se llama
expresión regular a una cadena de caracteres que representa un conjunto de
cadenas de caracteres.
Así la
cadena 'abc', considerado como una expresión regular, representa un conjunto
que contiene un solo elemento, la cadena 'abc'. Cada uno de los tres
caracteres está representando a sí mismo.
I - A. Clases de caracteres
La expresión regular '[abc]' representa el conjunto de las cadenas que forman un solo carácter en el conjunto ['a', 'b', 'c']. Me gustaría
añadir que los tres caracteres 'a', 'b' y 'c' deben ser consecutivos en el
alfabeto, se podría escribir nuestra expresión regular '[a-c]', así como en
Pascal, podría escribir['a'..'c'] en vez de ['a', 'b', 'c'].
A diferencia
de las letras del alfabeto que se representan simplemente a ellas mismas, los corchetes y el
guión son unos caracteres con un significado especial, que nosotros veremos posteriormente. Para que estos caracteres especiales (llamados también metacaracteres) sean tratados como caracteres normales, es decir interpretados literalmente, deben ser caracteres de "escape", prefijando una barra invertida: '\[', '\]', '\-'. La
barra invertida por lo tanto también es un metacarácter.
Otro
carácter especial del que es necesario hablar en este lugar, es el acento
circunflejo. Cuando se coloca inmediatamente después de un corchete abierto, significa la negación o exclusión. Por ejemplo la clase '[^0-9]' contiene todos los caracteres que no son números.
I B. Clases predefinidas
La barra oblicua invertida se usa también para dar un significado especial a caracteres que
por defecto se interpretan literalmente.
Por ejemplo
la letra 'd', precedida por una barra invertida, representa la clase de dígitos . Las tres expresiones son por lo
tanto similares: '[0123456789]', '[0-9]', '\d'.
Un usuario
advertido sobre las expresiones regulares en Perl me señaló me que en recientes
versiones de Perl, estas tres expresiones no serán sinónimas, porque la clase
'\d' incluye ahora las figuras de todos los alfabetos y no sólo los del
alfabeto latino.
Aquí hay una
tabla que contiene todas las clases predefinidas.
Valorar | Significado | Calificación equivalente |
\a | Campana | \x07 |
\d | Figura | [0-9] |
\e | De escape | \x1B |
\f | Nueva hoja de cálculo | \x0C |
\n | Nueva línea de | \0A |
\r | Retorno de carro | \0D |
\s | Espacio (en un sentido amplio) | [ \t] |
\t | Ficha | \x09 |
\v | Pestaña vertical | \x0B |
\w | Caracteres alfanuméricos (incluyendo '_') | [A-Za-z0-9_] |
. | Todos los personajes (excepto #13 y #10) | [^ \r\n] |
Tenga en
cuenta que si sustituimos 'a', 'd', 'e' o una de las otras letras por una mayúscula,
la expresión designa entonces juntos los caracteres que no son una cifra, etc.. Por lo tanto la clase '\D' es equivalene a la clase [^
0-9]' que vimos anteriormente.
El punto es una clase de caracteres a utilizar con precaución: contiene todos los caracteres,
a excepción del retorno de carro (#13) y salto
de línea (#10).
La barra
invertida se usa todavía para formar la expresión '\b' que significa, no una
clase de caracteres, pero al principio o al final de una palabra, se entiende como
secuencia de caracteres alfanuméricos. Esto es lo que se llama un ancla.
I C. Cuantificadores
Puesto que
ahora sabemos como designar conjuntos de caracteres, vea cómo tener en cuenta
el número de caracteres que se espera. Hemos visto, que por defecto es el número uno.
Para indicar
otro número, o incluso un intervalo, se puede utilizar apoyos, pero también los
caracteres '+', '*', y '?'. Las llaves permiten indicar un número exacto de
caracteres o un intervalo. El símbolo '+' significa una vez o varias. La
estrella significa "cero veces o más. El signo de interrogación
significa "cero o uno".
{Por
ejemplo, la expresión '\d{2}' significa "dos dígitos"; '\d{2,4}' significa "de dos a cuatro dígitos" ; '\d{2,}' significa " dos o más dígitos". La expresión '\d+' significa "una cifra o más." ; '\d*' significa "cero dígitos o
más" ; '\d?' significa "cero cifras o uno".
Por defecto,
los operadores '+', '*', y '?' son glotones,
es decir, la búsqueda devuelve no la primera coincidencia encontrada, sino la más larga. Esto a menudo es importante. Para cambiar este
comportamiento, es decir, para hacerlos perezosos (lazy), los
operadores debe ser seguidos de un signo de interrogación:
I D. Otros operadores
Los paréntesis
también son caracteres igualmente especiales: sirven para delimitar grupos de caracteres,
por ejemplo para aplicar cuantificadores. Así la expresión '(abc)?def' señala
un conjunto que contiene dos elementos, las cadenas 'abcdef' y 'def'. La
interrogación hace que al grupo 'abc' opcional.
Paréntesis
pueden también ser utilizados en combinación con la barra vertical, que
significa "o". Por ejemplo la expresión 'a(b|c)d' y señala un
conjunto que contiene las cadenas 'abd' y 'acd'.
Por último,
el carácter '^' y '$' significan respectivamente el principio y el final de la cadena. Vamos a ver en un momento cuál es el propósito de estos
caracteres. Pero sí, tenéis razón, esto hace que tengas dos diferentes
significados para el acento circunflejo. Dependiendo del lugar donde lo
pones, no se interpretará de la misma manera, y esto se aplica también a otros
caractaeres.
II. comprobar que una cadena es parte de un conjunto
Ahora es el
momento para familiarizarse con la unit RegularExpressions. Esta
unit fue creado con Delphi XE, nos permitirá realizar diversas operaciones
sobre cadenas de caracteres.
La más
simple de estas operaciones consiste en determinar si dicha cadena
pertenece a un conjunto.
II - A. Función IsMatch
La función
que necesitamos para esto es el método IsMatch. Puede ser llamada directamente, con dos cadenas de caracteres como argumentos: el sujeto (la cadena de
estudio) y la expresión regular.
program
IsMatch1;
{$
APPTYPE
CONSOLE
}
uses
RegularExpressions;
begin
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\w
'
));//
un
caractère
alphanumérique
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\w+
'
));//
un
ou
plusieurs
caractères
alphanumériques
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\w*
'
));//
zéro
ou
plus
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\w{7}
'
));//
sept
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
[a-z]{7}
'
));//
sept
minuscules
WriteLn(not
TRegEx.IsMatch('
bonjour
'
,'
\d
'
));//
un
chiffre
WriteLn(not
TRegEx.IsMatch('
bonjour
'
,'
\s
'
));//
un
espace
au
sens
large
(équivalent
à
'[0-9]')
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\D
'
));//
un
caractère
qui
n'est
pas
un
chiffre
ReadLn;
end
.Como han
notado si vieron de cerca el código anterior, la función IsMatch comprueba
sólo que al menos una parte de la cadena pasada como primer
argumento pertenece al conjunto representado por la expresión regular.
Si se quiere
asegurar de que la cadena entera pertenece al conjunto, se debe utilizar los
operadores '^' y '$', que significan respectivamente el inicio de la secuencia y
su final.
WriteLn(TRegEx.IsMatch('
bonjour
'
,'
\w
'
));//
TRUE
WriteLn(not
TRegEx.IsMatch('
bonjour
'
,'
^\w$
'
));//
TRUE
En 'bonjour',
hay varios caracteres alfanuméricos, pero no hay mas que un solo carácter
entre el principio y el fin de la cadena.
III. Extracto de un grupos de cadena de caracteres
III - A. Ejemplo
Imaginemos
que queremos, no sólo verificar que una cadena dada pertenece a un
conjunto, sino también extraer ciertos grupos de caracteres de esta cadena. Decir
por ejemplo queremos extraer grupos de dígitos de una cadena que contiene una
fecha como
program
Group1;
{$
APPTYPE
CONSOLE
}
uses
SysUtils, RegularExpressions;
const
SUBJECT ='
26/09/2015
'
;
PATTERN ='
(\d{2})/(\d{2})/(\d{4})
'
;var
expr: TRegEx;
match: TMatch;
group: TGroup;begin
expr := TRegEx.Create(PATTERN);
match := expr.Match(SUBJECT);if
match.Successthen
for
groupin
match.Groupsdo
WriteLn(Format('
TGroup.Index=%d
TGroup.Value="%s"
'
, [group.Index
, group.Value]));
ReadLn;end
.
Hemos
utilizado en nuestra expresión regular unos paréntesis para delimitar los grupos de
caracteres a extraer, después hemos usado la función Match. De cualquier
manera, no hemos verificado que las cifras extraídas eran de hecho una fecha válida. La expresión utilizada no tiene en cuenta de hecho que no hay más que 31 días en el mes, 12 meses
en un año, etc.. Una auditoría completa debería tomar en cuenta los
años bisiestos, pero esto es improbable que esto sea alcanzable a través de
expresiones regulares !
III - B. Grupos con nombre
También
podemos nombrar los grupos. Para nombrar un grupo, debe insertarse justo
después del paréntesis abierto en los caracteres
program
Group2;
{$
APPTYPE
CONSOLE
}
uses
SysUtils, RegularExpressions;
const
SUBJECT ='
26/09/2015
'
;
PATTERN ='
(
'
+'
?
'
+'
\d{2})/(
'
+'
?
'
+'
\d{2})/(
'
+'
?
'
+'
\d{4})
'
;var
group: TGroup;
match: TMatch;
begin
regEx: TRegEx;
regEx := TRegEx.Create(PATTERN, []);if
match.Successthen
match := regEx.Match(SUBJECT);begin
group := match.Groups['
year
'
];
WriteLn(group.Value);end
;
ReadLn;
end
.
III - C. Paréntesis no subcadenas
A veces es
necesario delimitar un grupo de caracteres, pero no es necesario capturar
los caracteres correspondientes. En este caso, para no dar trabajo innecesario
al programa, vamos a utilizar paréntesis no capturanes.
Imaginemos por
ejemplo que desea detectar en un código fuente en Pascal los Write y WriteLn,
sin ninguna diferencia entre los dos. Se podría usar la expresión 'Write(Ln)?'. Pero de esta forma se capturaría cada vez o una cadena vacía o la cadena
'Ln'. Sin embargo suponemos que no se quiera tener en cuenta la diferencia entre Write y WriteLn. Se utilizará
la siguiente expresión:
'
Write(?:Ln)?
'
Los
caracteres '?:' se añadieron inmediatamente después de la apertura del paréntesis para hacer paréntesis no de captura.
IV. la detección de múltiples partidos
Es posible
detectar múltiples grupos de caracteres que, en una cadena dada, pertenecen al
mismo conjunto.Para ello necesitamos las funciones
IV - A. La función NextMatch
En el
ejemplo siguiente se detecta grupos de caracteres que representan números.
program
Match1a;
{$
APPTYPE
CONSOLE
}
uses
SysUtils, RegularExpressions;
var
match: TMatch;
begin
match := TRegEx.Match('
10
+10
0.5
.5
'
,'
\s*[-+]?[0-9]*\.?[0-9]+\s*
'
);while
match.Successdo
begin
WriteLn(match.Value);
match := match.NextMatch;end
;
ReadLn;
end
.
IV - B. Tipo TMatchCollection
La misma operación se puede hacer de una manera diferente: a través de la función Matches,
que devuelve un resultado de tipo TMatchCollection.
program
MatchCollection1;
{$
APPTYPE
CONSOLE
}
uses
SysUtils,
RegularExpressions;var
expr: TRegEx;
collection: TMatchCollection;
i:Integer
;begin
expr.Create('
\w
'
);
collection := expr.Matches('
abc
'
);for
i :=0
to
collection.Count -1
do
with
collection[i]do
WriteLn(Format('
%d
%d
%d
%s
'
, [i,Index
, Length, Value]));
ReadLn;end
.
V. ruptura de una cadena
Descubre
ahora cómo romper una cadena mediante una expresión regular, es decir, cortar según una cierta regla y disponer los trozos en una tabla.
V. Ejemplo
La expresión
representa a un grupo de personajes que se tratan como delimitadores. En
el caso más simple, se trata de un solo carácter:
program
Split1;
{$
APPTYPE
CONSOLE
}
uses
SysUtils, RegularExpressions;var
a: TArray<string
>;
s:string
;
i:integer
;begin
a := TRegEx.Split(GetEnvironmentVariable('
PATH
'
),'
;
'
);for
sin
ado
WriteLn(s);
a := TRegEx.Split('
a
b,c-d
'
,'
[
,-]
'
);for
i :=0
to
High(a)do
WriteLn(a[i]);
ReadLn;end
.
VI. sustitución de grupos de caracteres
La siguiente
operación es un poco similar el anterior: en lugar de tratar a ciertos grupos de
caracteres como delimitadores, los reemplazaremos por medio de la expresión Replace.
VI - A. Reemplazo por una cadena constante
program
Replace1;
{$
APPTYPE
CONSOLE
}
uses
RegularExpressions;
begin
WriteLn(TRegEx.Replace('
WRITELN
writeln
'
,'
writeln
'
,'
WriteLn
'
, [roIgnoreCase]));end
.
ReadLn;
Como se
puede ver, la función Replace acepta cuatro parámetros: la cadena a tratar, la expresión regular que representa a los a grupos de sustituir, la
cadena por la que se sustituirá estos grupos, y finalmente un conjunto de
opciones. En el ejemplo anterior, se utilizó la opción que permite obtener una sustitución que no tiene en cuenta el caso de grupos a sustituir .
VI - B. Reemplazo por una cadena de compuestos
En lugar de reemplazar los grupos de cadena constante, se puede reemplazar por una cadena variable, compuesta de elementos de los grupos detectados.
program
Replace2;
{$
APPTYPE
CONSOLE
}
uses
RegularExpressions;
var
expr: TRegEx;
begin
expr := TRegEx.Create('
(\w+)\s(\w+)
'
);
WriteLn(expr.Replace('
abc
def
'
,'
\2
\1
'
));
ReadLn;
end
.
En la cadena
'\2 \1', la barra invertida seguida por un dígito representa el elemento
capturado que deberá insertarse en este lugar en la cadena de reemplazo. Así,
'\1' significa "primer elemento capturado".
Práctico,
¿no? Pero eso no es todo. La cadena de reemplazo puede ser incluso el resultado de una función que recibe como argumentos los elementos
capturados.
VI - C. Reemplazo por el resultado de una función
Supongamos
que queremos validar una cadena que representa una determinada posición de una
partida de ajedrez. La cadena debe ajustarse a la notación estándar, es
decir, notación FEN (Forsyth-Edwards Notation). Aquí la
cadena FEN correspondiente a la posición inicial de una pieza de ajedrez:
'
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
w
KQkq
-
0
1
'
Como se ve,
la ocupación de las líneas del tablero está representada por unos grupos de
caracteres que contienen letras (las piezas)
y unos números (el número de casillas vacías consecutivas). Debemos asegurarnos de que el número total de
casillas por cada fila del tablero sea igual a ocho, y para ello que
decidimos reemplazar los caracteres numéricos por caracteres repetidos tantas veces como cajas vacías hayan.
type
TFENValidator =class
function
ReplaceWith(const
aMatch: TMatch):string
;function
ExpandRow(const
aRow:string
):string
;function
IsFEN(const
aStr:string
):boolean
;end
;function
TFENValidator.ReplaceWith(const
aMatch: TMatch):string
;
const
EMPTY_SQUARE_SYMBOL ='
-
'
;
begin
result := StringOfChar(EMPTY_SQUARE_SYMBOL, StrToInt(aMatch.Groups.Item[1
].Value));
end
;function
TFENValidator.ExpandRow(const
aRow:string
):string
;
beginwith
TRegEx.Create('
([1-8])
'
)do
result := Replace(aRow, ReplaceWith);
end
;function
TFENValidator.IsFEN(const
aStr:string
):boolean
;
var
a,
b: TStringList;
expr: TRegEx;
i:integer
;
s:string
;
begin
a := TStringList.Create;
b := TStringList.Create;
b.Delimiter :='
/
'
;
b.StrictDelimiter :=TRUE
;
a.DelimitedText := aStr;
result := (a.Count =6
);if
not
resultthen
Exit;
b.DelimitedText := a[0
];
result := resultand
(b.Count =8
);if
not
resultthen
Exit;
expr.Create('
^[1-8BKNPQRbknpqr]+$
'
);for
i :=0
to
b.Count -1
do
begin
s := ExpandRow(b[i]);{
WriteLn(s);
}
result := resultand
expr.IsMatch(b[i])and
(Length(s) =8
);end
;
result := resultand
TRegEx.IsMatch(a[1
],'
^(w|b)$
'
);
result := resultand
TRegEx.IsMatch(a[2
],'
^([KQkq]+|\-)$
'
);
result := resultand
TRegEx.IsMatch(a[3
],'
^([a-h][36]|\-)$
'
);
expr.Create('
^\d+$
'
);
result := resultand
expr.IsMatch(a[4
])and
(StrToInt(a[4
]) >=0
);
result := resultand
expr.IsMatch(a[5
])and
(StrToInt(a[5
]) >=1
);
end
;
He aquí otro ejemplo de sustitución por función. Es un programa que busca en una cadena de variables para reemplazar su valor. Las variables siguieron la sintaxis utilizada para las variables de entorno como %date% o
%username%. La función que devuelve la cadena a sustituir obtendrá sus resultados de un diccionario.
program
Replace3b;
{$
APPTYPE
CONSOLE
}
uses
System.SysUtils,
System.RegularExpressions;
System.Classes,type
TExpander =class
fDictionary: TStrings;constructor
Create(aDictionary: TStrings);function
ReplaceWith(const
aMatch: TMatch):string
;function
Expand(const
s:string
):string
;end
;constructor
TExpander.Create(aDictionary: TStrings);
begin
fDictionary := aDictionary;
end
;function
TExpander.ReplaceWith(const
aMatch: TMatch):string
;
begin
result := fDictionary.Values[aMatch.Groups.Item[1
].Value];
end
;function
TExpander.Expand(const
s:string
):string
;
var
expr: TRegEx;
begin
expr.Create('
%(.+)%
'
);
result := expr.Replace(s, ReplaceWith);
end
;var
dictionary: TStrings;
expander: TExpander;begin
dictionary := TStringList.Create;
expander := TExpander.Create(dictionary);
dictionary.Values['
REPERTOIRE_PROJET
'
] := ExtractFileDir(ParamStr(0
));
WriteLn(expander.Expand('
%REPERTOIRE_PROJET%
'
#
13
#
10
'
%REPERTOIRE_PROJET%
'
));
dictionary.Free;
expander.Free;end
.
ReadLn;
IX. conclusión
La Unidad RegularExpressions se
basa en la unidad RegularExpressionsCore que, excepto el
nombre, es igual a la unidad PerlRegEx de Jan Goyvaerts.
Seleccione
uses
{$IF CompilerVersion >= 22.0}
RegularExpressionsCore;
{$ELSE}
PerlRegEx; (*
http://www.regular-expressions.info/download/TPerlRegEx.zip *)
{$IFEND}
La unidad PerlRegEx se
basa en la biblioteca PCRE (Expresiones regulares compatibles con Perl)
por Philip Hazel.
Los ejemplos
han sido probados con Delphi XE2
Fuente: rchastain.developpez.com
No hay comentarios:
Publicar un comentario