Come posso abbinare più ricorrenze con una regex in JavaScript simile a preg_match_all () di PHP?


160

Sto cercando di analizzare le stringhe con codifica URL composte da coppie chiave = valore separate da &o &.

Quanto segue corrisponderà solo alla prima occorrenza, suddividendo le chiavi e i valori in elementi di risultato separati:

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

I risultati per la stringa '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' sarebbero:

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

L'uso del flag globale, 'g', corrisponderà a tutte le occorrenze, ma restituirà solo le sottostringhe con corrispondenza completa, non le chiavi e i valori separati:

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

I risultati per la stringa '1111342 = Adam% 20Franco & 348572 = Bob% 20Jones' sarebbero:

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

Mentre potrei dividere la stringa &e spezzare ciascuna coppia chiave / valore singolarmente, esiste un modo per utilizzare il supporto delle espressioni regolari di JavaScript per abbinare più occorrenze del modello /(?:&|&)?([^=]+)=([^&]+)/simile alla preg_match_all()funzione di PHP ?

Sto cercando un modo per ottenere risultati con le sotto-partite separate come:

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

o

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

9
è un po 'strano che nessuno abbia raccomandato di usare replacequi. var data = {}; mystring.replace(/(?:&|&)?([^=]+)=([^&]+)/g, function(a,b,c,d) { data[c] = d; });fatto. "matchAll" in JavaScript è "sostituisci" con una funzione di gestione sostitutiva anziché una stringa.
Mike 'Pomax' Kamermans,

Nota che per coloro che continuano a trovare questa domanda nel 2020, la risposta è "non usare regex, usa URLSearchParams , che fa tutto questo per te".
Mike 'Pomax' Kamermans,

Risposte:


161

Sollevato dai commenti

Commento del 2020: piuttosto che usare regex, ora abbiamo URLSearchParams, che fa tutto questo per noi, quindi nessun codice personalizzato, per non parlare di regex, non è più necessario.

- Mike 'Pomax' Kamermans

Il supporto del browser è elencato qui https://caniuse.com/#feat=urlsearchparams


Suggerirei una regex alternativa, usando sottogruppi per catturare individualmente il nome e il valore dei parametri 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 è un oggetto:

{
  f: "q"
  codice geografico: ""
  hl: "de"
  cioè: "UTF8"
  iwloc: "addr"
  ll: "50.116616,8.680573"
  q: "Francoforte sul Meno"
  sll: "50.106047,8.679886"
  fonte: "s_q"
  spn: "0.35972,0.833588"
  sspn: "0.370369,0.833588"
  z: "11"
}

La regex si scompone come segue:

(?: # gruppo non acquisito
  \? | & # "?" o "&"
  (: Amp;?)? # (consenti "& amp;", per URL con codifica HTML errata)
) # fine gruppo non acquisito
( # gruppo 1
  [^ = & #] + # qualsiasi carattere tranne "=", "&" o "#"; almeno una volta
) # end group 1: questo sarà il nome del parametro
(?: # gruppo non acquisito
  =? # an "=", facoltativo
  (# gruppo 2
    [^ & #] * # qualsiasi carattere tranne "&" o "#"; qualsiasi numero di volte
  ) # end group 2 - questo sarà il valore del parametro
) # fine gruppo non acquisito

23
Questo è quello che speravo. Quello che non ho mai visto nella documentazione JavaScript è menzionare che il metodo exec () continuerà a restituire il prossimo set di risultati se chiamato più di una volta. Grazie ancora per l'ottimo consiglio!
Adam Franco,

1
Lo fa per questo: regular-expressions.info/javascript.html (Leggi: "Come usare l'oggetto JavaScript RegExp")
Tomalak,

1
c'è un bug in questo codice: il punto e virgola dopo il "while" dovrebbe essere rimosso.
Jan Willem B,

1
Perché generalmente utilizzo solo gruppi normali (ovvero catturare) se sono realmente interessato al loro contenuto.
Tomalak,

1
@KnightYoshi Sì. In JavaScript qualsiasi espressione produce il proprio risultato (come x = yassegnerebbe yal xe anche produrre y). Quando applichiamo tale conoscenza a if (match = re.exec(url)): Questa A) esegue il compito e B) restituisce il risultato re.exec(url)a while. Ora re.execritorna nullse non c'è corrispondenza, che è un valore falso. Quindi in effetti il ​​ciclo continuerà fino a quando c'è una corrispondenza.
Tomalak,

67

È necessario utilizzare l'interruttore 'g' per una ricerca globale

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

33
Questo in realtà non risolve il problema: "L'uso del flag globale, 'g', corrisponderà a tutte le occorrenze, ma restituirà solo le sottostringhe completamente abbinate, non le chiavi e i valori separati".
Adam Franco,

40

2020 modifica

Usa URLSearchParams , poiché questo lavoro non richiede più alcun tipo di codice personalizzato. I browser possono farlo per te con un solo costruttore:

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

i rendimenti

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

Quindi non c'è motivo di usare regex per questo.

Risposta originale

Se non si desidera fare affidamento sulla "corrispondenza cieca" fornita con la execcorrispondenza dello stile in esecuzione , JavaScript viene fornito con la funzionalità match-all integrata, ma fa parte della replacechiamata di funzione, quando si utilizza un "cosa fare con l'acquisizione gruppi " funzione di gestione :

var data = {};

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

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

fatto.

Invece di utilizzare la funzione di gestione dei gruppi di acquisizione per restituire effettivamente le stringhe di sostituzione (per la gestione delle sostituzioni, il primo argomento è la corrispondenza del modello completo e gli argomenti successivi sono singoli gruppi di acquisizione), prendiamo semplicemente i gruppi 2 e 3, e memorizziamo nella cache quella coppia.

Quindi, piuttosto che scrivere funzioni di analisi complicate, ricorda che la funzione "matchAll" in JavaScript è semplicemente "sostituisci" con una funzione di gestione sostitutiva, e si può avere molta efficienza nella corrispondenza dei pattern.


Ho una stringa something "this one" and "that one". Voglio mettere tutte le stringhe tra virgolette doppie in un elenco, cioè [questo, quello]. Finora mystring.match(/"(.*?)"/)funziona bene nel rilevare il primo, ma non so come adattare la tua soluzione per un singolo gruppo di acquisizione.
nu everest,

2
sembra che dovresti pubblicare una domanda su StackOverflow per questo, piuttosto che cercare di risolverlo nei commenti.
Mike 'Pomax' Kamermans,


1
Non sono sicuro del perché questa risposta abbia così pochi voti, ma è la migliore risposta alla domanda.
Calin,

Ciao @ Mike'Pomax'Kamermans, le linee guida della community raccomandano specificamente di modificare le voci per migliorarle, vedi: stackoverflow.com/help/behavior . Il nocciolo della tua risposta è estremamente utile, ma ho scoperto che il linguaggio "ricorda che matchAll è sostituito" non era chiaro e non era una spiegazione del perché il tuo codice (che non è ovvio) funzioni. Ho pensato che dovresti ottenere il meritato rappresentante, quindi ho modificato la tua risposta anziché duplicarla con un testo migliorato. Come originale richiedente di questa domanda, sono felice di ripristinare l'accettazione - di questa risposta (e della modifica) se vuoi ancora che lo faccia.
Adam Franco,

21

Per i gruppi di acquisizione, sono abituato a utilizzare preg_match_allin PHP e ho provato a replicare la sua funzionalità qui:

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

3
@teh_senaus è necessario specificare il modificatore globale con /galtrimenti in esecuzione exec()non cambierà l'indice corrente e si ripeterà per sempre.
Aram Kocharyan,

Se chiamo per convalidare questo codice myRe.test (str) e quindi provo a eseguire execAll, inizia con il secondo match e abbiamo perso il primo match.
fdrv,

@fdrv Devi reimpostare lastIndex su zero prima di iniziare il ciclo: this.lastIndex = 0;
CF,

15

Imposta il gmodificatore per una corrispondenza globale:

/…/g

11
Questo in realtà non risolve il problema: "L'uso del flag globale, 'g', corrisponderà a tutte le occorrenze, ma restituirà solo le sottostringhe completamente abbinate, non le chiavi e i valori separati".
Adam Franco,

11

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

Trovare corrispondenze successive

Se la tua espressione regolare usa il flag "g", puoi usare il metodo exec () più volte per trovare corrispondenze successive nella stessa stringa. Quando lo fai, la ricerca inizia dalla sottostringa di str specificata dalla proprietà lastIndex dell'espressione regolare (test () farà avanzare anche la proprietà lastIndex). Ad esempio, supponi di avere questo 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);
}

Questo script visualizza il seguente testo:

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

Nota: non posizionare l'espressione regolare letterale (o il costruttore RegExp) nella condizione while o creerà un ciclo infinito se esiste una corrispondenza a causa del ripristino della proprietà lastIndex su ogni iterazione. Assicurati anche che sia impostato il flag globale o che qui si verifichi un loop.


Se chiamo per convalidare questo codice myRe.test (str) e poi provo a farlo, inizia con la seconda partita e abbiamo perso la prima partita.
fdrv,

È inoltre possibile combinare String.prototype.matchcon la gbandiera: 'abbcdefabh'.match(/ab*/g)i rendimenti['abb', 'ab']
thom_nic

2

Se qualcuno (come me) ha bisogno del metodo di Tomalak con supporto di array (cioè selezione multipla), eccolo qui:

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

ingresso ?my=1&my=2&my=things

risultato 1,2,things(solo restituito in precedenza: cose)


1

Solo per attenersi alla domanda proposta come indicato dal titolo, puoi effettivamente scorrere ogni corrispondenza in una stringa usando String.prototype.replace(). Ad esempio, ciò che segue fa proprio questo per ottenere un array di tutte le parole basato su un'espressione regolare:

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 volessi ottenere gruppi di acquisizione o anche l'indice di ogni partita, potrei farlo anch'io. Di seguito viene mostrato come ogni partita viene restituita con l'intera partita, il primo gruppo di acquisizione e l'indice:

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

Dopo aver eseguito quanto sopra, wordssarà il seguente:

[
  {
    "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
  }
]

Per abbinare più occorrenze simili a quelle disponibili in PHP preg_match_all, puoi usare questo tipo di pensiero per crearne uno tuo o usare qualcosa di simile YourJS.matchAll(). YourJS definisce più o meno questa funzione come segue:

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

Poiché desideri analizzare la stringa di query di un URL, puoi anche utilizzare qualcosa di simile YourJS.parseQS()( yourjs.com/snippets/56 ), anche se molte altre librerie offrono anche questa funzionalità.
Chris West,

Modificare una variabile da un ambito esterno in un ciclo che dovrebbe restituire una sostituzione è in qualche modo negativo. Il tuo uso improprio sostituisce qui
Juan Mendes il

1

Se riesci a cavartela usando mapquesta è una soluzione a quattro righe:

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

Non è carino, non è efficiente, ma almeno è compatto. ;)


1

Utilizzare 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"]]

1

Ciao dal 2020. Vorrei portare String.prototype.matchAll () alla tua attenzione:

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

Uscite:

1111342 => Adam%20Franco
348572 => Bob%20Jones

Finalmente! Una nota di cautela: "ECMAScript 2020, l'undicesima edizione, introduce il metodo matchAll per le stringhe, per produrre un iteratore per tutti gli oggetti di corrispondenza generati da un'espressione regolare globale" . Secondo il sito collegato nella risposta, la maggior parte dei browser e nodeJS lo supportano attualmente, ma non IE, Safari o Samsung Internet. Speriamo che il supporto si allarghi presto, ma YMMV per un po '.
Adam Franco,

0

Per acquisire diversi parametri usando lo stesso nome, ho modificato il ciclo while nel metodo di Tomalak in questo modo:

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

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

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


Mentre mi piace la tua idea, non funziona bene con i singoli parametri, come ?cinema=1234&film=12&film=34mi aspetterei {cinema: 1234, film: [12, 34]}. Modificata la tua risposta per riflettere questo.
TWiStErRob

0

Bene ... ho avuto un problema simile ... Voglio una ricerca incrementale / a passi con RegExp (es: avvia la ricerca ... fai un po 'di elaborazione ... continua la ricerca fino all'ultima corrispondenza)

Dopo molte ricerche su Internet ... come sempre (questo sta diventando un'abitudine ora) finisco in StackOverflow e ho trovato la risposta ...

Ciò che non viene menzionato e le cose da menzionare è " lastIndex" Ora capisco perché l'oggetto RegExp implementa la lastIndexproprietà " "


0

Dividerlo sembra la migliore opzione per me:

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

0

Per evitare l'inferno regex potresti trovare il tuo primo incontro, tagliare un pezzo e poi provare a trovare quello successivo sulla sottostringa. In C # sembra qualcosa del genere, mi dispiace non averlo trasferito su JavaScript per te.

        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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.