30

Detalles de añadidos

Encontré en internet las siguientes dos formas de negar:

  • ?!
  • [^\w]

Pero no logro encontrar documentación en español y el inglés que usan para describir el funcionamiento lo considero demasiado avanzado para comprender el significado de ambos y como usarlos apropiadamente para obtener el resultado esperado. La respuesta actual soluciona el problema pero no da definición de uso.


Planteamiento del problema

Quiero seleccionar todas aquellas palabras que no estén entre comillas dentro de un texto. Sé como hacer lo contrario.

Ejemplo:

Lorem ipsum "dolor sit amet", consectetur adipiscing elit, maecenas est felis "sit amet".

Con la siguiente expresión regular podría tomar las palabras que están entre paréntesis:

/"([\w\s]+)"/gim

El resultado

[
    1 => 'dolor sit amet',
    2 => 'sit amet'
]

Lo que busco

[
    1 => 'Lorem ipsum ',
    2 => ', consectetur adipiscing elit, maecenas est felis ',
    3 => '.',
]

Otro ejemplo sería, de la lista siguiente:

  • Hola
  • Hol@
  • hol.
  • hol*
  • holo

Imprimir / seleccionar aquellos que no usen caracteres alfanuméricos (sé como tomar los contrario a la indicación establecida). Tomar todo lo que no es un caracter alfanumérico, tomar todas aquellas palabras que no tenga una «l», tomar todo aquello que no inicie con la letra «z», etc.

Ejemplo funcional: http://www.regextester.com/15

Yo esperaría hacer algo como esto para todo lo que no inicie con «a»:

/!^a.*/

Pero evidentemente no me funciona, quedo a espera de su feedback.

Aclaración

Me gustaría también entender la solución que planteen y no sólo sea un copy-paste para resolver el problema.

Nota: La expresión regular que aquí cito para obtener el texto me funciona en PHP y JavaScript (lenguajes que uso para resolver el problema), he visto que hay pequeñas variaciones de expresiones regulares en los distintos lenguajes pero entre éstos 2 no es algo sustancial. Por tanto me gustaría que la solución propuesta funcione en uno los 2.

Fuentes de apoyo

Chofoteddy
  • 5,975
  • 5
  • 25
  • 65
  • depende el lenguaje y framework de programación que uses porque lamentablemente existen varios 'estándares' de expresiones regulares y no todos los lenguajes y frameworks soportan el mismo 'estándar' – JuanK Dec 08 '15 at 22:27
  • @JuanK Así es, por eso pongo como nota que me interesa que funcioné sólo para `PHP` y / o `JavaScript` – Chofoteddy Dec 08 '15 at 22:31
  • Creo que ayudaría si pusieras los resultados exactos que esperas en tu ejemplo – Jorge Pestano Dec 08 '15 at 22:37
  • Quizás no entendí bien tu idea, pero en mi opinión hay un error en la sección "Lo que busco": el segundo elemento seleccionado debería comenzar con una coma y terminar con un espacio. – Nicolás Dec 09 '15 at 01:10
  • @Chofoteddy, agregue una respuesta relacionada a expresiones regulares en el caso que lo creas util. – Federico Piazza May 31 '16 at 22:50

4 Answers4

31

Agrego esta respuesta como información relacionada a expresiones regulares. Es mi primer respuesta en SO en Español y no es una traducción, por lo que si no está correcta puedo eliminarla o corregirla.

Con respecto a lo que comentaste en tu pregunta:

(?!  ) -> conocido como Negated lookahead en inglés
[^\w]  -> conocido como Character class en inglés (en este caso negada)

Esto son dos conceptos diferentes. Por un lado tenés lo que se considera un lookaround y por otro lado una character class. Funcionan de este modo:


Lookarounds

Los lookaround, se podria entender como diferentes formas de ver si un patrón está (o no está) precedido o sucedido por otro patrón. Por ejemplo, la expresión hola(?!chau) va a hacer matching de la palabra hola siempre y cuando no exista la palabra chau a continuación.

Regular expression visualization

Es decir:

hola, ¿qué tal?   <-- OK
hola SO           <-- OK
holachau          <-- Falla

Tu pregunta esta relacionada sobre "cómo negar", pero también quería mencionarte que los lookarounds se dividen en:

  • Lookahead (ver hacia adelante):
    • Positive: se define como hola(?=chau) y va a matchear la palabra hola solo si a continuacion existe chau
    • Negative: se define como hola(?!chau) y va a matchear la palabra hola solo si a continuacion NO existe chau
  • Lookbehind (ver hacia atras):
    • Positive: se define como (?<=chau)hola y va a matchear la palabra hola solo si a existe chau antes que hola
    • Negative: se define como (?<!chau)hola y va a matchear la palabra hola solo si NO existe chau antes que hola

Es importante mencionar que los lookbehind no están soportados por Javascript en todos los navegadores (ver compatibilidad).

Podes encontrar mas informacion sobre los lookarounds en:
http://www.regular-expressions.info/lookaround.html


Character classes (Clases de caracteres)

Por otro lado, existen los character class, que en español una forma de entenderlo sería como conjunto de caracteres (o clase de caracteres) y se usa usando los corchetes [..].

Es decir, que si tenemos [aeiou], solo se va a hacer matching con las vocales sin tildes.

Regular expression visualization

Del mismo modo, una clase se puede negar, como mencionaste usando ^ al principio... por lo que [^aeiou] en este caso va a hacer matching con un caracter que no sea una vocal sin tilde.

Regular expression visualization

Aca hay mas informacion de los character classes:
http://www.regular-expressions.info/charclass.html


Verbos

Ahora, luego de darte un poco de contexto. Si querés usar expresiones regulares para tomar/coincidir todas las palabras que no estén entre comillas, entonces PCRE (Expresiones Regulares Compatibles con Perl, soportadas por PHP, R, Delphi y otros) tiene verbos que son muy útiles en tu caso.

Los más conocidos son (*SKIP) y (*FAIL) que se suelen usar en conjunto y se suelen usar de esta forma:

".*?"(*SKIP)(*FAIL)|(\w+)

Ejemplo practico

Este tipo de patrones se suele llamar técnica de descarte (discard technique), y siempre usan la misma forma de patrones separado por OR:

patrón de descarte 1|patrón de descarte 2|patrón de descarte N|(GUARDAR ESTO)

Regular expression visualization

Por ende, la expresión de arriba ".*?"(*SKIP)(*FAIL)|(\w+) va a descartar todas las coincidencias de lo que esté antes de skip y fail (".*?"), y se va a capturar el ultimo patrón (que está usando paréntesis... los paréntesis se usan para capturar contenido).

La expresión regular ".*?"(*SKIP)(*FAIL)|(\w+) explicada sería:

".*?"     Lo uso para buscar lo que SI quiero descartar, y para indicarle
          al engine que descarte agrego (*SKIP)(*FAIL)
|(\w+)    o (si el patrón no se descarta) busco las palabras y las capturo
              

Por ende, en el link de arriba, cuando se aplica esa expresión al texto:

Lorem ipsum "dolor sit amet", consectetur adipiscing elit, maecenas est felis "sit amet".

Se captura el siguiente contenido:

MATCH 1
1.  [0-5]   `Lorem`
MATCH 2
1.  [6-11]  `ipsum`
MATCH 3
1.  [30-41] `consectetur`
MATCH 4
1.  [42-52] `adipiscing`
MATCH 5
1.  [53-57] `elit`
MATCH 6
1.  [59-67] `maecenas`
MATCH 7
1.  [68-71] `est`
MATCH 8
1.  [72-77] `felis`

Conclusión, las expresiones regulares en mi opinión son espectaculares pero sólo si se saben usar. En mi caso personal, no puedo vivir sin ellas, pero como todo... para clavar un clavo se necesita un martillo y no un destornillador. En el caso de las regex, son excelentes para pattern matching, pero si necesitás lógica entonces definitivamente no es la herramienta indicada.

Pollo
  • 1,980
  • 6
  • 21
Federico Piazza
  • 426
  • 5
  • 8
  • 2
    He quedado atónito con tu respuesta, buena explicación, documentación. Te felicito y agradezco tu excelente respuesta, te ganaste el voto de la mejor respuesta. Te agradezco la información, ¿hay algún libro que me recomiendes sobre lo que explicaste? (no importa que sea en inglés). Por cierto: Match == Coincidir / Coincidencia. Revisa la última modificación de tu respuesta para mayor info: http://es.stackoverflow.com/posts/11509/revisions. Excelente trabajo. – Chofoteddy Jun 01 '16 at 01:50
  • @Chofoteddy, un placer ayudar. No tengo acentos jaja, gracias por corregir eso. Con respecto a documentacion, aprendi mucho de http://www.regular-expressions.info/quickstart.html. Ya tengo mi primer voto positivo! :) – Federico Piazza Jun 01 '16 at 02:18
  • Federico: la sugerencia de edición https://es.stackoverflow.com/review/suggested-edits/241195 incluía mejoras importantes en la respuesta que creo que la hacían aún mejor. ¿Podrías considerar aceptarlas? Se está debatiendo en Meta: https://es.meta.stackoverflow.com/q/4916/83 – fedorqui Nov 09 '20 at 10:19
9

Lo mejor en estos casos es tomar el camino fácil (Regexp es un infierno). Entonces si ya tienes como encontrar lo que no quieres encontrar con

/"([\w\s]+)"/gim

Entonces lo más fácil es usar preg_split para borrar todo lo que coincida con esa expresión

preg_split("/\"([\w\s]+)\"/", $input_line);

Al ejecutar esto en la cadena que tienes de ejemplo te devolvera tres bloques , que son los bloques que no están contenidos en comillas

[
    1 => 'Lorem ipsum ',
    2 => ', consectetur adipiscing elit, maecenas est felis ',
    3 => '.',
]

Si quieres obtener lo que se borrará haces primero preg_match() y luego puedes hacer un split normal de la cadena usando explode sin necesidad de preg_split.

Desde luego puedes usar preg_split pero serían ciclos de procesamiento innecesarios.

Para el otro caso es un poco más fácil

todo lo que tenga caracteres no alfanuméricos

Hola
Hol@
hol.
hol*
holo
Imprimir / seleccionar aquellos que no usen caracteres alfanuméricos

Basta con usar un rango negado como esta expresión que marca todos los caracteres no alfanuméricos

[^a-zA-z0-9]+

Ya con esta expresión puedes obtener las entradas que hacen mathc usando preg_grep

preg_grep("/[^a-zA-z0-9]+/", explode("\n", $input_lines));

output

array(3
1   =>  Hol@
2   =>  hol.
3   =>  hol*
)

todo lo que no comience por a

  • arroz
  • beso
  • academia
  • foto
  • juego
  • arte

Con esta expresión: ^[^a]+

preg_grep("/^[^a]+/", explode("\n", $input_lines));

output

array(3
1   =>  beso
3   =>  foto
4   =>  juego
)
JuanK
  • 2,437
  • 12
  • 35
  • Entiendo que con eso lo borraría, pero en caso que deseé disponer de esos textos que quedaron como lo hace la función [preg_match()](http://php.net/manual/es/function.preg-match.php) en PHP, ¿cómo quedaría?. Haciendo uso de funciones de remplazo me queda un texto plano donde no puedo saber en que partes se elimino texto, etc. – Chofoteddy Dec 08 '15 at 23:02
  • para obtener lo que se borrará haces primero `preg_match()` y luego puedes hacer un split normal de la cadena usando `explode` sin necesidad de `preg_split` -> adicionado a la respuesta – JuanK Dec 08 '15 at 23:11
  • En javascript también se podría utilizar `split`. – eloyesp Dec 09 '15 at 00:17
  • @JuanK si, funciona para cadenas de ese tipo pero no me sirve como negación de una búsqueda, es decir: "encuentra todas las palabras que no tengan una **a**" – Chofoteddy Dec 11 '15 at 17:36
  • Puedes usar tambiien preg_replace y sustituir las coincidencias por "". – Lorthas Jan 06 '17 at 20:15
4

Si utilizas la siguiente expresión regular:

"([\w ]*)"

regex

O más exactamente algo similar al siguiente código:

<?php
  $str = "Lorem ipsum \"dolor sit amet\", consectetur adipiscing elit, maecenas est felis \"sit amet\".";
  $array = preg_split("/\"([\\w ]*)\"/", $str, -1,
          PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
  print_r($array);
?>

Obtienes la siguiente salida:

Array
(
    [0] => Lorem ipsum 
    [1] => dolor sit amet
    [2] => , consectetur adipiscing elit, maecenas est felis 
    [3] => sit amet
    [4] => .
)

Ver demo en línea.

Si echas un vistazo en la documentación de la función preg_split, encontrarás que la bandera PREG_SPLIT_NO_EMPTY elimina de la salida las cadenas vacías y la bandera PREG_SPLIT_DELIM_CAPTURE devuelve la parte de la expresión regular entre paréntesis en el resultado.

Paul Vargas
  • 181
  • 1
  • 20
  • 39
1

Nota: La respuesta de Federico Piazza es excelente, lean esa primero.
-Yo quiero complementarla con cómo se haría en JavaScript. Federico habla de la técnica de descarte, pero la presenta con verbos de control que son exclusivos de PCRE (no están en JavaScript).


Técnica de descarte (También llamada "el mejor truco de Regex" por RexEgg)
-Funciona en JavaScript.

Es muy sencillo, consiste en

/lo que no quieras|(esto sí)/


¡Eso es todo!

Este "truco" se basa en que efectivamente va a coincidir con lo que uno no quiere que coincida, pero acá viene el truco: no se va a capturar! Esa sutil diferencia es la que nos va a permitir saber si coincidió con nuestra excepción o si coincidió con lo que queríamos que coincida.

Los paréntesis en (esto sí) crean un grupo y, como todo grupo, cuando coinciden con el texto lo capturan... Eso quiere decir que se obtienen por separado en el resultado de RegExp.exec() o de String.matchAll(). Por lo tanto, solo es cuestión de verificar si se capturó algo en el grupo 1 o no.

Tomemos el ejemplo de la pregunta: seleccionar todo el texto exceptuando las partes entre comillas.

/".*?"|([^"]+)/g


Código:

const regex = /".*?"|([^"]+)/g,
      texto = 'Lorem ipsum "dolor sit amet", consectetur adipiscing elit, maecenas est felis "sit amet".';

let resultado = 
        [ ...texto.matchAll(regex) ]                  //obtenemos todas las coincidencias
            .filter(match => match[1] !== undefined)  //excepto si no capturaron en grupo 1
            .map(match => match[0]);                  //nos quedamos con la coincidencia

console.log(resultado);
Pollo
  • 1,980
  • 6
  • 21