2011-01-14 18 views
28

ho una stringa:Regex con i gruppi di cattura denominati ottenere tutte le partite in Ruby

s="123--abc,123--abc,123--abc" 

Ho provato ad utilizzare nuova funzionalità Ruby 1.9 di "gruppi denominati" a prendere tutte le informazioni sul gruppo denominato:

/(?<number>\d*)--(?<chars>\s*)/ 

Esiste un'API come Python findall che restituisce una raccolta matchdata? In questo caso ho bisogno di restituire due incontri, perché 123 e abc ripetono due volte. Ogni dato di corrispondenza contiene dettagli di ciascuna informazione di acquisizione con nome in modo da poter utilizzare m['number'] per ottenere il valore della corrispondenza.

risposta

29

Le immagini con nome sono adatte solo per un risultato corrispondente.
L'analogico di Ruby di findall è String#scan. È possibile utilizzare scan risultato come un array, o di passare un blocco ad esso:

irb> s = "123--abc,123--abc,123--abc" 
=> "123--abc,123--abc,123--abc" 

irb> s.scan(/(\d*)--([a-z]*)/) 
=> [["123", "abc"], ["123", "abc"], ["123", "abc"]] 

irb> s.scan(/(\d*)--([a-z]*)/) do |number, chars| 
irb*  p [number,chars] 
irb> end 
["123", "abc"] 
["123", "abc"] 
["123", "abc"] 
=> "123--abc,123--abc,123--abc" 
+0

/(\ d *) - ([az] *)/se uso queste regex, come posso ottenere la stringa di corrispondenza completa, in questo caso è ['123 - abc', '123 - abc '], quindi posso creare i dati di corrispondenza per ciascun elemento da solo – mlzboy

+0

@mlzboy, ci sono due soluzioni. Più semplice di loro è quello di aggiungere il terzo gruppo regex: '/ ((\ d *) - ([az] *))/do | tutti, numero, chars |' – Nakilon

+2

grazie, a quanto pare il rubino non ha il supporto il pozzetto di cattura denominato – mlzboy

2

@Nakilon è corretto che mostra scan con una regex, tuttavia non c'è nemmeno bisogno di avventurarsi in terreni regex se si don' t vuole:

s = "123--abc,123--abc,123--abc" 
s.split(',') 
#=> ["123--abc", "123--abc", "123--abc"] 

s.split(',').inject([]) { |a,s| a << s.split('--'); a } 
#=> [["123", "abc"], ["123", "abc"], ["123", "abc"]] 

Ciò restituisce un array di array, che è conveniente se si dispone di più occorrenze e bisogno di vedere/processo di tutti.

s.split(',').inject({}) { |h,s| n,v = s.split('--'); h[n] = v; h } 
#=> {"123"=>"abc"} 

Questo restituisce un hash, che, poiché gli elementi hanno la stessa chiave, ha solo il valore di chiave univoco. Questo è buono quando hai un mazzo di chiavi duplicate ma vuoi quelle uniche. Lo svantaggio si verifica se sono necessari i valori univoci associati alle chiavi, ma sembra essere una domanda diversa.

+0

'Hash [s.split (", "). Map {| i | i.split ("-")}] ' – Nakilon

2

Un anno fa ho voluto espressioni regolari che erano più facili da leggere e chiamato le catture, così ho fatto la seguente aggiunta a String (non dovrebbe forse essere lì, ma era comodo, al momento):

scan2.rb:

class String 
    #Works as scan but stores the result in a hash indexed by variable/constant names (regexp PLACEHOLDERS) within parantheses. 
    #Example: Given the (constant) strings BTF, RCVR and SNDR and the regexp /#BTF# (#RCVR#) (#SNDR#)/ 
    #the matches will be returned in a hash like: match[:RCVR] = <the match> and match[:SNDR] = <the match> 
    #Note: The #STRING_VARIABLE_OR_CONST# syntax has to be used. All occurences of #STRING# will work as #{STRING} 
    #but is needed for the method to see the names to be used as indices. 
    def scan2(regexp2_str, mark='#') 
    regexp    = regexp2_str.to_re(mark)      #Evaluates the strings. Note: Must be reachable from here! 
    hash_indices_array = regexp2_str.scan(/\(#{mark}(.*?)#{mark}\)/).flatten #Look for string variable names within (#VAR#) or # replaced by <mark> 
    match_array   = self.scan(regexp) 

    #Save matches in hash indexed by string variable names: 
    match_hash = Hash.new 
    match_array.flatten.each_with_index do |m, i| 
     match_hash[hash_indices_array[i].to_sym] = m 
    end 
    return match_hash 
    end 

    def to_re(mark='#') 
    re = /#{mark}(.*?)#{mark}/ 
    return Regexp.new(self.gsub(re){eval $1}, Regexp::MULTILINE) #Evaluates the strings, creates RE. Note: Variables must be reachable from here! 
    end 

end 

Esempio di utilizzo (irb1.9):

> load 'scan2.rb' 
> AREA = '\d+' 
> PHONE = '\d+' 
> NAME = '\w+' 
> "1234-567890 Glenn".scan2('(#AREA#)-(#PHONE#) (#NAME#)') 
=> {:AREA=>"1234", :PHONE=>"567890", :NAME=>"Glenn"} 

Note:

Ovviamente sarebbe stato più elegante mettere i modelli (ad es. AREA, TELEFONO ...) in un hash e aggiungi questo hash con i pattern agli argomenti di scan2.

2

Se si utilizza ruby> = 1.9 e le catture di nome, si potrebbe:

class String 
    def scan2(regexp2_str, placeholders = {}) 
    return regexp2_str.to_re(placeholders).match(self) 
    end 

    def to_re(placeholders = {}) 
    re2 = self.dup 
    separator = placeholders.delete(:SEPARATOR) || '' #Returns and removes separator if :SEPARATOR is set. 
    #Search for the pattern placeholders and replace them with the regex 
    placeholders.each do |placeholder, regex| 
     re2.sub!(separator + placeholder.to_s + separator, "(?<#{placeholder}>#{regex})") 
    end  
    return Regexp.new(re2, Regexp::MULTILINE) #Returns regex using named captures. 
    end 
end 

Usage (ruby> = 1.9):

> "1234:Kalle".scan2("num4:name", num4:'\d{4}', name:'\w+') 
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle"> 

o

> re="num4:name".to_re(num4:'\d{4}', name:'\w+') 
=> /(?<num4>\d{4}):(?<name>\w+)/m 

> m=re.match("1234:Kalle") 
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle"> 
> m[:num4] 
=> "1234" 
> m[:name] 
=> "Kalle" 

Utilizzando l'opzione di separazione:

> "1234:Kalle".scan2("#num4#:#name#", SEPARATOR:'#', num4:'\d{4}', name:'\w+') 
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle"> 
7

è possibile estrarre le variabili utilizzate dal regexp utilizzando names metodo. Quindi quello che ho fatto è che ho usato il metodo regolare scan per ottenere le partite, quindi i nomi zippati e ogni partita per creare un Hash.

class String 
    def scan2(regexp) 
    names = regexp.names 
    scan(regexp).collect do |match| 
     Hash[names.zip(match)] 
    end 
    end 
end 

Usage:

>> "aaa http://www.google.com.tr aaa https://www.yahoo.com.tr ddd".scan2 /(?<url>(?<protocol>https?):\/\/[\S]+)/ 
=> [{"url"=>"http://www.google.com.tr", "protocol"=>"http"}, {"url"=>"https://www.yahoo.com.tr", "protocol"=>"https"}] 
0

Mi è piaciuto molto @ La soluzione di Umut-Utkan, ma non ha abbastanza di fare quello che volevo così ho riscritto un po '(nota, il seguito non potrebbe essere bella codice, ma sembra funzionare)

class String 
    def scan2(regexp) 
    names = regexp.names 
    captures = Hash.new 
    scan(regexp).collect do |match| 
     nzip = names.zip(match) 
     nzip.each do |m| 
     captgrp = m[0].to_sym 
     captures.add(captgrp, m[1]) 
     end 
    end 
    return captures 
    end 
end 

Ora, se si fa

p '12f3g4g5h5h6j7j7j'.scan2(/(?<alpha>[a-zA-Z])(?<digit>[0-9])/) 

Si ottiene

{:alpha=>["f", "g", "g", "h", "h", "j", "j"], :digit=>["3", "4", "5", "5", "6", "7", "7"]} 

(es. tutti i caratteri alfa trovati in un array e tutte le cifre trovate in un altro array). A seconda del tuo scopo per la scansione, questo potrebbe essere utile. Ad ogni modo, mi piace vedere esempi di quanto sia facile riscrivere o estendere la funzionalità di base di Ruby con poche righe!

2

Ho avuto bisogno di qualcosa di simile di recente. Questo dovrebbe funzionare come String#scan, ma restituire invece una matrice di oggetti MatchData.

class String 
    # This method will return an array of MatchData's rather than the 
    # array of strings returned by the vanilla `scan`. 
    def match_all(regex) 
    match_str = self 
    match_datas = [] 
    while match_str.length > 0 do 
     md = match_str.match(regex) 
     break unless md 
     match_datas << md 
     match_str = md.post_match 
    end 
    return match_datas 
    end 
end 

Esecuzione dei dati di esempio nei risultati REPL nella seguente:

> "123--abc,123--abc,123--abc".match_all(/(?<number>\d*)--(?<chars>[a-z]*)/) 
=> [#<MatchData "123--abc" number:"123" chars:"abc">, 
    #<MatchData "123--abc" number:"123" chars:"abc">, 
    #<MatchData "123--abc" number:"123" chars:"abc">] 

Si può anche trovare il mio codice di prova utili:

describe String do 
    describe :match_all do 
    it "it works like scan, but uses MatchData objects instead of arrays and strings" do 
     mds = "ABC-123, DEF-456, GHI-098".match_all(/(?<word>[A-Z]+)-(?<number>[0-9]+)/) 
     mds[0][:word].should == "ABC" 
     mds[0][:number].should == "123" 
     mds[1][:word].should == "DEF" 
     mds[1][:number].should == "456" 
     mds[2][:word].should == "GHI" 
     mds[2][:number].should == "098" 
    end 
    end 
end 
+0

Personalmente farei appartenere alla classe Regexp, ma questa è comunque una soluzione molto bella. Sorpreso, questo non è un metodo fondamentale. –

17

Chiming in super-ritardo, ma qui sta un modo semplice di replicare la scansione # della stringa, ma ottenendo invece il matchdata:

matches = [] 
foo.scan(regex){ matches << $~ } 

matches contiene ora gli oggetti MatchData che corrispondono alla scansione della stringa.

+4

'$ LAST_MATCH_INFO' è l'analoga variabile" inglese "per' $ ~ ', spero che salvi alcuni googling per le persone che vogliono un codice più leggibile. – Michael

+1

Tutto si riduce alle preferenze (o guide di stile!), Ma qualcuno direbbe '$ ~' è più leggibile di '$ LAST_MATCH_DATA'. '$ ~' ha due cose da fare: è più succinto (2 caratteri contro 12) e la sua tilde segue il modello dell'operatore di corrispondenza dell'espressione regolare, '= ~'. Naturalmente, dovresti sempre codificare ciò che il tuo pubblico (colleghi, futuro-tu) capirà facilmente. Non riesco mai a ricordare cosa fanno molte di queste variabili $: '$;', '$ &', e '$ @'? Chi può dire senza guardarli? :-) –

+0

Come funziona? Cosa sta assicurando che '$ ~' continui a cambiare per ogni iterazione? –

0

Mi piace il match_all dato da John, ma penso che abbia un errore.

La linea:

match_datas << md 

opere se non ci sono catture() nella regex.

Questo codice fornisce l'intera linea fino a includere il modello abbinato/catturato dalla regex. (La parte [0] di MatchData) Se regex ha capture(), allora questo risultato probabilmente non è ciò che l'utente (me) desidera nell'output finale.

Credo nel caso in cui ci sono catture() nella regex, il codice corretto dovrebbe essere:

match_datas << md[1] 

L'eventuale uscita di match_datas sarà un array di cattura modello corrisponde partire da match_datas [0] . Questo non è proprio quello che ci si può aspettare se un normale MatchData si vuole che include un match_datas [0] Valore che è tutto abbinato stringa seguito da match_datas [1], match_datas [[2], .. che sono le catture (se qualsiasi) nel modello regex.

Le cose sono complesse, il che potrebbe essere il motivo per cui match_all non è stato incluso in MatchData nativo.

0

Piggybacking off di risposta di Marco Hubbart, ho aggiunto il seguente scimmia-patch:

class ::Regexp 
    def match_all(str) 
    matches = [] 
    str.scan(self) { matches << $~ } 

    matches 
    end 
end 

che può essere utilizzato come /(?<letter>\w)/.match_all('word'), e ritorni:

[#<MatchData "w" letter:"w">, #<MatchData "o" letter:"o">, #<MatchData "r" letter:"r">, #<MatchData "d" letter:"d">]

Questo si basa su, come altri hanno detto, l'uso di $~ nel blocco di scansione per i dati della partita.

Problemi correlati