¿Cómo puedo hacer coincidir múltiples ocurrencias con una expresión regular en JavaScript similar a la preg_match_all () de PHP?

160

Estoy tratando de analizar cadenas codificadas por URL que están formadas por pares clave = valor separados por uno &o por otro &.

Lo siguiente solo coincidirá con la primera aparición, separando las claves y los valores en elementos de resultados separados:

var result = mystring.match(/(?:&|&)?([^=]+)=([^&]+)/)

Los resultados para la cadena '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' serían:

['1111342', 'Adam%20Franco']

El uso del indicador global, 'g', coincidirá con todas las ocurrencias, pero solo devolverá las subcadenas totalmente coincidentes, no las claves y valores separados:

var result = mystring.match(/(?:&|&)?([^=]+)=([^&]+)/g)

Los resultados para la cadena '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' serían:

['1111342=Adam%20Franco', '&348572=Bob%20Jones']

Si bien podría dividir la cadena &y separar cada par clave / valor individualmente, ¿hay alguna forma de usar el soporte de expresión regular de JavaScript para que coincida con múltiples ocurrencias del patrón /(?:&|&)?([^=]+)=([^&]+)/similar a la preg_match_all()función de PHP ?

Estoy buscando una forma de obtener resultados con los sub-partidos separados como:

[['1111342', '348572'], ['Adam%20Franco', 'Bob%20Jones']]

o

[['1111342', 'Adam%20Franco'], ['348572', 'Bob%20Jones']]
Adam Franco
fuente
9
Es un poco extraño que nadie recomendó usar replaceaquí. var data = {}; mystring.replace(/(?:&|&)?([^=]+)=([^&]+)/g, function(a,b,c,d) { data[c] = d; });hecho. "matchAll" en JavaScript es "reemplazar" con una función de controlador de reemplazo en lugar de una cadena.
Mike 'Pomax' Kamermans
Tenga en cuenta que para aquellos que todavía encuentran esta pregunta en 2020, la respuesta es "no use expresiones regulares, use URLSearchParams , que hace todo esto por usted".
Mike 'Pomax' Kamermans

Respuestas:

161

Izado de los comentarios

Comentario 2020: en lugar de usar regex, ahora tenemos URLSearchParams, que hace todo esto por nosotros, por lo que ya no es necesario un código personalizado, y mucho menos regex.

- Mike 'Pomax' Kamermans

El soporte del navegador se enumera aquí https://caniuse.com/#feat=urlsearchparams


Sugeriría una expresión regular alternativa, usando subgrupos para capturar el nombre y el valor de los parámetros individualmente y re.exec():

function getUrlParams(url) {
  var re = /(?:\?|&(?:amp;)?)([^=&#]+)(?:=?([^&#]*))/g,
      match, params = {},
      decode = function (s) {return decodeURIComponent(s.replace(/\+/g, " "));};

  if (typeof url == "undefined") url = document.location.href;

  while (match = re.exec(url)) {
    params[decode(match[1])] = decode(match[2]);
  }
  return params;
}

var result = getUrlParams("http://maps.google.de/maps?f=q&source=s_q&hl=de&geocode=&q=Frankfurt+am+Main&sll=50.106047,8.679886&sspn=0.370369,0.833588&ie=UTF8&ll=50.116616,8.680573&spn=0.35972,0.833588&z=11&iwloc=addr");

result es un objeto:

{
  f: "q"
  geocódigo: ""
  hl: "de"
  es decir: "UTF8"
  iwloc: "addr"
  ll: "50.116616,8.680573"
  q: "Frankfurt am Main"
  sll: "50.106047,8.679886"
  fuente: "s_q"
  spn: "0.35972,0.833588"
  sspn: "0.370369,0.833588"
  z: "11"
}

La expresión regular se descompone de la siguiente manera:

(?: # grupo no capturador
  \? | & # "?" o "&"
  (?:amperio;)? # (permitir "& amp;", para URL codificadas HTML incorrectamente)
) # finaliza el grupo sin captura
( # grupo 1
  [^ = & #] + # cualquier carácter excepto "=", "&" o "#"; al menos una vez
) # fin grupo 1 - este será el nombre del parámetro
(?: # grupo no capturador
  =? # an "=", opcional
  ( # Grupo 2
    [^ & #] * # cualquier carácter excepto "&" o "#"; cualquier cantidad de veces
  ) # fin grupo 2 - este será el valor del parámetro
) # finaliza el grupo sin captura
Tomalak
fuente
23
Esto es lo que esperaba. Lo que nunca he visto en la documentación de JavaScript es mencionar que el método exec () continuará devolviendo el siguiente conjunto de resultados si se llama más de una vez. Gracias de nuevo por el gran consejo!
Adam Franco
1
Esto se debe a esto: regular-expressions.info/javascript.html (Lea detenidamente : "Cómo usar el objeto JavaScript RegExp")
Tomalak
1
hay un error en este código: se debe eliminar el punto y coma después del "while".
Jan Willem B
1
Porque generalmente solo uso grupos normales (es decir, capturar) si realmente estoy interesado en su contenido.
Tomalak
1
@KnightYoshi Sí. En JavaScript cualquier expresión también produce su propio resultado (como x = ysería asignar ya xy también producen y). Cuando aplicamos ese conocimiento a if (match = re.exec(url)): Esta A) realiza la asignación y B) devuelve el resultado de re.exec(url)a while. Ahora re.execvuelve nullsi no hay coincidencia, que es un valor falso. En efecto, el ciclo continuará mientras haya una coincidencia.
Tomalak
67

Necesita usar el interruptor 'g' para una búsqueda global

var result = mystring.match(/(&|&)?([^=]+)=([^&]+)/g)
meouw
fuente
33
En realidad, esto no resuelve el problema: "El uso del indicador global, 'g', coincidirá con todas las apariciones, pero solo devolverá las subcadenas totalmente coincidentes, no las claves y los valores separados".
Adam Franco
40

2020 editar

Use URLSearchParams , ya que este trabajo ya no requiere ningún tipo de código personalizado. Los navegadores pueden hacer esto por usted con un solo constructor:

const str = "1111342=Adam%20Franco&348572=Bob%20Jones";
const data = new URLSearchParams(str);
for (pair of data) console.log(pair)

rendimientos

Array [ "1111342", "Adam Franco" ]
Array [ "348572", "Bob Jones" ]

Así que ya no hay razón para usar regex para esto.

Respuesta original

Si no desea confiar en la "coincidencia ciega" que viene con la execcoincidencia de estilo de ejecución , JavaScript viene con la funcionalidad de coincidencia integrada, pero es parte de la replacellamada a la función, cuando se usa "qué hacer con la captura grupos " función de manejo :

var data = {};

var getKeyValue = function(fullPattern, group1, group2, group3) {
  data[group2] = group3;
};

mystring.replace(/(?:&|&)?([^=]+)=([^&]+)/g, getKeyValue);

hecho.

En lugar de utilizar la función de manejo de grupos de captura para devolver cadenas de reemplazo (para el manejo de reemplazo, el primer argumento es la coincidencia de patrón completo, y los argumentos posteriores son grupos de captura individuales) simplemente tomamos las capturas de los grupos 2 y 3, y almacenamos ese par en caché.

Por lo tanto, en lugar de escribir funciones de análisis complicadas, recuerde que la función "matchAll" en JavaScript es simplemente "reemplazar" con una función de controlador de reemplazo, y se puede tener mucha eficiencia de coincidencia de patrones.

Mike 'Pomax' Kamermans
fuente
Tengo una cadena something "this one" and "that one". Quiero colocar todas las cadenas entre comillas dobles en una lista, es decir [esta, esa]. Hasta ahora mystring.match(/"(.*?)"/)funciona bien para detectar el primero, pero no sé cómo adaptar su solución para un solo grupo de captura.
nu everest
2
Parece que debería publicar una pregunta en Stackoverflow para eso, en lugar de tratar de resolverlo en los comentarios.
Mike 'Pomax' Kamermans
He creado una nueva pregunta: stackoverflow.com/questions/26174122/…
nu everest
1
No estoy seguro de por qué esta respuesta tiene tan pocos votos positivos, pero es la mejor respuesta a la pregunta.
Calin
Hola @ Mike'Pomax'Kamermans, las guías de la comunidad recomiendan específicamente editar entradas para mejorarlas, ver: stackoverflow.com/help/behavior . El núcleo de su respuesta es extremadamente útil, pero encontré que el lenguaje "recuerde que matchAll is replace" no estaba claro y no era una explicación de por qué su código (que no es obvio) funciona. Pensé que deberías obtener el merecido representante, así que edité tu respuesta en lugar de duplicarla con texto mejorado. Como el autor original de esta pregunta, estoy feliz de revertir la aceptación de esta respuesta (y la edición) si aún así lo desea.
Adam Franco
21

Para capturar grupos, estoy acostumbrado a usar preg_match_allen PHP y he intentado replicar su funcionalidad aquí:

<script>

// Return all pattern matches with captured groups
RegExp.prototype.execAll = function(string) {
    var match = null;
    var matches = new Array();
    while (match = this.exec(string)) {
        var matchArray = [];
        for (i in match) {
            if (parseInt(i) == i) {
                matchArray.push(match[i]);
            }
        }
        matches.push(matchArray);
    }
    return matches;
}

// Example
var someTxt = 'abc123 def456 ghi890';
var results = /[a-z]+(\d+)/g.execAll(someTxt);

// Output
[["abc123", "123"],
 ["def456", "456"],
 ["ghi890", "890"]]

</script>
Aram Kocharyan
fuente
3
@teh_senaus necesita especificar el modificador global con /glo que exec()se ejecuta de lo contrario no cambiará el índice actual y se repetirá para siempre.
Aram Kocharyan
Si llamo para validar este código myRe.test (str) y luego intento hacer execAll, se inicia en la segunda coincidencia y perdimos la primera coincidencia.
fdrv
@fdrv Debe restablecer el último índice a cero antes de iniciar el ciclo: this.lastIndex = 0;
CF
15

Establezca el gmodificador para una coincidencia global:

/…/g
Gumbo
fuente
11
En realidad, esto no resuelve el problema: "El uso del indicador global, 'g', coincidirá con todas las apariciones, pero solo devolverá las subcadenas totalmente coincidentes, no las claves y los valores separados".
Adam Franco
11

Fuente:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec

Encontrar coincidencias sucesivas

Si su expresión regular usa la bandera "g", puede usar el método exec () varias veces para encontrar coincidencias sucesivas en la misma cadena. Cuando lo hace, la búsqueda comienza en la subcadena de str especificada por la propiedad lastIndex de la expresión regular (test () también avanzará la propiedad lastIndex). Por ejemplo, suponga que tiene este script:

var myRe = /ab*/g;
var str = 'abbcdefabh';
var myArray;
while ((myArray = myRe.exec(str)) !== null) {
  var msg = 'Found ' + myArray[0] + '. ';
  msg += 'Next match starts at ' + myRe.lastIndex;
  console.log(msg);
}

Este script muestra el siguiente texto:

Found abb. Next match starts at 3
Found ab. Next match starts at 912

Nota: No coloque el literal de expresión regular (o el constructor RegExp) dentro de la condición while o creará un bucle infinito si hay una coincidencia debido a que la propiedad lastIndex se restablece en cada iteración. También asegúrese de que el indicador global esté configurado o de que también se produzca un bucle.

KIM Taegyoon
fuente
Si llamo para validar este código myRe.test (str) y luego intento hacer while, aparece en el segundo partido y perdimos el primero.
fdrv
También se pueden combinar String.prototype.matchcon la gbandera: 'abbcdefabh'.match(/ab*/g)retornos['abb', 'ab']
thom_nic
2

Si alguien (como yo) necesita el método de Tomalak con soporte de matriz (es decir, selección múltiple), aquí está:

function getUrlParams(url) {
  var re = /(?:\?|&(?:amp;)?)([^=&#]+)(?:=?([^&#]*))/g,
      match, params = {},
      decode = function (s) {return decodeURIComponent(s.replace(/\+/g, " "));};

  if (typeof url == "undefined") url = document.location.href;

  while (match = re.exec(url)) {
    if( params[decode(match[1])] ) {
        if( typeof params[decode(match[1])] != 'object' ) {
            params[decode(match[1])] = new Array( params[decode(match[1])], decode(match[2]) );
        } else {
            params[decode(match[1])].push(decode(match[2]));
        }
    }
    else
        params[decode(match[1])] = decode(match[2]);
  }
  return params;
}
var urlParams = getUrlParams(location.search);

entrada ?my=1&my=2&my=things

resultado 1,2,things(solo devuelto anteriormente: cosas)

fedu
fuente
1

Solo para seguir con la pregunta propuesta como lo indica el título, puede iterar sobre cada coincidencia en una cadena usando String.prototype.replace(). Por ejemplo, lo siguiente hace exactamente eso para obtener una matriz de todas las palabras basadas en una expresión regular:

function getWords(str) {
  var arr = [];
  str.replace(/\w+/g, function(m) {
    arr.push(m);
  });
  return arr;
}

var words = getWords("Where in the world is Carmen Sandiego?");
// > ["Where", "in", "the", "world", "is", "Carmen", "Sandiego"]

Si quisiera obtener grupos de captura o incluso el índice de cada partido, también podría hacerlo. A continuación se muestra cómo se devuelve cada coincidencia con la coincidencia completa, el primer grupo de captura y el índice:

function getWords(str) {
  var arr = [];
  str.replace(/\w+(?=(.*))/g, function(m, remaining, index) {
    arr.push({ match: m, remainder: remaining, index: index });
  });
  return arr;
}

var words = getWords("Where in the world is Carmen Sandiego?");

Después de ejecutar lo anterior, wordsserá el siguiente:

[
  {
    "match": "Where",
    "remainder": " in the world is Carmen Sandiego?",
    "index": 0
  },
  {
    "match": "in",
    "remainder": " the world is Carmen Sandiego?",
    "index": 6
  },
  {
    "match": "the",
    "remainder": " world is Carmen Sandiego?",
    "index": 9
  },
  {
    "match": "world",
    "remainder": " is Carmen Sandiego?",
    "index": 13
  },
  {
    "match": "is",
    "remainder": " Carmen Sandiego?",
    "index": 19
  },
  {
    "match": "Carmen",
    "remainder": " Sandiego?",
    "index": 22
  },
  {
    "match": "Sandiego",
    "remainder": "?",
    "index": 29
  }
]

Para hacer coincidir múltiples ocurrencias similares a las que están disponibles en PHP preg_match_all, puede usar este tipo de pensamiento para hacer las suyas o usar algo como YourJS.matchAll(). YourJS define más o menos esta función de la siguiente manera:

function matchAll(str, rgx) {
  var arr, extras, matches = [];
  str.replace(rgx.global ? rgx : new RegExp(rgx.source, (rgx + '').replace(/[\s\S]+\//g , 'g')), function() {
    matches.push(arr = [].slice.call(arguments));
    extras = arr.splice(-2);
    arr.index = extras[0];
    arr.input = extras[1];
  });
  return matches[0] ? matches : null;
}
Chris West
fuente
Como desea analizar la cadena de consulta de una URL, también puede usar algo como YourJS.parseQS()( yourjs.com/snippets/56 ), aunque muchas otras bibliotecas también ofrecen esta funcionalidad.
Chris West
Modificar una variable desde un ámbito externo en un bucle que se supone que devuelve un reemplazo es algo malo. Su mal uso reemplaza aquí
Juan Mendes
1

Si puede salirse con la suya, mapesta es una solución de cuatro líneas:

var mystring = '1111342=Adam%20Franco&348572=Bob%20Jones';

var result = mystring.match(/(&|&amp;)?([^=]+)=([^&]+)/g) || [];
result = result.map(function(i) {
  return i.match(/(&|&amp;)?([^=]+)=([^&]+)/);
});

console.log(result);

No es bonito, no es eficiente, pero al menos es compacto. ;)

fboes
fuente
1

Uso window.URL:

> s = 'http://www.example.com/index.html?1111342=Adam%20Franco&348572=Bob%20Jones'
> u = new URL(s)
> Array.from(u.searchParams.entries())
[["1111342", "Adam Franco"], ["348572", "Bob Jones"]]
jnnnnn
fuente
1

Hola desde 2020. Permítanme llamar su atención sobre String.prototype.matchAll () :

let regexp = /(?:&|&amp;)?([^=]+)=([^&]+)/g;
let str = '1111342=Adam%20Franco&348572=Bob%20Jones';

for (let match of str.matchAll(regexp)) {
    let [full, key, value] = match;
    console.log(key + ' => ' + value);
}

Salidas:

1111342 => Adam%20Franco
348572 => Bob%20Jones
Klesun
fuente
¡Finalmente! Una nota de precaución: "ECMAScript 2020, la 11ª edición, introduce el método matchAll para Strings, para producir un iterador para todos los objetos de coincidencia generados por una expresión regular global" . Según el sitio vinculado en la respuesta, la mayoría de los navegadores y nodeJS lo admiten actualmente, pero no IE, Safari o Samsung Internet. Esperemos que el apoyo se amplíe pronto, pero YMMV por un tiempo.
Adam Franco
0

Para capturar varios parámetros con el mismo nombre, modifiqué el ciclo while en el método de Tomalak de esta manera:

  while (match = re.exec(url)) {
    var pName = decode(match[1]);
    var pValue = decode(match[2]);
    params[pName] ? params[pName].push(pValue) : params[pName] = [pValue];
  }

entrada: ?firstname=george&lastname=bush&firstname=bill&lastname=clinton

devoluciones: {firstname : ["george", "bill"], lastname : ["bush", "clinton"]}

ivar
fuente
Si bien me gusta tu idea, no funciona bien con parámetros individuales, como es de ?cinema=1234&film=12&film=34esperar {cinema: 1234, film: [12, 34]}. Editó su respuesta para reflejar esto.
TWiStErRob
0

Bueno ... tuve un problema similar ... Quiero una búsqueda incremental / por pasos con RegExp (por ejemplo: iniciar búsqueda ... hacer un poco de procesamiento ... continuar la búsqueda hasta la última coincidencia)

Después de muchas búsquedas en Internet ... como siempre (esto se está convirtiendo en un hábito ahora) termino en StackOverflow y encontré la respuesta ...

Lo que no se refiere y lo que hay que mencionar es " lastIndex" Ahora entiendo por qué el objeto RegExp implementa la lastIndexpropiedad " "

ZEE
fuente
0

Dividirlo me parece la mejor opción:

'1111342=Adam%20Franco&348572=Bob%20Jones'.split('&').map(x => x.match(/(?:&|&amp;)?([^=]+)=([^&]+)/))
pguardiario
fuente
0

Para evitar el infierno de expresiones regulares, puedes encontrar tu primer partido, corta un trozo e intenta encontrar el siguiente en la subcadena. En C # esto se parece a esto, lo siento, no lo he portado a JavaScript para usted.

        long count = 0;
        var remainder = data;
        Match match = null;
        do
        {
            match = _rgx.Match(remainder);
            if (match.Success)
            {
                count++;
                remainder = remainder.Substring(match.Index + 1, remainder.Length - (match.Index+1));
            }
        } while (match.Success);
        return count;
Andrew Paté
fuente