regex - que - expresiones regulares ejemplos
¿Cómo escribo expresiones regulares más mantenibles? (13)
Empecé a sentir que el uso de expresiones regulares disminuye la capacidad de mantenimiento del código. Hay algo malo en la terseness y el poder de las expresiones regulares. Perl compone esto con efectos secundarios como operadores predeterminados.
TENGO el hábito de documentar expresiones regulares con al menos una oración que da la intención básica y al menos un ejemplo de lo que coincidiría.
Debido a que las expresiones regulares se construyen, siento que es absolutamente necesario comentar los componentes más grandes de cada elemento en la expresión. A pesar de esto, incluso mis propias expresiones regulares me hacen rascarme la cabeza como si estuviera leyendo Klingon.
¿Intencionalmente te mudas con tus expresiones regulares? ¿Descompones los más cortos y poderosos en pasos más simples? Me he rendido en anidar expresiones regulares. ¿Hay construcciones de expresiones regulares que evites debido a problemas de mainabilidad?
No dejes que este ejemplo enturbie la pregunta.
Si lo siguiente de Michael Ash tuviera algún tipo de error, ¿tendrías alguna posibilidad de hacer algo más que tirarlo por completo?
^(?:(?:(?:0?[13578]|1[02])(//|-|/.)31)/1|(?:(?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2))(?:(?:1[6-9]|[2-9]/d)?/d{2})$|^(?:0?2(//|-|/.)29/3(?:(?:(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(//|-|/.)(?:0?[1-9]|1/d|2[0-8])/4(?:(?:1[6-9]|[2-9]/d)?/d{2})$
Por solicitud, se puede encontrar el propósito exacto usando el enlace del Sr. Ash arriba.
Partidos 01.1.02 | 30-30-2001 | 2/29/2000
No partidos 02/29/01 | 13/01/2002 | 11/00/02
Algunas personas, cuando se enfrentan a un problema, piensan "Lo sé, usaré expresiones regulares". Ahora ellos tienen dos problemas. - Jamie Zawinski en comp.lang.emacs.
Mantenga las expresiones regulares tan simples como sea posible ( KISS ). En su ejemplo de fecha, es probable que use una expresión regular para cada tipo de fecha.
O incluso mejor, lo reemplazó con una biblioteca (es decir, una biblioteca de análisis de fecha).
También tomaría medidas para garantizar que la fuente de entrada tuviera algunas restricciones (es decir, solo un tipo de cadenas de fecha, idealmente ISO-8601 ).
También,
- Una cosa a la vez (con la posible excepción de extraer valores)
- Las construcciones avanzadas están bien si se usan correctamente (como simplemente para la expresión y por lo tanto para reducir el mantenimiento)
EDITAR:
"Las construcciones avanzadas conducen a problemas de mantenimiento"
Mi punto original fue que si se usa correctamente , debería conducir a expresiones más simples , no más difíciles. Las expresiones más simples deberían reducir el mantenimiento.
He actualizado el texto de arriba para decir tanto.
Señalaría que las expresiones regulares difícilmente califican como construcciones avanzadas en sí mismas. No estar familiarizado con un determinado constructo no lo convierte en un constructo avanzado, simplemente desconocido. Lo que no cambia el hecho de que las expresiones regulares son potentes, compactas y, si se usan adecuadamente, elegantes. Al igual que un escalpelo, está completamente en manos de quien lo maneja.
Algunas personas usan RE para las cosas incorrectas (estoy esperando la primera pregunta SO sobre cómo detectar un programa de C ++ válido usando un solo RE).
Usualmente encuentro que, si no puedo encajar mi RE dentro de 60 caracteres, es mejor que sea una pieza de código ya que casi siempre será más legible.
En cualquier caso, siempre documento, en el código, lo que se supone que el RE debe lograr, con gran detalle. Esto se debe a que sé, por amarga experiencia, lo difícil que es para otra persona (o incluso para mí, seis meses después) tratar de comprender.
No creo que sean malvados, aunque sí creo que algunas personas que los usan son malvadas (sin mirarte, Michael Ash :-) Son una gran herramienta pero, como una motosierra, te cortarás las piernas si no sabes cómo usarlas correctamente.
ACTUALIZACIÓN: En realidad, acabo de seguir el enlace a esa monstruosidad, y es para validar las fechas en formato m / d / a entre los años 1600 y 9999. Ese es un caso clásico en el que el código completo sería más legible y mantenible. .
Simplemente lo divide en tres campos y verifica los valores individuales. Casi lo consideraría un delito digno de terminación si uno de mis secuaces me lo comprara. Ciertamente los enviaría de vuelta para escribirlo correctamente.
Aquí está la misma expresión regular desglosada en pedazos digeribles. Además de ser más legibles, algunas de las subexpresiones pueden ser útiles por sí mismas. También es significativamente más fácil cambiar los separadores permitidos.
#!/usr/local/ActivePerl-5.10/bin/perl
use 5.010; #only 5.10 and above
use strict;
use warnings;
my $sep = qr{ [/.-] }x; #allowed separators
my $any_century = qr/ 1[6-9] | [2-9][0-9] /x; #match the century
my $any_decade = qr/ [0-9]{2} /x; #match any decade or 2 digit year
my $any_year = qr/ $any_century? $any_decade /x; #match a 2 or 4 digit year
#match the 1st through 28th for any month of any year
my $start_of_month = qr/
(?: #match
0?[1-9] | #Jan - Sep or
1[0-2] #Oct - Dec
)
($sep) #the separator
(?:
0?[1-9] | # 1st - 9th or
1[0-9] | #10th - 19th or
2[0-8] #20th - 28th
)
/g{-1} #and the separator again
/x;
#match 28th - 31st for any month but Feb for any year
my $end_of_month = qr/
(?:
(?: 0?[13578] | 1[02] ) #match Jan, Mar, May, Jul, Aug, Oct, Dec
($sep) #the separator
31 #the 31st
/g{-1} #and the separator again
| #or
(?: 0?[13-9] | 1[0-2] ) #match all months but Feb
($sep) #the separator
(?:29|30) #the 29th or the 30th
/g{-1} #and the separator again
)
/x;
#match any non-leap year date and the first part of Feb in leap years
my $non_leap_year = qr/ (?: $start_of_month | $end_of_month ) $any_year/x;
#match 29th of Feb in leap years
#BUG: 00 is treated as a non leap year
#even though 2000, 2400, etc are leap years
my $feb_in_leap = qr/
0?2 #match Feb
($sep) #the separtor
29 #the 29th
/g{-1} #the separator again
(?:
$any_century? #any century
(?: #and decades divisible by 4 but not 100
0[48] |
[2468][048] |
[13579][26]
)
|
(?: #or match centuries that are divisible by 4
16 |
[2468][048] |
[3579][26]
)
00
)
/x;
my $any_date = qr/$non_leap_year|$feb_in_leap/;
my $only_date = qr/^$any_date$/;
say "test against garbage";
for my $date (qw(022900 foo 1/1/1)) {
say "/t$date ", $date ~~ $only_date ? "matched" : "didn''t match";
}
say '''';
#comprehensive test
my @code = qw/good unmatch month day year leap/;
for my $sep (qw( / - . )) {
say "testing $sep";
my $i = 0;
for my $y ("00" .. "99", 1600 .. 9999) {
say "/t", int $i/8500*100, "% done" if $i++ and not $i % 850;
for my $m ("00" .. "09", 0 .. 13) {
for my $d ("00" .. "09", 1 .. 31) {
my $date = join $sep, $m, $d, $y;
my $re = $date ~~ $only_date || 0;
my $code = not_valid($date);
unless ($re == !$code) {
die "error $date re $re code $code[$code]/n"
}
}
}
}
}
sub not_valid {
state $end = [undef, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
my $date = shift;
my ($m,$d,$y) = $date =~ m{([0-9]+)[-./]([0-9]+)[-./]([0-9]+)};
return 1 unless defined $m; #if $m is set, the rest will be too
#components are in roughly the right ranges
return 2 unless $m >= 1 and $m <= 12;
return 3 unless $d >= 1 and $d <= $end->[$m];
return 4 unless ($y >= 0 and $y <= 99) or ($y >= 1600 and $y <= 9999);
#handle the non leap year case
return 5 if $m == 2 and $d == 29 and not leap_year($y);
return 0;
}
sub leap_year {
my $y = shift;
$y = "19$y" if $y < 1600;
return 1 if 0 == $y % 4 and 0 != $y % 100 or 0 == $y % 400;
return 0;
}
Bueno, todo el propósito en la vida del modificador de PCRE / x es permitirle escribir expresiones regulares de manera más legible, como en este ejemplo trivial:
my $expr = qr/
[a-z] # match a lower-case letter
/d{3,5} # followed by 3-5 digits
/x;
Creo que la respuesta a mantener una expresión regular no es tanto con comentarios o construcciones de expresiones regulares.
Si me encargaran de depurar el ejemplo que dio, me sentaría frente a una herramienta de depuración de expresiones regulares (como Regex Coach ) y repasaría la expresión regular de los datos que debe procesar.
Hace poco publiqué una pregunta sobre comentarios de expresiones regulares con comentarios integrados. Hubo respuestas útiles y particularmente una de @mikej
Vea la publicación de Martin Fowler en ComposedRegex para obtener más ideas sobre cómo mejorar la legibilidad de expresiones regulares. En resumen, aboga por dividir una expresión regular compleja en partes más pequeñas a las que se les pueden dar nombres de variables significativos. p.ej
He aprendido a evitar todo, pero la expresión regular más simple. Prefiero otros modelos como el escaneo de cadenas de Icon o los combinadores de análisis de Haskell. En ambos modelos, puede escribir código definido por el usuario que tenga los mismos privilegios y el mismo estado que las operaciones de cadena integradas. Si estuviera programando en Perl, probablemente armaría algunos combinadores de análisis en Perl. Lo he hecho para otros idiomas.
Una muy buena alternativa es usar las gramáticas de expresión de análisis como Roberto Ierusalimschy ha hecho con su paquete de LPEG , pero a diferencia de los combinadores de analizadores, esto es algo que no se puede hacer en una tarde. Pero si alguien ya ha realizado PEG para su plataforma, es una muy buena alternativa a las expresiones regulares.
He encontrado que un buen método es simplemente dividir el proceso de coincidencia en varias fases. Es probable que no se ejecute tan rápido, pero tiene la ventaja adicional de que también es capaz de decir a un nivel de grano más fino por qué no se produce la coincidencia.
Otra ruta es utilizar el análisis LL o LR. Algunos idiomas no se pueden expresar como expresiones regulares, incluso con las extensiones no fsm de perl.
No espero que las expresiones regulares sean legibles, así que las dejo como están y las reescribo si es necesario.
Por lo general, solo trato de envolver todas mis llamadas de Expresión Regular dentro de su propia función, con un nombre significativo y algunos comentarios básicos. Me gusta pensar en Expresiones regulares como un lenguaje de escritura solamente, legible solo por el que lo escribió (a menos que sea realmente simple). Espero totalmente que alguien probablemente deba reescribir la expresión por completo si tuviera que cambiar su intención y esto probablemente sea para mantener vivo el entrenamiento de Expresión Regular.
Todavía podría trabajar con eso. Acabo de usar el regulador . Una cosa que le permite hacer es guardar la expresión regular junto con los datos de prueba para ello.
Por supuesto, también podría añadir comentarios.
Esto es lo que Expresso produjo. Nunca lo había usado antes, pero ahora, Regulator está sin trabajo:
// using System.Text.RegularExpressions; /// /// Regular expression built for C# on: Thu, Apr 2, 2009, 12:51:56 AM /// Using Expresso Version: 3.0.3276, http://www.ultrapico.com /// /// A description of the regular expression: /// /// Select from 3 alternatives /// ^(?:(?:(?:0?[13578]|1[02])(//|-|/.)31)/1|(?:(?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2))(?:(?:1[6-9]|[2-9]/d)?/d{2})$ /// Beginning of line or string /// Match expression but don''t capture it. [(?:(?:0?[13578]|1[02])(//|-|/.)31)/1|(?:(?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2)] /// Select from 2 alternatives /// (?:(?:0?[13578]|1[02])(//|-|/.)31)/1 /// Match expression but don''t capture it. [(?:0?[13578]|1[02])(//|-|/.)31] /// (?:0?[13578]|1[02])(//|-|/.)31 /// Match expression but don''t capture it. [0?[13578]|1[02]] /// Select from 2 alternatives /// 0?[13578] /// 0, zero or one repetitions /// Any character in this class: [13578] /// 1[02] /// 1 /// Any character in this class: [02] /// [1]: A numbered capture group. [//|-|/.] /// Select from 3 alternatives /// Literal / /// - /// Literal . /// 31 /// Backreference to capture number: 1 /// (?:(?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2) /// Return /// New line /// Match expression but don''t capture it. [(?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2] /// (?:0?[13-9]|1[0-2])(//|-|/.)(?:29|30)/2 /// Match expression but don''t capture it. [0?[13-9]|1[0-2]] /// Select from 2 alternatives /// 0?[13-9] /// 0, zero or one repetitions /// Any character in this class: [13-9] /// 1[0-2] /// 1 /// Any character in this class: [0-2] /// [2]: A numbered capture group. [//|-|/.] /// Select from 3 alternatives /// Literal / /// - /// Literal . /// Match expression but don''t capture it. [29|30] /// Select from 2 alternatives /// 29 /// 29 /// 30 /// 30 /// Backreference to capture number: 2 /// Return /// New line /// Match expression but don''t capture it. [(?:1[6-9]|[2-9]/d)?/d{2}] /// (?:1[6-9]|[2-9]/d)?/d{2} /// Match expression but don''t capture it. [1[6-9]|[2-9]/d], zero or one repetitions /// Select from 2 alternatives /// 1[6-9] /// 1 /// Any character in this class: [6-9] /// [2-9]/d /// Any character in this class: [2-9] /// Any digit /// Any digit, exactly 2 repetitions /// End of line or string /// ^(?:0?2(//|-|/.)29/3(?:(?:(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$ /// Beginning of line or string /// Match expression but don''t capture it. [0?2(//|-|/.)29/3(?:(?:(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))] /// 0?2(//|-|/.)29/3(?:(?:(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))) /// 0, zero or one repetitions2 /// [3]: A numbered capture group. [//|-|/.] /// Select from 3 alternatives /// Literal / /// - /// Literal . /// 29 /// Backreference to capture number: 3 /// Match expression but don''t capture it. [(?:(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))] /// Match expression but don''t capture it. [(?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)] /// Select from 2 alternatives /// (?:1[6-9]|[2-9]/d)?(?:0[48]|[2468][048]|[13579][26]) /// Match expression but don''t capture it. [1[6-9]|[2-9]/d], zero or one repetitions /// Select from 2 alternatives /// 1[6-9] /// 1 /// Any character in this class: [6-9] /// [2-9]/d /// Any character in this class: [2-9] /// Any digit /// Match expression but don''t capture it. [0[48]|[2468][048]|[13579][26]] /// Select from 3 alternatives /// 0[48] /// 0 /// Any character in this class: [48] /// [2468][048] /// Any character in this class: [2468] /// Any character in this class: [048] /// [13579][26] /// Any character in this class: [13579] /// Any character in this class: [26] /// (?:(?:16|[2468][048]|[3579][26])00) /// Return /// New line /// Match expression but don''t capture it. [(?:16|[2468][048]|[3579][26])00] /// (?:16|[2468][048]|[3579][26])00 /// Match expression but don''t capture it. [16|[2468][048]|[3579][26]] /// Select from 3 alternatives /// 16 /// 16 /// [2468][048] /// Any character in this class: [2468] /// Any character in this class: [048] /// [3579][26] /// Any character in this class: [3579] /// Any character in this class: [26] /// 00 /// End of line or string /// ^(?:(?:0?[1-9])|(?:1[0-2]))(//|-|/.)(?:0?[1-9]|1/d|2[0-8])/4(?:(?:1[6-9]|[2-9]/d)?/d{2})$ /// Beginning of line or string /// Match expression but don''t capture it. [(?:0?[1-9])|(?:1[0-2])] /// Select from 2 alternatives /// Match expression but don''t capture it. [0?[1-9]] /// 0?[1-9] /// 0, zero or one repetitions /// Any character in this class: [1-9] /// Match expression but don''t capture it. [1[0-2]] /// 1[0-2] /// 1 /// Any character in this class: [0-2] /// Return /// New line /// [4]: A numbered capture group. [//|-|/.] /// Select from 3 alternatives /// Literal / /// - /// Literal . /// Match expression but don''t capture it. [0?[1-9]|1/d|2[0-8]] /// Select from 3 alternatives /// 0?[1-9] /// 0, zero or one repetitions /// Any character in this class: [1-9] /// 1/d /// 1 /// Any digit /// 2[0-8] /// 2 /// Any character in this class: [0-8] /// Backreference to capture number: 4 /// Match expression but don''t capture it. [(?:1[6-9]|[2-9]/d)?/d{2}] /// (?:1[6-9]|[2-9]/d)?/d{2} /// Match expression but don''t capture it. [1[6-9]|[2-9]/d], zero or one repetitions /// Select from 2 alternatives /// 1[6-9] /// 1 /// Any character in this class: [6-9] /// [2-9]/d /// Any character in this class: [2-9] /// Any digit /// Any digit, exactly 2 repetitions /// End of line or string /// /// /// public static Regex regex = new Regex( "^(?:(?:(?:0?[13578]|1[02])(///|-|//.)31)//1|/r/n(?:(?:0?[13-9]"+ "|1[0-2])(///|-|//.)(?:29|30)//2))/r/n(?:(?:1[6-9]|[2-9]//d)?//d"+ "{2})$|^(?:0?2(///|-|//.)29//3(?:(?:(?:1[6-9]|[2-9]//d)?(?:0["+ "48]|[2468][048]|[13579][26])|/r/n(?:(?:16|[2468][048]|[3579][2"+ "6])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))/r/n(///|-|//.)(?:0?[1-9"+ "]|1//d|2[0-8])//4(?:(?:1[6-9]|[2-9]//d)?//d{2})$", RegexOptions.CultureInvariant | RegexOptions.Compiled );
Utilice Expresso que proporciona un desglose jerárquico e inglés de una expresión regular.
O
Este tip de Darren Neimke:
.NET permite la creación de patrones de expresión regular con comentarios incrustados a través de la opción del compilador RegExOptions.IgnorePatternWhitespace y la sintaxis (? # ...) incrustada dentro de cada línea de la cadena del patrón.
Esto permite que se incluyan comentarios tipo código-psuedo en cada línea y tiene el siguiente efecto en la legibilidad:
Dim re As New Regex ( _
"(?<= (?# Start a positive lookBEHIND assertion ) " & _
"(#|@) (?# Find a # or a @ symbol ) " & _
") (?# End the lookBEHIND assertion ) " & _
"(?= (?# Start a positive lookAHEAD assertion ) " & _
" /w+ (?# Find at least one word character ) " & _
") (?# End the lookAHEAD assertion ) " & _
"/w+/b (?# Match multiple word characters leading up to a word boundary)", _
RegexOptions.Multiline Or RegexOptions.IgnoreCase Or RegexOptions.IgnoreWhitespace _
)
Aquí hay otro ejemplo de .NET (requiere las opciones RegexOptions.Multiline
y RegexOptions.IgnorePatternWhitespace
):
static string validEmail = @"/b # Find a word boundary
(?<Username> # Begin group: Username
[a-zA-Z0-9._%+-]+ # Characters allowed in username, 1 or more
) # End group: Username
@ # The e-mail ''@'' character
(?<Domainname> # Begin group: Domain name
[a-zA-Z0-9.-]+ # Domain name(s), we include a dot so that
# mail.somewhere is also possible
.[a-zA-Z]{2,4} # The top level domain can only be 4 characters
# So .info works, .telephone doesn''t.
) # End group: Domain name
/b # Ending on a word boundary
";
Si su RegEx es aplicable a un problema común, otra opción es documentarlo y enviarlo a RegExLib , donde será calificado y comentado. Nada le gana a muchos pares de ojos ...
Otra herramienta de RegEx es el regulador
Wow, eso es feo. Parece que debería funcionar, módulo un error inevitable que trata con 00 como un año de dos dígitos (debería ser un año bisiesto un cuarto de tiempo, pero sin el siglo no tiene forma de saber cómo debería ser). Hay una gran cantidad de redundancia que probablemente se debe tener en cuenta en subexpresiones y yo crearía tres subexpresiones para los tres casos principales (ese es mi próximo proyecto esta noche). También utilicé un carácter diferente para el delimitador para evitar tener que saltar barras diagonales, cambié las alternaciones de un solo carácter en clases de caracteres (lo que felizmente nos permite evitar tener que escapar el período) y cambié /d
a [0-9]
desde el primero coincide con cualquier carácter de dígito (incluido U+1815
DIGITO CIGÜÉNICO MONGOLIAN DIGIT FIVE
: ᠕) en Perl 5.8 y 5.10.
Advertencia, código no probado:
#!/usr/bin/perl
use strict;
use warnings;
my $match_date = qr{
#match 29th - 31st of all months but 2 for the years 1600 - 9999
#with optionally leaving off the first two digits of the year
^
(?:
#match the 31st of 1, 3, 5, 7, 8, 10, and 12
(?: (?: 0? [13578] | 1[02] ) ([/-.]) 31) /1
|
#or match the 29th and 30th of all months but 2
(?: (?: 0? [13-9] | 1[0-2] ) ([/-.]) (?:29|30) /2)
)
(?:
(?: #optionally match the century
1[6-9] | #16 - 19
[2-9][0-9] #20 - 99
)?
[0-9]{2} #match the decade
)
$
|
#or match 29 for 2 for leap years
^
(?:
#FIXME: 00 is treated as a non leap year
#even though 2000, 2400, etc are leap years
0?2 #month 2
([/-.]) #separtor
29 #29th
/3 #separator from before
(?: #leap years
(?:
#match rule 1 (div 4) minus rule 2 (div 100)
(?: #match any century
1[6-9] |
[2-9][0-9]
)?
(?: #match decades divisible by 4 but not 100
0[48] |
[2468][048] |
[13579][26]
)
|
#or match rule 3 (div 400)
(?:
(?: #match centuries that are divisible by 4
16 |
[2468][048] |
[3579][26]
)
00
)
)
)
)
$
|
#or match 1st through 28th for all months between 1600 and 9999
^
(?: (?: 0?[1-9]) | (?:1[0-2] ) ) #all months
([/-.]) #separator
(?:
0?[1-9] | #1st - 9th or
1[0-9] | #10th - 19th or
2[0-8] #20th - 28th
)
/4 #seprator from before
(?:
(?: #optionally match the century
1[6-9] | #16 - 19
[2-9][0-9] #20 - 99
)?
[0-9]{2} #match the decade
)
$
}x;