Usando expresiones regulares, ¿cómo hacer coincidir cadenas de manera eficiente entre comillas dobles con comillas dobles incrustadas?

Permítanos tener un texto en el cual queremos hacer coincidir todas las cadenas entre comillas dobles; pero dentro de estas comillas dobles, puede haber comillas dobles entre comillas. Ejemplo:

"He said \"Hello\" to me for the first time" 

Usando expresiones regulares, ¿cómo se combina esto de manera eficiente?

Una solución muy eficiente para hacer coincidir tales entradas es usar el patrón normal* (special normal*)* ; este nombre se cita del excelente libro de Jeffrey Friedl, Mastering Regular Expressions .

Es un patrón útil en general para unir entradas que constan de entradas regulares (la parte normal) con separadores en medio (la parte especial).

Tenga en cuenta que, como todas las expresiones regex, debería usarse cuando no hay mejor opción; mientras que uno podría usar este patrón para analizar datos CSV, por ejemplo, si usa Java, en su lugar es mejor usar OpenCSV.

También tenga en cuenta que si bien los cuantificadores en el nombre del patrón son estrellas (es decir, cero o más), puede variarlos para adaptarlos a sus necesidades.

Cadenas con comillas dobles incrustadas

Tomemos el ejemplo anterior otra vez; y considere que esta muestra de texto puede estar en cualquier parte de su entrada:

 "He said \"Hello\" to me for the first time" 

No importa cuánto lo intentes, ninguna cantidad de magia “dot plus greedy / lazy quantifiers” te ayudará a resolverlo. En su lugar, clasifique la entrada entre comillas como normal y especial:

  • normal es todo menos una barra diagonal inversa o una comilla doble: [^\\"] ;
  • especial es la secuencia de una barra invertida seguida de una comilla doble: \\" .

Sustituyendo esto en el patrón normal* (special normal*)* , se obtiene la siguiente expresión regular:

 [^\\"]*(\\"[^\\"]*)* 

Agregar las comillas dobles para que coincida con el texto completo da la expresión regular final:

 "[^\\"]*(\\"[^\\"]*)*" 

Notará que esto también coincidirá con cadenas vacías entre comillas.

Palabras con separadores de guiones

Aquí tendremos que usar una variante en los cuantificadores, ya que:

  • no queremos palabras vacías,
  • no queremos palabras que comiencen con un guion,
  • cuando aparece un guion, debe tener al menos una letra antes de otro guion, si corresponde.

Para simplificar, también supondremos que solo se permiten letras ASCII en minúsculas.

Muestra de entrada:

 the-word-to-match 

Vamos a descomponer de nuevo en normal y especial:

  • normal: una letra ASCII minúscula: [az] ;
  • especial: el tablero: -

La forma canónica del patrón sería:

 [az]*(-[az]*)* 

Pero como dijimos:

  • no queremos palabras que comiencen con una raya: la primera * debe convertirse en + ;
  • cuando se encuentra un guión, debe haber al menos una letra después de él: el segundo * debe convertirse en + .

Terminamos con:

 [az]+(-[az]+)* 

Agregar anclas de palabras para obtener el resultado final:

 \b[az]+(-[az]+)*\b 

Otras variaciones del operador

Los ejemplos anteriores se limitan a reemplazar * con + , pero por supuesto puede tener tantas variaciones como desee. Un ejemplo ultra clásico sería una dirección IP:

  • normal es hasta tres dígitos ( \d{1,3} ),
  • especial es el punto: ( \. ),
  • la primera normal aparece solo una vez, por lo tanto no hay cuantificador,
  • la normal dentro de (special normal*) también aparece solo una vez, por lo tanto no hay cuantificador,
  • finalmente la parte (special normal*) aparece exactamente tres veces, por lo tanto {3} .

Que da la expresión (decorada con anclas de palabras):

 \b\d{1,3}(\.\d{1,3}){3}\b 

Conclusión

La flexibilidad de este patrón lo convierte en una de las herramientas más útiles en su caja de herramientas de expresiones regulares. Si bien existen muchos problemas para los que no se deben usar expresiones regulares, si existen bibliotecas, en algunas situaciones, debe usar expresiones regulares. ¡Y se convertirá en uno de tus mejores amigos una vez que hayas practicado un poco!

Consejos

  • Es más que probable que no necesite (o desee) capturar la parte repetida (la parte (special normal*) ); por lo tanto, se recomienda que use un grupo que no captura. Por ejemplo, use "[^\\"]*(?:\\"[^\\"]*)*" para las cadenas entre comillas. De hecho, si lo hubiera deseado, la captura casi nunca llevaría a los resultados deseados en este caso, porque repetir un grupo de captura solo le dará la última captura (todas las repeticiones anteriores serán sobrescritas), a menos que esté usando este patrón en .NET. (gracias @ohaal)