Expresiones regulares con la unit RegularExpressions

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.Image non disponible

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 '26/09/2015'.


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.Success then
for group in match.Groups do
WriteLn(Format('TGroup.Index=%d TGroup.Value="%s"', [group.Index, group.Value]));
ReadLn;
end.


Image non disponible

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.Success then
match := regEx.Match(SUBJECT);
begin
group := match.Groups['year'];
WriteLn(group.Value);
end;
ReadLn;
end.



Image non disponible
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  Match, Success y NextMatch.

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.Success do
begin
WriteLn(match.Value);
match := match.NextMatch;
end;
ReadLn;
end.

Image non disponible


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.

 Image non disponible

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 s in a do
WriteLn(s);
a := TRegEx.Split('a b,c-d', '[ ,-]');
for i := 0 to High(a) do
WriteLn(a[i]);
ReadLn;
end.

Image non disponible

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;

Image non disponible

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.


Image non disponible


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;
begin
with 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 result then
Exit;
b.DelimitedText := a[0];
result := result and (b.Count = 8);
if not result then
Exit;
expr.Create('^[1-8BKNPQRbknpqr]+$');
for i := 0 to b.Count - 1 do
begin
s := ExpandRow(b[i]);
{WriteLn(s);}
result := result and expr.IsMatch(b[i]) and (Length(s) = 8);
end;
result := result and TRegEx.IsMatch(a[1], '^(w|b)$');
result := result and TRegEx.IsMatch(a[2], '^([KQkq]+|\-)$');
result := result and TRegEx.IsMatch(a[3], '^([a-h][36]|\-)$');
expr.Create('^\d+$');
result := result and expr.IsMatch(a[4]) and (StrToInt(a[4]) >= 0);
result := result and 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%%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;

Image non disponible
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

No hay comentarios:

Publicar un comentario