¿Expresiones regulares legibles sin perder su poder?

77

Muchos programadores conocen la alegría de generar una expresión regular rápida, en estos días a menudo con la ayuda de algún servicio web, o más tradicionalmente en un mensaje interactivo, o tal vez escribiendo un pequeño script que tiene la expresión regular en desarrollo, y una colección de casos de prueba. . En cualquier caso, el proceso es iterativo y bastante rápido: sigue pirateando la cadena de aspecto críptico hasta que coincida y capture lo que quieres y rechazará lo que no quieres.

Para un caso simple, el resultado podría ser algo como esto, como una expresión regular de Java:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

Muchos programadores también conocen el dolor de tener que editar una expresión regular, o simplemente codificar alrededor de una expresión regular en una base de código heredada. Con un poco de edición para dividirlo, la expresión regular anterior todavía es muy fácil de comprender para cualquier persona razonablemente familiarizada con expresiones regulares, y un veterano de expresiones regulares debería ver de inmediato lo que hace (responda al final de la publicación, en caso de que alguien quiera el ejercicio) de resolverlo ellos mismos).

Sin embargo, las cosas no necesitan volverse mucho más complejas para que una expresión regular se convierta realmente en algo de solo escritura, e incluso con documentación diligente (lo que, por supuesto , todo el mundo hace para todas las expresiones regulares complejas que escriben ...), modificar las expresiones regulares se convierte en una tarea desalentadora. También puede ser una tarea muy peligrosa, si regexp no se prueba cuidadosamente en unidades (pero, por supuesto, todos tienen pruebas unitarias exhaustivas para todas sus expresiones regulares complejas, tanto positivas como negativas ...).

Entonces, para resumir, ¿hay una solución / alternativa de escritura-lectura para expresiones regulares sin perder su poder? ¿Cómo se vería la expresión regular anterior con un enfoque alternativo? Cualquier idioma está bien, aunque una solución en varios idiomas sería lo mejor, en la medida en que las expresiones regulares son en varios idiomas.


Y luego, lo que hace la expresión regular anterior es esto: analizar una cadena de números en formato 1:2:3.4, capturando cada número, donde los espacios están permitidos y solo 3se requieren.

Hyde
fuente
2
Cosa relacionada en SO: stackoverflow.com/a/143636/674039
wim
24
Leer / editar expresiones regulares es realmente trivial si sabes lo que se supone que deben capturar. Es posible que haya oído hablar de esta característica raramente utilizada en la mayoría de los idiomas llamados "comentarios". Si no coloca uno encima de una expresión regular compleja que explica lo que hace, pagará el precio más tarde. Además, revisión de código.
TC1
2
Dos opciones para limpiar esto sin romperlo en pedazos más pequeños. Su presencia o ausencia varía de un idioma a otro. (1) expresiones regulares de línea extendidas, donde se ignora el espacio en blanco en la expresión regular (a menos que se escape) y se agrega un formulario de comentario de una sola línea, por lo que puede dividirlo en fragmentos lógicos con sangría, espacio entre líneas y comentarios. (2) grupos de captura con nombre, donde puede asignar un nombre a cada paréntesis, que agrega un poco de documentación propia y completa automáticamente un hash de coincidencias, mucho mejor que una matriz de coincidencias indexada numéricamente o variables $ N.
Ben Lee
3
Parte del problema es el lenguaje regex en sí y las malas elecciones históricas en su diseño que se arrastran como equipaje. En un lenguaje sensato, los paréntesis de agrupación son puramente un dispositivo sintáctico para dar forma al árbol de análisis. Pero en las implementaciones de expresiones regulares que se remontan a Unix tienen semántica: vincular registros a coincidencias de subexpresión. Entonces, ¡necesitas algunos corchetes más feos y complicados solo para lograr una agrupación pura!
Kaz
2
No es realmente una respuesta práctica, pero puede ser útil mencionar que el poder de la expresión regular es exactamente como el de un autómata finito. Es decir, las expresiones regulares pueden validar / analizar la misma clase de cadenas validadas y analizadas por autómatas finitos. Por lo tanto, una representación legible por humanos de una expresión regular probablemente debería ser capaz de construir rápidamente un gráfico, y creo que la mayoría de los lenguajes basados ​​en texto son realmente malos; Es por eso que utilizamos herramientas visuales para tales cosas. Echa un vistazo a hackingoff.com/compilers/regular-expression-to-nfa-dfa para inspirarte.
damix911

Respuestas:

80

Varias personas han mencionado componer desde partes más pequeñas, pero nadie ha proporcionado un ejemplo todavía, así que aquí está el mío:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

No es el más legible, pero siento que es más claro que el original.

Además, C # tiene el @operador que puede anteponerse a una cadena para indicar que debe tomarse literalmente (sin caracteres de escape), por numberlo que sería@"([\d]+)";

Bobson
fuente
Justo ahora noté cómo ambos [\\d]+y [0-9]+deberían ser justos \\d+(bueno, algunos pueden encontrar [0-9]+más legibles). No voy a editar la pregunta, pero es posible que desee corregir esta respuesta.
hyde
@hyde - Buena captura. Técnicamente no son lo mismo: \dcoincidirán con cualquier cosa que se considere un número, incluso en otros sistemas de numeración (chino, árabe, etc.), mientras [0-9]que solo coincidirán con los dígitos estándar. Sin \\dembargo, sí estandaricé y lo incluí en el optionalDecimalpatrón.
Bobson
42

La clave para documentar la expresión regular es documentarla. Con demasiada frecuencia, las personas arrojan lo que parece ser ruido de línea y lo dejan así.

Dentro de Perl, el /xoperador al final de la expresión regular suprime los espacios en blanco, lo que permite documentar la expresión regular.

La expresión regular anterior se convertiría en:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

Sí, consume un poco de espacio en blanco vertical, aunque uno podría acortarlo sin sacrificar demasiada legibilidad.

Y luego, lo que hace la expresión regular anterior es esto: analizar una cadena de números en formato 1: 2: 3.4, capturando cada número, donde se permiten espacios y solo se requiere 3.

Mirando esta expresión regular, uno puede ver cómo funciona (y no funciona). En este caso, esta expresión regular coincidirá con la cadena 1.

Se pueden adoptar enfoques similares en otro idioma. La opción de python re.VERBOSE funciona allí.

Perl6 (el ejemplo anterior era para perl5) lleva esto más allá con el concepto de reglas que conduce a estructuras aún más poderosas que el PCRE (proporciona acceso a otras gramáticas (sin contexto y sensibles al contexto) que solo las regulares regulares y extendidas).

En Java (de donde se deriva este ejemplo), se puede usar la concatenación de cadenas para formar la expresión regular.

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

Es cierto que esto crea muchos más "en la cadena, lo que posiblemente genere cierta confusión allí, puede leerse más fácilmente (especialmente con el resaltado de sintaxis en la mayoría de los IDE) y documentarse.

La clave es reconocer el poder y la naturaleza de "escribir una vez" en la que a menudo caen las expresiones regulares. Escribir el código para evitar esto defensivamente para que la expresión regular permanezca clara y comprensible es clave. Formateamos el código Java para mayor claridad: las expresiones regulares no son diferentes cuando el lenguaje le da la opción de hacerlo.


fuente
13
Hay una gran diferencia entre "documentar" y "agregar saltos de línea".
44
@JonofAllTrades Hacer que el código pueda leerse es el primer paso para cualquier cosa. Agregar saltos de línea también permite agregar comentarios para ese subconjunto de RE en la misma línea (algo que es más difícil de hacer en una sola línea larga de texto de expresión regular).
2
@JonofAllTrades, estoy muy en desacuerdo. "Documentar" y "agregar saltos de línea" no son tan diferentes, ya que ambos tienen el mismo propósito: hacer que el código sea más fácil de entender. Y para el código mal formateado, "agregar saltos de línea" cumple ese propósito mucho mejor que agregar documentación.
Ben Lee
2
Agregar saltos de línea es un comienzo, pero es aproximadamente el 10% del trabajo. Otras respuestas dan más detalles, lo cual es útil.
26

El modo "detallado" que ofrecen algunos idiomas y bibliotecas es una de las respuestas a estas inquietudes. En este modo, el espacio en blanco en la cadena regexp se elimina (por lo que debe usar \s) y los comentarios son posibles. Aquí hay un breve ejemplo en Python que admite esto de manera predeterminada:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

En cualquier idioma que no sea así, implementar un traductor del modo detallado al modo "normal" debería ser una tarea simple. Si le preocupa la legibilidad de sus expresiones regulares, probablemente justifique esta inversión de tiempo con bastante facilidad.

Xion
fuente
15

Cada lenguaje que utiliza expresiones regulares le permite componerlas a partir de bloques más simples para facilitar la lectura, y con cualquier cosa más complicada que (o tan complicada como) su ejemplo, definitivamente debería aprovechar esa opción. El problema particular con Java y muchos otros lenguajes es que no tratan las expresiones regulares como ciudadanos de "primera clase", sino que requieren que se escabullen en el lenguaje a través de literales de cadena. Esto significa muchas comillas y barras invertidas que en realidad no son parte de la sintaxis de expresiones regulares y hacen que las cosas sean difíciles de leer, y también significa que no puede ser mucho más legible que eso sin definir efectivamente su propio mini-idioma e intérprete.

La mejor forma prototípica de integrar expresiones regulares era, por supuesto, Perl, con su opción de espacios en blanco y operadores de comillas de expresiones regulares. Perl 6 amplía el concepto de construir expresiones regulares de partes a gramáticas recursivas reales, que es mucho mejor usar, realmente no hay comparación en absoluto. El lenguaje puede haber perdido el barco de la puntualidad, pero su soporte de expresiones regulares fue The Good Stuff (tm).

Kilian Foth
fuente
1
Por "bloques más simples" mencionados al comienzo de la respuesta, ¿quiere decir simplemente concatenación de cadenas o algo más avanzado?
hyde
77
Me refería a definir subexpresiones como literales de cadena más cortos, asignándolos a variables locales con nombres significativos y luego concatenando. Creo que los nombres son más importantes para la legibilidad que solo la mejora del diseño.
Kilian Foth
11

Me gusta usar Expresso: http://www.ultrapico.com/Expresso.htm

Esta aplicación gratuita tiene las siguientes características que encuentro útiles con el tiempo:

  • Simplemente puede copiar y pegar su expresión regular y la aplicación la analizará por usted
  • Una vez que se escribe su expresión regular, puede probarla directamente desde la aplicación (la aplicación le dará la lista de capturas, reemplazos ...)
  • Una vez que lo haya probado, generará el código C # para implementarlo (tenga en cuenta que el código contendrá las explicaciones sobre su expresión regular).

Por ejemplo, con la expresión regular que acaba de enviar, se vería así: Pantalla de muestra con la expresión regular dada inicialmente

Por supuesto, intentarlo vale más que mil palabras para describirlo. Tenga en cuenta también que estoy relacionado de alguna manera con el editor de esta aplicación.

E. Jaep
fuente
44
¿Le importaría explicar esto con más detalle? ¿Cómo y por qué responde a la pregunta que se hace? Las "respuestas de solo enlace" no son del todo bienvenidas en Stack Exchange
mosquito
55
@gnat Lo siento por eso. Estás absolutamente en lo correcto. Espero que mi respuesta editada proporcione más información.
E. Jaep
9

Para algunas cosas, podría ser útil usar una gramática como BNF. Estos pueden ser mucho más fáciles de leer que las expresiones regulares. Una herramienta como GoldParser Builder puede convertir la gramática en un analizador que haga el trabajo pesado por usted.

Las gramáticas BNF, EBNF, etc. pueden ser mucho más fáciles de leer y crear que una expresión regular complicada. GOLD es una herramienta para tales cosas.

El siguiente enlace wiki de c2 tiene una lista de posibles alternativas que se pueden buscar en Google, con algunas discusiones sobre ellas incluidas. Básicamente es un enlace "ver también" para completar la recomendación de mi motor de gramática:

Alternativas a las expresiones regulares

Tomando "alternativa" para significar "facilidad semánticamente equivalente con diferente sintaxis", existen al menos estas alternativas a / con RegularExpressions:

  • Expresiones regulares básicas
  • Expresiones regulares "extendidas"
  • Expresiones regulares compatibles con Perl
  • ... y muchas otras variantes ...
  • Sintaxis RE al estilo SNOBOL (SnobolLanguage, IconLanguage)
  • Sintaxis de SRE (RE's como EssExpressions)
  • diferentes sintaxis FSM
  • Gramáticas de intersección de estado finito (bastante expresivas)
  • ParsingExpressionGrammars, como en OMetaLanguage y LuaLanguage ( http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html )
  • El modo de análisis de RebolLanguage
  • ProbabilityBasedParsing ...
Nick P
fuente
¿Le importaría explicar más sobre lo que hace este enlace y para qué sirve? Las "respuestas de solo enlace" no son bienvenidas en Stack Exchange
mosto
1
Bienvenido a Programadores, Nick P. Por favor, ignore el downvote / r, pero lea la página sobre meta que @gnat enlazó.
Christoffer Lette
@ Christoffer Lette Agradezco su respuesta. Intentaremos tener esto en cuenta en futuras publicaciones. El comentario de @ gnat Paulo Scardine refleja la intención de mis publicaciones. Las gramáticas BNF, EBNF, etc. pueden ser mucho más fáciles de leer y crear que una expresión regular complicada. GOLD es una herramienta para tales cosas. El enlace c2 tiene una lista de posibles alternativas que se pueden buscar en Google, con algunas discusiones sobre ellas incluidas. Básicamente era un enlace "ver también" para completar mi recomendación de motor de gramática.
Nick P
6

Esta es una vieja pregunta y no vi ninguna mención de Expresiones verbales, así que pensé agregar esa información aquí también para futuros buscadores. Las expresiones verbales se diseñaron específicamente para hacer comprensible la expresión regular humana, sin necesidad de aprender el significado del símbolo de expresión regular. Ver el siguiente ejemplo. Creo que esto hace mejor lo que estás pidiendo.

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

Este ejemplo es para Javascript, puede encontrar esta biblioteca ahora para muchos de los lenguajes de programación.

Parivar Saraff
fuente
2
¡Esto es asombroso!
Jeremy Thompson
3

La forma más sencilla sería seguir utilizando expresiones regulares, pero construya su expresión componiendo expresiones más simples con nombres descriptivos, por ejemplo, http://www.martinfowler.com/bliki/ComposedRegex.html (y sí, esto es de string concat)

sin embargo, como alternativa, también puede utilizar una biblioteca de combinación de analizadores, por ejemplo, http://jparsec.codehaus.org/, que le proporcionará un analizador decente recursivo completo. Una vez más, el poder real aquí proviene de la composición (esta vez composición funcional).

jk.
fuente
3

Pensé que valdría la pena mencionar de logstash grok expresiones. Grok se basa en la idea de componer expresiones de análisis largas a partir de las más cortas. Permite pruebas convenientes de estos bloques de construcción y viene preempaquetado con más de 100 patrones de uso común . Aparte de estos patrones, permite el uso de todas las sintaxis de expresiones regulares.

El patrón anterior expresado en grok es (lo probé en la aplicación del depurador pero podría haber cometido un error):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

Las partes y espacios opcionales hacen que parezca un poco más feo de lo habitual, pero tanto aquí como en otros casos, usar grok puede hacer que la vida sea mucho más agradable.

yoniLavi
fuente
2

En F # tienes el módulo FsVerbalExpressions . Le permite componer expresiones regulares a partir de expresiones verbales, también tiene algunas expresiones regulares precompiladas (como URL).

Uno de los ejemplos de esta sintaxis es el siguiente:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

Si no está familiarizado con la sintaxis de F #, groupName es la cadena "GroupNumber".

Luego crean una expresión verbal (VerbEx) que construyen como "COD (? <GroupNumber> [0-9] {3}) END". Que luego prueban en la cadena "COD123END", donde obtienen el grupo de captura con nombre "GroupNumber". Esto da como resultado 123.

Sinceramente, la expresión regular normal es mucho más fácil de comprender.

CodeMonkey
fuente
-2

Primero, comprenda que el código que simplemente funciona es un código incorrecto. Un buen código también debe informar con precisión cualquier error encontrado.

Por ejemplo, si está escribiendo una función para transferir efectivo de la cuenta de un usuario a la cuenta de otro usuario; no solo devolvería un booleano "trabajado o fallado" porque eso no le da a la persona que llama ninguna idea de lo que salió mal y no le permite informar al usuario correctamente. En cambio, es posible que tenga un conjunto de códigos de error (o un conjunto de excepciones): no se pudo encontrar la cuenta de destino, fondos insuficientes en la cuenta de origen, permiso denegado, no se puede conectar a la base de datos, demasiada carga (vuelva a intentarlo más tarde), etc. .

Ahora piense en su ejemplo de "analizar una cadena de números en formato 1: 2: 3.4". Todo lo que hace la expresión regular es informar un "pasar / fallar" que no permite que se presenten comentarios adecuados al usuario (ya sea que estos comentarios sean un mensaje de error en un registro o una GUI interactiva donde los errores se muestran en rojo como tipos de usuario, o cualquier otra cosa). ¿Qué tipos de errores no describe correctamente? Carácter incorrecto en el primer número, primer número demasiado grande, faltan dos puntos después del primer número, etc.

Para convertir "código incorrecto que simplemente funciona" en "código correcto que proporciona errores descriptivos adecuados", debe dividir la expresión regular en muchas expresiones regulares más pequeñas (por lo general, expresiones regulares que son tan pequeñas que es más fácil hacerlo sin expresiones regulares en primer lugar )

Hacer que el código sea legible / mantenible es solo una consecuencia accidental de hacer que el código sea bueno.

Brendan
fuente
66
Probablemente no sea una buena suposición. El mío es porque A) Esto no responde a la pregunta ( ¿Cómo hacer que sea legible?), B) La coincidencia de expresiones regulares es pasa / falla, y si la desglosa hasta el punto en que puede decir exactamente por qué falló, usted perder mucha potencia y velocidad, y aumentar la complejidad, C) No hay indicios de la pregunta de que incluso exista la posibilidad de que falle el partido, es simplemente una cuestión de hacer que el Regex sea legible. Cuando tiene control de los datos que ingresan y / o los valida de antemano, puede asumir que son válidos.
Bobson
A) Romperlo en pedazos más pequeños lo hace más legible (como consecuencia de hacerlo bueno). C) Cuando las cadenas desconocidas / no validadas ingresan a una pieza de software que un desarrollador sensato analizaría (con informe de errores) en ese punto y convertiría los datos en un formulario que no necesita reparaciones; no es necesario regex después de eso. B) es una tontería que solo se aplica al código incorrecto (consulte los puntos A y C).
Brendan
Pasando de su C: ¿Qué pasa si esta es su lógica de validación? El código del OP podría ser exactamente lo que está sugiriendo: validar la entrada, informar si no es válida y convertirla a una forma utilizable (a través de las capturas). Todo lo que tenemos es la expresión misma. ¿Cómo sugeriría analizarlo que no sea con una expresión regular? Si agrega un código de muestra que logrará el mismo resultado, eliminaré mi voto negativo.
Bobson
Si esto es "C: Validación (con informe de error)", entonces es un código incorrecto porque el informe de error es incorrecto. Si falla ¿Fue porque la cadena era NULL, o porque el primer número tenía demasiados dígitos, o porque el primer separador no lo era :? Imagine un compilador que solo tenía un mensaje de error ("ERROR") que era demasiado estúpido para decirle al usuario cuál es el problema. Ahora imagine miles de sitios web que son tan estúpidos y muestran (por ejemplo) "dirección de correo electrónico incorrecta" y nada más.
Brendan
Además, imagine que un operador de la mesa de ayuda medio capacitado recibe un informe de error de un usuario completamente inexperto que dice: El software dejó de funcionar: la última línea en el registro del software es "ERROR: Error al extraer el número de versión menor de la cadena de versión '1: 2-3.4 '(dos puntos esperados después del segundo número) "
Brendan