Uso de expresiones regulares en ObjectScript

Esta es la traducción del artículo original.

Al igual que con Pattern Matching, se pueden utilizar Expresiones Regulares para identificar patrones en textos en ObjectScript, sólo que con una potencia mucho mayor.

En este artículo se proporciona una breve introducción sobre las Expresiones Regulares y lo que puede hacerse con ellas en ObjectScript. La información que se proporciona aquí se basa en varias fuentes, entre las que destaca el libro “Mastering Regular Expressions” (Dominando las expresiones regulares) escrito por Jeffrey Friedl y, por supuesto, la documentación online de la plataforma.

El procesamiento de textos utiliza patrones que algunas veces pueden ser complejos. Cuando utilizamos expresiones regulares, normalmente tenemos varias estructuras: el texto en el que buscamos los patrones, el patrón en sí mismo (la expresión regular) y las coincidencias (las partes del texto que coinciden con el patrón). Para que sea más sencillo distinguir entre estas estructuras, se utilizan las siguientes convenciones a lo largo de este documento:

Los ejemplos en el texto se muestran en monospace y sin comillas adicionales:

Esta es una "cadena de texto" en la cual deseamos encontrar "algo".

A menos que sean evidentes, las expresiones regulares que estén dentro del texto principal se visualizan en un fondo gris, como en el siguiente ejemplo: \".*?\".

Cuando sea necesario, las coincidencias se resaltarán mediante colores diferentes:

Esta es una "cadena de texto" en la cual deseamos encontrar "algo".

Los ejemplos de códigos que sean más grandes se mostrarán en cuadros:

set t="Esta es una ""cadena de texto"" en la cual queremos encontrar ""algo""."
set r="\"".*?\"""
w $locate(t,r,,,tMatch)

 

1. Un poco de historia (y algunas curiosidades)

A principios de los años 40, los neurofisiólogos desarrollaron modelos para representar el sistema nervioso de los humanos. Unos años después, un matemático describió estos modelos mediante expresiones algebraicas que a las que llamó “conjuntos regulares”. La notación que se utilizó en estas expresiones algebraicas se denominó “expresiones regulares”.

En 1965, las expresiones regulares se mencionaron por primera vez en el contexto de la informática. Con qed, un editor que formó parte del sistema operativo UNIX, el uso de las expresiones regulares se extendió. Las siguientes versiones de ese editor proporcionan secuencias de comandos g/expresiones regulares/p (global, expresión regular, imprimir), las cuales realizan búsquedas de coincidencias de las expresiones regulares en todas las líneas del texto y muestran los resultados. Estas secuencias de comandos se convirtieron finalmente en la utilidad grep.

Hoy en día, existen varias implementaciones de expresiones regulares (RegEx) para muchos lenguajes de programación.

2. Regex 101

En esta sección se describen los componentes de las expresiones regulares, su evaluación y algunos de los motores disponibles. Los detalles de cómo utilizarlos se describen en la sección 4.

2.1. Componentes de las expresiones regulares

2.1.1. Metacaracteres

Los siguientes caracteres tienen un significado especial en las expresiones regulares.

.  *  +  ?  (  )  [  ]  \  ^ $  |

Para utilizarlos como valores literales, deben utilizarse utilizando la contrabarra \. También pueden definirse explícitamente secuencias de valores literales utilizando \Q <literal sequence> \E.

2.1.2. Valores literales

El texto normal y caracteres de escape se tratan como valores literales, por ejemplo:

  • abc

abc

  • \f

salto de página

  • \n

salto de línea

  • \r

retorno de carro

  • \r

tabulación

  • \0+tres dígitos (por ejemplo,\0101)

Número octal.
El motor de RegEx que utiliza Caché / IRIS (ICU), es compatible con los números octales hasta el \0377 (255 en el sistema decimal). Al migrar  expresiones regulares desde otro motor
 hay que considerar cómo procesa los octales dicho motor.

  • \x+dos dígitos (por ejemplo, \x41)

Número hexadecimal.
La biblioteca ICU cuenta con varias opciones para procesar los números hexadecimales, consulte los documentos de apoyo sobre ICU (los enlaces están disponibles en la sección 5.8)

 

2.1.3. Anclas

Las anclas (anchors) permiten encontrar ciertos puntos en un texto/cadena, por ejemplo:

  • \A           Inicio de la cadena
  • \Z           Final de la cadena
  • ^            Inicio del texto o línea
  • $            Final de un texto o línea
  • \b           Límite de palabras
  • \B          No en un límite de palabras
  • \<           Inicio de una palabra
  • \>           Final de una palabra

Algunos motores de regex se comportan de forma diferente, por ejemplo, al definir lo que constituye exactamente una palabra y cuáles caracteres se consideran delimitadores de palabras.

2.1.4. Cuantificadores

Con los cuantificadores, puede definir qué tan frecuentemente el elemento anterior puede crear una coincidencia:

  • {x}          exactamente x número de veces
  • {x,y}      mínimo x, máximo y número de veces
  • *             0  ó más, es equivalente a {0,}
  • +            1  ó más, es equivalente a {1,}
  • ?             0 ó 1
     

Codicia

Se dice que los cuantificadores son “codiciosos” (greedy), ya que toman tantos caracteres como sea posible. Supongamos que tenemos la siguiente cadena de texto y queremos encontrar el texto que está entre las comillas:

Este es "un texto" con "cuatro comillas".

Debido a la naturaleza codiciosa de los selectores, la expresión regular \".*\" encontrará demasiado texto:

Este es "un texto" con "cuatro comillas".

En este ejemplo, la expresión regular .* pretende incluir tantos caracteres que se encuentren entre comillas como sea posible. Sin embargo, debido a la presencia del selector punto ( . ) también buscará las comillas, de modo que no obtendremos el resultado que deseamos.

Con algunos motores de regex (incluyendo el que utiliza Caché / IRIS) se puede controlar la codicia de los cuantificadores, al incluir un signo de interrogación en ellos. Entonces, la expresión regular \".*?\" ahora hace que coincidan las dos partes del texto que se encuentran entre las comillas, es decir, exactamente lo que buscábamos:

Este es "un texto" con "cuatro comillas".

 

2.1.5. Clases de caracteres (rangos)

Los corchetes se utilizan para definir los rangos o conjuntos de caracteres, por ejemplo, [a-zA-Z0-9] o [abcd] . En el lenguaje de las expresiones regulares esta notación se refiere a una clase de caracteres. Un rango coincide con los caracteres individuales, de modo que el orden de los caracteres dentro de la definición del rango no es importante: [dbac] devuelve tantas coincidencias como [abcd].

Para excluir un rango de caracteres, simplemente se coloca el símbolo ^ frente a la definición del rango (¡dentro de los corchetes!): [^abc] buscará todas las coincidencias excepto con a, b o c.

Algunos motores de regex proporcionan clases de caracteres previamente definidas (POSIX), por ejemplo:

  • [:alnum:]    [a-zA-z0-9]
  • [:alpha:]    [a-zA-Z]
  • [:blank:]    [ \t]
2.1.6. Grupos

Los componentes de una expresión regular pueden agruparse utilizando un par de paréntesis. Esto resulta útil para aplicar cuantificadores a un grupo de selectores, así como para referirse a los grupos tanto desde las expresiones regulares como desde ObjectScript. Los grupos también pueden anidarse.

Las siguientes coincidencias en las cadenas regex consisten en un número de tres dígitos, seguidos por un guion, a continuación, tres pares de letras mayúsculas y un dígito, seguidas por un guion, y para finalizar el mismo número de tres dígitos como en la primera parte:

([0-9]{3})-([A-Z][[0-9]){3}-\1

El siguiente ejemplo muestra cómo utilizar las referencias previas para que coincidan no solamente la estructura, sino también el contenido: el punto de referencia (en morado) le indica al motor que busque el mismo número de tres dígitos al principio y al final de la cadena (en amarillo). El ejemplo también muestra cómo aplicar un cuantificador a las estructuras más complejas (en verde).

La expresión regular de arriba coincidiría con la siguiente cadena:

123-D1E2F3-123

Pero no coincidiría con las siguientes cadenas:

123-D1E2F3-456             (los últimos tres dígitos son diferentes de los primeros tres)

123-1DE2F3-123             (la parte central no consiste en tres letras/pares de dígitos)

123-D1E2-123                 (la parte central incluye únicamente dos letras/pares de dígitos)

Los grupos también puede accederse utilizando los búferes de captura desde ObjectScript ( sección 4.5.1). Esta es una característica muy útil que permite buscar y extraer información al mismo tiempo.

2.1.7. Alternancia

Con el carácter de barra vertical se especifica alternancia entre opciones, por ejemplo, skyfall|done. Esto permite comparar expresiones más complejas, como las clases de caracteres que se describieron en la sección 3.1.5.

2.1.8. Referencias

Los referencias permiten consultar grupos previamente definidos (selectores dentro de los paréntesis). En el siguiente ejemplo se muestra una expresión regular que coincide con tres caracteres consecutivos, los cuales deben ser iguales:

([a-zA-Z])\1\1

Los puntos de referencia se especifican con \x, donde x representa la expresión x-ésima entre paréntesis.

2.1.9. Reglas de precedencia
  1. [] antes de ()
  2. , + y ? antes de una secuencia: ab es equivalente a a(b*), y no a (ab)*
  3. Secuencia antes de una alternancia: ab|c es equivalente a (ab)|c, y no a a(b|c)

2.2. Un poco de teoría

La evaluación de las expresiones regulares, por lo general, se realiza mediante la implementación de alguno de los dos siguientes métodos (las descripciones se han simplificado, en las referencias que se mencionan en el capítulo 5 se pueden encontrar más detalles):

  1. Orientado por el texto (DFA – Autómata finito determinista)
    El motor de búsqueda avanza a través del texto de entrada, carácter por carácter, e intenta encontrar coincidencias en el texto recorrido hasta el momento. Cuando llega al final del texto que de entrada, declara que el proceso se realizó con éxito.
     
  2. Orientado por las expresiones regulares (NFA – Autómata finito no determinista)
    El motor de búsqueda avanza a través de la expresión regular, token por token, e intenta aplicarla al texto. Cuando llega al último token (y encuentra todas las coincidencias), declara que el proceso se realizó con éxito. 

El método 1 es determinista, su tiempo de ejecución depende únicamente de la longitud del texto introducido. El orden de los selectores en las expresiones regulares no influye en el tiempo de ejecución.

El método 2 no es determinista, el motor reproduce todas las combinaciones posibles de los selectores en la expresión regular, hasta que encuentre una coincidencia o se produzca algún error. Por ende, este método es particularmente lento cuando no encuentra una coincidencia (debido a que tiene que reproducir todas la combinaciones posibles). El orden de los selectores influye en el tiempo de ejecución. Sin embargo, este método le permite retroceder y capturar los búferes.

2.3. Motores de búsqueda

Existen muchos motores de regex disponibles, algunos ya están incorporados en los lenguajes de programación o en los sistemas operativos, otros son librerías que pueden utilizarse en casi cualquier parte. Aquí se muestran algunos motores de regex, los cuales se agruparon por el tipo de método de evaluación:

  • DFA:      grep, awk, lex
  • NFA:      Perl, Tcl, Python, Emacs, sed, vi, ICU

En la siguiente tabla se realiza una comparación de las características que están disponibles en regex, entre varias bibliotecas y en lenguajes de programación:

https://community.intersystems.com/sites/default/files/inline/images/regex_engines.png

 

 

 

 

 

 

 

 

 

 

 

 

Puede encontrar más información aquí: https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines

3. RegEx y Caché

InterSystems Caché/IRIS utiliza la biblioteca ICU para buscar expresiones regulares, en los documentación online se describen muchas de estas características. El objetivo de las siguientes secciones es hacer una introducción rápida sobre cómo utilizarlas.

3.4. $match() y $locate()

En ObjectScript (COS), las funciones $match() y $locate() brindan acceso directo a la mayoría de las características de regex a las que se tiene acceso desde la biblioteca de ICU. $match(String, Regex) busca en la cadena de entrada un patrón de regex. Cuando la función encuentra una coincidencia devuelve 1, en caso contrario devuelve 0.

Ejemplos:

  • w $match("baaacd",".*(a)\1\1.*") devuelve 1
  • w $match("bbaacd",".*(a)\1\1.*") devuelve 0

$locate(String,Regex,Start,End,Value) busca en la cadena de entrada un patrón de regex, del mismo modo que lo hace $match(). Sin embargo, $locate() proporciona mayor control del proceso y también devuelve más información. En Start, se puede indicar a $locate en qué punto de la cadena de entrada debe comenzar la búsqueda. Cuando $locate() encuentra una coincidencia, regresa al punto donde se encuentra el primer carácter de la coincidencia y establece End  para la siguiente posición del carácter después de la coincidencia. El contenido de la coincidencia es devuelto en Value.