Como posso combinar várias ocorrências com uma regex em JavaScript semelhante à preg_match_all () do PHP?

160

Estou tentando analisar seqüências de caracteres codificadas em URL que são compostas de pares chave = valor separados por um &ou outro& .

O seguinte corresponderá apenas à primeira ocorrência, dividindo as chaves e os valores em elementos de resultado separados:

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

Os resultados para a sequência '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' seriam:

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

O uso do sinalizador global 'g' corresponderá a todas as ocorrências, mas retornará apenas as sub-sequências totalmente correspondentes, não as chaves e valores separados:

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

Os resultados para a sequência '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' seriam:

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

Embora eu possa dividir a string &e separar cada par de chave / valor individualmente, há alguma maneira de usar o suporte à expressão regular do JavaScript para corresponder a várias ocorrências do padrão /(?:&|&)?([^=]+)=([^&]+)/semelhante ao PHPpreg_match_all() função ?

Estou buscando uma maneira de obter resultados com as sub-correspondências separadas como:

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

ou

[['1111342', 'Adam%20Franco'], ['348572', 'Bob%20Jones']]
Adam Franco
fonte
9
é um pouco estranho que ninguém recomendou usar replaceaqui. var data = {}; mystring.replace(/(?:&|&)?([^=]+)=([^&]+)/g, function(a,b,c,d) { data[c] = d; });feito. "matchAll" no JavaScript é "replace" por uma função de manipulador de substituição em vez de uma string.
Mike 'Pomax' Kamermans
Observe que para aqueles que ainda encontram essa pergunta em 2020, a resposta é "não use regex, use URLSearchParams , que faz tudo isso para você".
Mike 'Pomax' Kamermans 23/01

Respostas:

161

Içado dos comentários

Comentário de 2020: em vez de usar regex, agora temos URLSearchParams , o que faz tudo isso para nós, portanto, nenhum código personalizado, muito menos regex, é mais necessário.

- Mike 'Pomax' Kamermans

O suporte ao navegador está listado aqui https://caniuse.com/#feat=urlsearchparams


Eu sugeriria uma regex alternativa, usando subgrupos para capturar o nome e o valor dos parâmetros individualmente e 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 é um objeto:

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

A regex é dividida da seguinte maneira:

(?: # grupo não captador
  \? | & # "?" ou "&"
  (?: amp;)? # (permitir "& amp;", para URLs codificados incorretamente em HTML)
) # final do grupo não capturante
( # grupo 1
  [^ = & #] + # qualquer caractere, exceto "=", "&" ou "#"; pelo menos uma vez
) # end group 1 - este será o nome do parâmetro
(?: # grupo não captador
  =? # an "=", opcional
  (# grupo 2
    [^ & #] * # qualquer caractere, exceto "&" ou "#"; qualquer número de vezes
  ) # end group 2 - este será o valor do parâmetro
) # final do grupo não capturante
Tomalak
fonte
23
Era isso que eu estava esperando. O que nunca vi na documentação do JavaScript é mencionar que o método exec () continuará retornando o próximo conjunto de resultados se chamado mais de uma vez. Mais uma vez obrigado pela ótima dica!
Adam Franco
1
Ele faz por causa disso: regular-expressions.info/javascript.html (Leia através de: "Objeto como usar o JavaScript RegExp")
Tomalak
1
existe um erro neste código: o ponto e vírgula após o "while" deve ser removido.
Jan Willem B
1
Porque geralmente só uso grupos normais (ou seja, captura) se estou realmente interessado no conteúdo deles.
precisa
1
@KnightYoshi Sim. Em JavaScript qualquer expressão também produz seu próprio resultado (como x = yseria atribuir ya xe também produzem y). Quando aplicamos esse conhecimento a if (match = re.exec(url)): Este A) faz a atribuição e B) retorna o resultado de re.exec(url)para o while. Agora re.execretorna nullse não houver correspondência, que é um valor falso. Portanto, o loop continuará enquanto houver uma correspondência.
Tomalak
67

Você precisa usar o botão 'g' para uma pesquisa global

var result = mystring.match(/(&|&)?([^=]+)=([^&]+)/g)
meouw
fonte
33
Na verdade, isso não resolve o problema: "O uso do sinalizador global 'g' corresponderá a todas as ocorrências, mas retornará apenas as sub-strings totalmente correspondentes, não as chaves e valores separados."
Adam Franco
40

2020 editar

Use URLSearchParams , pois esse trabalho não requer mais nenhum tipo de código personalizado. Os navegadores podem fazer isso por você com um único construtor:

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

rendimentos

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

Portanto, não há mais razão para usar o regex para isso.

Resposta original

Se você não deseja confiar na "correspondência cega" que vem com a execcorrespondência de estilos de execução , o JavaScript vem com a funcionalidade correspondente a todas incorporada, mas faz parte da replacechamada de função ao usar um "o que fazer com a captura" função de manipulação de grupos " :

var data = {};

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

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

feito.

Em vez de usar a função de tratamento do grupo de captura para realmente retornar seqüências de substituição (para o tratamento de substituição, o primeiro argumento é a correspondência completa de padrões e os argumentos subsequentes são grupos de captura individuais), simplesmente pegamos as capturas dos grupos 2 e 3 e colocamos em cache esse par.

Portanto, em vez de escrever funções de análise complicadas, lembre-se de que a função "matchAll" no JavaScript é simplesmente "substituir" por uma função manipuladora de substituição, e pode-se obter muita eficiência na correspondência de padrões.

Mike 'Pomax' Kamermans
fonte
Eu tenho uma corda something "this one" and "that one". Eu quero colocar todas as seqüências de caracteres duplas entre aspas em uma lista, ou seja, [este, aquele]. Até agora, mystring.match(/"(.*?)"/)funciona bem em detectar o primeiro, mas não sei como adaptar sua solução para um único grupo de captura.
nu everest
2
Parece que você deve postar uma pergunta no Stackoverflow para isso, em vez de tentar resolvê-la nos comentários.
Mike 'Pomax' Kamermans
Eu criei uma nova pergunta: stackoverflow.com/questions/26174122/…
nu everest
1
Não sei por que essa resposta tem tão poucos votos positivos, mas é a melhor resposta para a pergunta.
Calin
Olá @ Mike'Pomax'Kamermans, as diretrizes da comunidade recomendam especificamente a edição de entradas para aprimorá-las, consulte: stackoverflow.com/help/behavior . O núcleo de sua resposta é extremamente útil, mas achei que o idioma "lembre-se de que matchAll é substituir" não estava claro e não explicava por que seu código (que não é óbvio) funciona. Eu pensei que você deveria receber o merecido representante, então editei sua resposta em vez de duplicá-la com texto melhorado. Como o autor original desta pergunta, fico feliz em reverter a aceitação - desta resposta (e da edição), se você ainda quiser.
Adam Franco
21

Para capturar grupos, estou acostumado a usar preg_match_allno PHP e tentei replicar sua funcionalidade aqui:

<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
fonte
3
@teh_senaus, você precisa especificar o modificador global, /gcaso contrário a execução exec()não mudará o índice atual e fará um loop para sempre.
Aram Kocharyan
Se eu chamar para validar esse código myRe.test (str) e tentar executar execAll, ele estrelará na segunda partida e perdemos a primeira partida.
fdrv
@fdrv Você precisa redefinir o lastIndex para zero antes de iniciar o loop: this.lastIndex = 0;
CF
15

Defina o gmodificador para uma correspondência global:

/…/g
quiabo
fonte
11
Na verdade, isso não resolve o problema: "O uso do sinalizador global 'g' corresponderá a todas as ocorrências, mas retornará apenas as sub-strings totalmente correspondentes, não as chaves e valores separados."
Adam Franco
11

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

Localizando correspondências sucessivas

Se sua expressão regular usa o sinalizador "g", você pode usar o método exec () várias vezes para encontrar correspondências sucessivas na mesma string. Quando você faz isso, a pesquisa inicia na substring de str especificada pela propriedade lastIndex da expressão regular (test () também avançará na propriedade lastIndex). Por exemplo, suponha que você tenha 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 exibe o seguinte texto:

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

Nota: Não coloque a expressão regular literal (ou construtor RegExp) na condição while ou isso criará um loop infinito se houver uma correspondência devido à propriedade lastIndex sendo redefinida a cada iteração. Verifique também se o sinalizador global está definido ou se ocorrerá um loop aqui também.

KIM Taegyoon
fonte
Se eu chamar para validar esse código myRe.test (str) e tentar fazer isso, ele estrelará na segunda partida e perdemos a primeira partida.
fdrv
Você também pode combinar String.prototype.matchcom a gbandeira: 'abbcdefabh'.match(/ab*/g)retornos['abb', 'ab']
thom_nic
2

Se alguém (como eu) precisa do método Tomalak com suporte a array (ou seja, seleção múltipla), aqui 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(retornado anteriormente apenas: coisas)

fedu
fonte
1

Apenas para ficar com a pergunta proposta, conforme indicado pelo título, você pode realmente interagir com cada correspondência em uma string usando String.prototype.replace(). Por exemplo, o seguinte faz exatamente isso para obter uma matriz de todas as palavras com base em uma expressão 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"]

Se eu quisesse obter grupos de captura ou até o índice de cada partida, eu também poderia fazer isso. A seguir, mostramos como cada correspondência é retornada com a correspondência inteira, o 1º grupo de capturas e o í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?");

Depois de executar o acima, wordsserá o seguinte:

[
  {
    "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 combinar várias ocorrências semelhantes ao que está disponível no PHP, preg_match_allvocê pode usar esse tipo de pensamento para criar o seu próprio ou usar algo parecido YourJS.matchAll(). YourJS define mais ou menos essa função da seguinte maneira:

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
fonte
Como você deseja analisar a string de consulta de um URL, também pode usar algo como YourJS.parseQS()( yourjs.com/snippets/56 ), embora muitas outras bibliotecas também ofereçam essa funcionalidade.
Chris Oeste
Modificar uma variável de um escopo externo em um loop que deve retornar uma substituição é meio ruim. Seu uso indevido substitui aqui
Juan Mendes
1

Se você pode se dar bem usando mapesta é uma solução de quatro linhas:

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);

Não é bonito, não é eficiente, mas pelo menos é compacto. ;)

fboes
fonte
1

Use 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
fonte
1

Olá, a partir de 2020. Deixe-me chamar sua atenção a 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);
}

Saídas:

1111342 => Adam%20Franco
348572 => Bob%20Jones
Klesun
fonte
Finalmente! Uma observação: "O ECMAScript 2020, 11ª edição, introduz o método matchAll para Strings, para produzir um iterador para todos os objetos de correspondência gerados por uma expressão regular global" . De acordo com o site vinculado na resposta, a maioria dos navegadores e nodeJS o suportam atualmente, mas não o IE, Safari ou Samsung Internet. Esperamos que o suporte seja ampliado em breve, mas o YMMV por um tempo.
Adam Franco
0

Para capturar vários parâmetros usando o mesmo nome, modifiquei o loop while no método do Tomalak desta maneira:

  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

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

ivar
fonte
Embora eu goste da sua ideia, ela não funciona muito bem com parâmetros simples, como ?cinema=1234&film=12&film=34seria de esperar {cinema: 1234, film: [12, 34]}. Editou sua resposta para refletir isso.
TWIStErRob
0

Bem ... Eu tive um problema semelhante ... Quero uma pesquisa incremental / passo com o RegExp (por exemplo: iniciar pesquisa ... faça algum processamento ... continue a pesquisa até a última correspondência)

Depois de muita pesquisa na Internet ... como sempre (isso está se tornando um hábito agora), acabo no StackOverflow e encontrei a resposta ...

O que não é referido e o que é importante mencionar é " lastIndex" Agora entendo por que o objeto RegExp implementa a lastIndexpropriedade " "

ZEE
fonte
0

Dividir parece a melhor opção para mim:

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

Para evitar o inferno regular, você pode encontrar sua primeira correspondência, corte um pedaço e tente encontrar o próximo na substring. Em C #, isso se parece com isso, desculpe, eu não o transportei para JavaScript para você.

        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 pate
fonte