Post Top Ad

Your Ad Spot

domingo, 28 de junio de 2020

Expresiones regulares en JavaScript: afirmaciones de búsqueda por ejemplo

Estas son aserciones de búsqueda en expresiones regulares en JavaScript:
  • Positiva anticipada: (?=«pattern»)
  • Lookahead negativo: (?!«pattern»)
  • Mirada hacia atrás positiva: (?<=«pattern»)
  • Mirada negativa hacia atrás: (?<!«pattern»)
Esta publicación de blog muestra ejemplos de cómo usarlos.

Hoja de trucos: afirmaciones de búsqueda   

En la ubicación actual en la cadena de entrada:
  • Afirmaciones anticipadas (ECMAScript 3):
    • Búsqueda anticipada positiva: (?=«pattern»)coincide si patterncoincide con lo que viene después de la ubicación actual.
    • Búsqueda anticipada negativa: (?!«pattern»)coincide si patternno coincide con lo que viene después de la ubicación actual.
  • Afirmaciones retrospectivas (ECMAScript 2018):
    • Retrospectiva positiva: (?<=«pattern»)coincide si patterncoincide con lo que viene antes de la ubicación actual.
    • Retroceso negativo: (?<!«pattern»)coincide si patternno coincide con lo que precede a la ubicación actual.
Para obtener más información, consulte "JavaScript para programadores impacientes": aserciones anticipadas , aserciones posteriores .

Una palabra de precaución sobre las expresiones regulares   

Las expresiones regulares son una espada de doble filo: poderosa y corta, pero también descuidada y críptica. A veces, los enfoques diferentes y más largos (especialmente los analizadores adecuados) pueden ser mejores, especialmente para el código de producción.
Otra advertencia es que las afirmaciones retrospectivas son una característica relativamente nueva que puede no ser compatible con todos los motores de JavaScript a los que se dirige.

Ejemplo: Especificar lo que viene antes o después de un partido (búsqueda positiva)   

En la siguiente interacción, extraemos palabras citadas:
> 'how "are" "you" doing'.match(/(?<=")[a-z]+(?=")/g)
[ 'are', 'you' ]
Dos afirmaciones de búsqueda nos ayudan aquí:
  • (?<=") "Debe ir precedido de una cita"
  • (?=") "Debe ir seguido de una cita"
Las aserciones de búsqueda son especialmente convenientes .match()en su /gmodo, que devuelve coincidencias completas (grupo de captura 0). Cualquiera que sea el patrón de una aseveración de búsqueda de coincidencias, no se captura. Sin aserciones de búsqueda, las citas aparecen en el resultado:
> 'how "are" "you" doing'.match(/"([a-z]+)"/g)
[ '"are"', '"you"' ]

Ejemplo: Especificar lo que no viene antes o después de una coincidencia (aspecto negativo)   

¿Cómo podemos lograr lo contrario de lo que hicimos en la sección anterior y extraer todas las palabras sin comillas de una cadena?
  • Entrada: 'how "are" "you" doing'
  • Salida: ['how', 'doing']
Nuestro primer intento es simplemente convertir las aserciones positivas de lookaround en aserciones negativas de lookaround. Por desgracia, eso falla:
> 'how "are" "you" doing'.match(/(?<!")[a-z]+(?!")/g)
[ 'how', 'r', 'o', 'doing' ]
El problema es que extraemos secuencias de caracteres que no están entre corchetes. Eso significa que en la cadena '"are"', la "r" en el medio se considera sin comillas, porque está precedida por una "a" y seguida por una "e".
Podemos arreglar esto afirmando que el prefijo y el sufijo no deben ser ni comillas ni letras:
> 'how "are" "you" doing'.match(/(?<!["a-z])[a-z]+(?!["a-z])/g)
[ 'how', 'doing' ]
Otra solución es exigir a través de \beso que la secuencia de caracteres [a-z]+comience y termine en los límites de palabras:
> 'how "are" "you" doing'.match(/(?<!")\b[a-z]+\b(?!")/g)
[ 'how', 'doing' ]
Una cosa que es agradable acerca de la mirada hacia atrás negativa y hacia adelante negativa es que también funcionan al principio o al final, respectivamente, de una cadena, como se demuestra en el ejemplo.

No hay alternativas simples a las afirmaciones negativas de búsqueda   

Las afirmaciones negativas de búsqueda son una herramienta poderosa y difícil de emular a través de otros medios (expresión regular).
Si no desea utilizarlos, normalmente debe adoptar un enfoque completamente diferente. Por ejemplo, en este caso, podría dividir la cadena en palabras (entre comillas y sin comillas) y luego filtrarlas:
const str = 'how "are" "you" doing';

const allWords = str.match(/"?[a-z]+"?/g);
const unquotedWords = allWords.filter(w => !w.startsWith('"') || !w.endsWith('"'));
assert.deepEqual(unquotedWords, ['how', 'doing']);
Beneficios de este enfoque:
  • Funciona en motores más antiguos.
  • Es facil de entender.

Interludio: apuntando afirmaciones de búsqueda hacia adentro   

Todos los ejemplos que hemos visto hasta ahora tienen en común que las afirmaciones de búsqueda dictan lo que debe venir antes o después del partido, pero sin incluir esos caracteres en el partido.
Las expresiones regulares que se muestran en el resto de esta publicación de blog son diferentes: sus afirmaciones de búsqueda apuntan hacia adentro y restringen lo que hay dentro del partido.

Ejemplo: las cadenas de coincidencias no comienzan con 'abc'  

Supongamos que queremos hacer coincidir todas las cadenas que no comienzan 'abc'Nuestro primer intento podría ser la expresión regular /^(?!abc)/.
Eso funciona bien para .test():
> /^(?!abc)/.test('xyz')
true
Sin embargo, .exec()nos da una cadena vacía:
> /^(?!abc)/.exec('xyz')
{ 0: '', index: 0, input: 'xyz', groups: undefined }
El problema es que las aserciones como las aserciones de búsqueda no expanden el texto coincidente. Es decir, no capturan caracteres de entrada, solo hacen demandas sobre la ubicación actual en la entrada.
Por lo tanto, la solución es agregar un patrón que capture los caracteres de entrada:
> /^(?!abc).*$/.exec('xyz')
{ 0: 'xyz', index: 0, input: 'xyz', groups: undefined }
Según lo deseado, esta nueva expresión regular rechaza las cadenas que tienen el prefijo 'abc':
> /^(?!abc).*$/.exec('abc')
null
> /^(?!abc).*$/.exec('abcd')
null
Y acepta cadenas que no tienen el prefijo completo:
> /^(?!abc).*$/.exec('ab')
{ 0: 'ab', index: 0, input: 'ab', groups: undefined }

Ejemplo: coincide con subcadenas que no contienen '.mjs'  

En el siguiente ejemplo, queremos encontrar
import ··· from '«module-specifier»';
donde module-specifierno se agota '.mjs'.
const code = `
import {transform} from './util';
import {Person} from './person.mjs';
import {zip} from 'lodash';
`.trim();
assert.deepEqual(
  code.match(/^import .*? from '[^']+(?<!\.mjs)';$/umg),
  [
    "import {transform} from './util';",
    "import {zip} from 'lodash';",
  ]);
Aquí, la afirmación de mirar atrás (?<!\.mjs)actúa como una protección y evita que la expresión regular coincida con las cadenas que contienen '.mjs'en esta ubicación.

Ejemplo: omitir líneas con comentarios   

Escenario: Queremos analizar las líneas con la configuración, mientras omitimos los comentarios. Por ejemplo:
const RE_SETTING = /^(?!#)([^:]*):(.*)$/

const lines = [
  'indent: 2', // setting
  '# Trim trailing whitespace:', // comment
  'whitespace: trim', // setting
];
for (const line of lines) {
  const match = RE_SETTING.exec(line);
  if (match) {
    const key = JSON.stringify(match[1]);
    const value = JSON.stringify(match[2]);
    console.log(`KEY: ${key} VALUE: ${value}`);
  }
}

// Output:
// 'KEY: "indent" VALUE: " 2"'
// 'KEY: "whitespace" VALUE: " trim"'
¿Cómo llegamos a la expresión regular RE_SETTING?
Comenzamos con la siguiente expresión regular para la configuración:
/^([^:]*):(.*)$/
Intuitivamente, es una secuencia de las siguientes partes:
  • Inicio de la línea
  • No dos puntos (cero o más)
  • Un solo colon
  • Cualquier personaje (cero o más)
  • El final de la línea
Esta expresión regular rechaza algunos comentarios:
> /^([^:]*):(.*)$/.test('# Comment')
false
Pero acepta otros (que tienen dos puntos en ellos):
> /^([^:]*):(.*)$/.test('# Comment:')
true
Podemos arreglar eso prefijando (?!#)como guardia. Intuitivamente, significa: "La ubicación actual en la cadena de entrada no debe ser seguida por el carácter #".
La nueva expresión regular funciona como se desea:
> /^(?!#)([^:]*):(.*)$/.test('# Comment:')
false

Ejemplo: comillas inteligentes   

Supongamos que queremos convertir pares de comillas dobles rectas en comillas rizadas:
  • Entrada: `"yes" and "no"`
  • Salida: `“yes” and “no”`
Este es nuestro primer intento:
> `The words "must" and "should".`.replace(/"(.*)"/g, '“$1”')
'The words “must" and "should”.'
Solo la primera cita y la última cita son rizadas. El problema aquí es que el *cuantificador coincide con avidez (tanto como sea posible).
Si ponemos un signo de interrogación después del *, coincide de mala gana :
> `The words "must" and "should".`.replace(/"(.*?)"/g, '“$1”')
'The words “must” and “should”.'

Soporte de escape a través de barras invertidas   

¿Qué sucede si queremos permitir el escape de las comillas mediante barras diagonales inversas? Podemos hacerlo usando el protector (?<!\\)antes de las comillas:
> String.raw`\"stright\" and "curly"`.replace(/(?<!\\)"(.*?)(?<!\\)"/g, '“$1”')
'\\"stright\\" and “curly”'
Como paso posterior al procesamiento, aún tendríamos que hacer:
.replace(/\\"/g, `"`)
Sin embargo, esta expresión regular puede fallar cuando hay una barra invertida con barra invertida:
> String.raw`Backslash: "\\"`.replace(/(?<!\\)"(.*?)(?<!\\)"/g, '“$1”')
'Backslash: "\\\\"'
La segunda barra invertida evitó que las citas se volvieran rizadas.
Podemos arreglar eso si hacemos que nuestra guardia sea más sofisticada ( ?:hace que el grupo no capture):
(?<=[^\\](?:\\\\)*)
(Crédito: @jonasraoni )
El nuevo protector permite pares de barras invertidas antes de las comillas:
> String.raw`Backslash: "\\"`.replace(/(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g, '“$1”')
'Backslash: “\\\\”'
Queda un problema. Este protector evita que la primera cita coincida si aparece al comienzo de una cadena:
> `"abc"`.replace(/(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g, '“$1”')
'"abc"'
Podemos arreglar eso cambiando el primer guardia a: (?<=[^\\](?:\\\\)*|^)
> `"abc"`.replace(/(?<=[^\\](?:\\\\)*|^)"(.*?)(?<=[^\\](?:\\\\)*)"/g, '“$1”')
'“abc”'

Lectura adicional   

  • Capítulo "Expresiones regulares ( RegExp)" en "JavaScript para programadores impacientes"

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

outbrain

Páginas