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.
programIsMatch1;{$APPTYPECONSOLE}
uses
RegularExpressions;
begin
WriteLn(TRegEx.IsMatch('bonjour','\w'));//uncaractèrealphanumérique
WriteLn(TRegEx.IsMatch('bonjour','\w+'));//unouplusieurscaractèresalphanumériques
WriteLn(TRegEx.IsMatch('bonjour','\w*'));//zéroouplus
WriteLn(TRegEx.IsMatch('bonjour','\w{7}'));//sept
WriteLn(TRegEx.IsMatch('bonjour','[a-z]{7}'));//septminuscules
WriteLn(notTRegEx.IsMatch('bonjour','\d'));//unchiffre
WriteLn(notTRegEx.IsMatch('bonjour','\s'));//unespaceausenslarge(équivalentà'[0-9]')
WriteLn(TRegEx.IsMatch('bonjour','\D'));//uncaractèrequin'estpasunchiffre
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(notTRegEx.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
programGroup1;{$APPTYPECONSOLE}
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);ifmatch.Successthenforgroupinmatch.Groupsdo
WriteLn(Format('TGroup.Index=%dTGroup.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
programGroup2;{$APPTYPECONSOLE}
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, []);ifmatch.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.
programMatch1a;{$APPTYPECONSOLE}
uses
SysUtils, RegularExpressions;
var
match: TMatch;
begin
match := TRegEx.Match('10+100.5.5','\s*[-+]?[0-9]*\.?[0-9]+\s*');whilematch.Successdobegin
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.
programMatchCollection1;{$APPTYPECONSOLE}
uses
SysUtils,
RegularExpressions;var
expr: TRegEx;
collection: TMatchCollection;
i:Integer;begin
expr.Create('\w');
collection := expr.Matches('abc');fori :=0tocollection.Count -1dowithcollection[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;{$APPTYPECONSOLE}uses
SysUtils, RegularExpressions;var
a: TArray<string>;
s:string;
i:integer;begin
a := TRegEx.Split(GetEnvironmentVariable('PATH'),';');forsinado
WriteLn(s);
a := TRegEx.Split('ab,c-d','[,-]');fori :=0toHigh(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
programReplace1;{$APPTYPECONSOLE}
uses
RegularExpressions;
begin
WriteLn(TRegEx.Replace('WRITELNwriteln','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.
programReplace2;{$APPTYPECONSOLE}
uses
RegularExpressions;
var
expr: TRegEx;
begin
expr := TRegEx.Create('(\w+)\s(\w+)');
WriteLn(expr.Replace('abcdef','\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 =classfunctionReplaceWith(constaMatch: TMatch):string;functionExpandRow(constaRow:string):string;functionIsFEN(constaStr:string):boolean;end;functionTFENValidator.ReplaceWith(constaMatch: TMatch):string;
const
EMPTY_SQUARE_SYMBOL ='-';
begin
result := StringOfChar(EMPTY_SQUARE_SYMBOL, StrToInt(aMatch.Groups.Item[1].Value));end;functionTFENValidator.ExpandRow(constaRow:string):string;
beginwithTRegEx.Create('([1-8])')do
result := Replace(aRow, ReplaceWith);end;functionTFENValidator.IsFEN(constaStr: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);ifnotresultthen
Exit;
b.DelimitedText := a[0];
result := resultand(b.Count =8);ifnotresultthen
Exit;
expr.Create('^[1-8BKNPQRbknpqr]+$');fori :=0tob.Count -1dobegin
s := ExpandRow(b[i]);{WriteLn(s);}
result := resultandexpr.IsMatch(b[i])and(Length(s) =8);end;
result := resultandTRegEx.IsMatch(a[1],'^(w|b)$');
result := resultandTRegEx.IsMatch(a[2],'^([KQkq]+|\-)$');
result := resultandTRegEx.IsMatch(a[3],'^([a-h][36]|\-)$');
expr.Create('^\d+$');
result := resultandexpr.IsMatch(a[4])and(StrToInt(a[4]) >=0);
result := resultandexpr.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.programReplace3b;{$APPTYPECONSOLE}
uses
System.SysUtils,
System.RegularExpressions;
System.Classes,type
TExpander =class
fDictionary: TStrings;constructorCreate(aDictionary: TStrings);functionReplaceWith(constaMatch: TMatch):string;functionExpand(consts:string):string;end;constructorTExpander.Create(aDictionary: TStrings);
begin
fDictionary := aDictionary;end;functionTExpander.ReplaceWith(constaMatch: TMatch):string;
begin
result := fDictionary.Values[aMatch.Groups.Item[1].Value];end;functionTExpander.Expand(consts: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