JavaRush /Java Blog /Random-IT /RegEx: 20 brevi passaggi per padroneggiare le espressioni...
Artur
Livello 40
Tallinn

RegEx: 20 brevi passaggi per padroneggiare le espressioni regolari. Parte 3

Pubblicato nel gruppo Random-IT
RegEx: 20 brevi passaggi per padroneggiare le espressioni regolari. Parte 1. RegEx: 20 brevi passaggi per padroneggiare le espressioni regolari. Parte 2: In questa parte passeremo a cose un po' più complesse. Ma padroneggiarli, come prima, non sarà difficile. Ripeto che RegEx è in realtà più semplice di quanto possa sembrare a prima vista, e non è necessario essere uno scienziato missilistico per padroneggiarlo e iniziare a usarlo nella pratica. L'originale inglese di questo articolo è qui . 20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 1

Passaggio 11: parentesi ()come gruppi di acquisizione

20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 2Nell'ultimo problema, abbiamo cercato diversi tipi di valori interi e valori numerici in virgola mobile (punto). Ma il motore delle espressioni regolari non distingueva tra questi due tipi di valori, poiché tutto veniva catturato in un'unica grande espressione regolare. Possiamo dire al motore delle espressioni regolari di distinguere tra diversi tipi di corrispondenze se racchiudiamo i nostri mini-pattern tra parentesi:
pattern: ([AZ])|([az]) 
string:   L'attuale presidente della Bolivia è Evo Morales .
corrispondenze: ^^^ ^^^^^^^ ^^^^^^^^^ ^^ ^^^^^^^ ^^ ^^^ ^^^^^^^ 
gruppo:    122 2222222 122222222 22 1222222 22 122 1222222  
( Esempio ) L'espressione regolare precedente definisce due gruppi di acquisizione indicizzati a partire da 1. Il primo gruppo di acquisizione corrisponde a qualsiasi singola lettera maiuscola e il secondo gruppo di acquisizione corrisponde a qualsiasi singola lettera minuscola. Utilizzando il segno "o" |e le parentesi ()come gruppo di acquisizione, possiamo definire una singola espressione regolare che corrisponde a più tipi di stringhe. Se applichiamo questo alla nostra regex di ricerca long/float della parte precedente dell'articolo, il motore regex catturerà le corrispondenze corrispondenti nei gruppi appropriati. Controllando a quale gruppo corrisponde una sottostringa, possiamo immediatamente determinare se si tratta di un valore float o di un valore long:
modello: (\d*\.\d+[fF]|\d+\.\d*[fF]|\d+[fF])|(\d+[lL]) 
stringa:   42L 12 x 3.4f 6l 3.3 0F LF .2F0 .
partite: ^^^ ^^^^ ^^ ^^ ^^^ 
gruppo:    222 1111 22 11 111  
( Esempio ) Questa espressione regolare è piuttosto complessa e, per capirla meglio, analizziamola e osserviamo ciascuno di questi modelli:
( // corrisponde a qualsiasi sottostringa "float".
  \d*\.\d+[fF]
  |
  \d+\.\d*[fF]
  |
  \d+[fF]
)
| //O
( // corrisponde a qualsiasi sottostringa "lunga".
  \d+[lL]
)
Il segno |e i gruppi di acquisizione tra parentesi ()ci consentono di abbinare diversi tipi di sottostringhe. In questo caso, stiamo confrontando numeri in virgola mobile "float" o interi long "long".
(
  \d*\.\d+[fF] // 1+ cifre a destra del punto decimale
  |
  \d+\.\d*[fF] // 1+ cifre a sinistra del punto decimale
  |
  \d+[fF] // nessun punto, solo 1+ cifre
)
|
(
  \d+[lL] // nessun punto, solo 1+ cifre
)
Nel gruppo di cattura "float", abbiamo tre opzioni: numeri con almeno 1 cifra a destra del punto decimale, numeri con almeno 1 cifra a sinistra del punto decimale e numeri senza punto decimale. Tutti sono "float" purché abbiano le lettere "f" o "F" aggiunte alla fine. All'interno del gruppo di cattura "lungo" abbiamo solo un'opzione: dobbiamo avere 1 o più cifre seguite dal carattere "l" o "L". Il motore delle espressioni regolari cercherà queste sottostringhe in una determinata stringa e le indicizzerà nel gruppo di acquisizione appropriato. Notache non stiamo corrispondendo a nessuno dei numeri a cui non è stato aggiunto alcun "l", "L", "f" o "F". Come dovrebbero essere classificati questi numeri? Bene, se hanno un punto decimale, il linguaggio Java per impostazione predefinita è "double". Altrimenti devono essere "int".

Consolidiamo ciò che abbiamo imparato con un paio di enigmi:

Aggiungi altri due gruppi di acquisizione alla regex precedente in modo che classifichi anche i numeri double o int. (Questa è un'altra domanda complicata, non scoraggiarti se ci vuole un po' di tempo, come ultima risorsa guarda la mia soluzione.)
modello:
stringa:   42L 12 x 3.4f 6l 3.3 0F LF .2F 0. 
corrisponde a: ^^^ ^^ ^^^^ ^^ ^^^ ^^ ^^^ ^^ 
gruppo:    333 44 1111 33 222 11 111 22
( Soluzione ) Il problema successivo è un po' più semplice. Utilizza i gruppi di acquisizione tra parentesi (), il segno "o" |e gli intervalli di caratteri per ordinare le seguenti età: "bere legale negli Stati Uniti". (>= 21) e "non è consentito bere negli USA" (<21):
modello:
stringa:   7 10 17 18 19 20 21 22 23 24 30 40 100 120 
corrispondenze: ^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^^ ^^^ 
gruppo:    2 22 22 22 22 22 11 11 11 11 11 11 111 111 
( Soluzione )

Passaggio 12: identificare prima le corrispondenze più specifiche

20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 3Potresti aver avuto qualche problema con l'ultima attività se avessi provato a definire i "bevitori legali" come il primo gruppo di cattura anziché il secondo. Per capire perché, guardiamo un altro esempio. Supponiamo di voler registrare separatamente cognomi contenenti meno di 4 caratteri e cognomi contenenti 4 o più caratteri. Diamo nomi più brevi al primo gruppo di acquisizione e vediamo cosa succede:
modello: ([AZ] [az]?[az]?)|([AZ] [az] [az] +) 
stringa:   Kim Job s Xu Clo yd Moh r Ngo Roc k.
partite: ^^^ ^^^ ^^ ^^^ ^^^ ^^^ ^^^ 
gruppo:    111 111 11 111 111 111 111   
( Esempio ) Per impostazione predefinita, la maggior parte dei motori di espressioni regolari utilizza la corrispondenza greedy con i caratteri di base che abbiamo visto finora. Ciò significa che il motore delle espressioni regolari acquisirà il gruppo più lungo definito il prima possibile nell'espressione regolare fornita. Quindi, anche se il secondo gruppo di cui sopra potrebbe catturare più caratteri in nomi come "Jobs" e "Cloyd", ad esempio, ma poiché i primi tre caratteri di quei nomi sono già stati catturati dal primo gruppo di cattura, non possono essere catturati nuovamente dal secondo . Ora facciamo una piccola correzione: cambiamo semplicemente l'ordine dei gruppi di acquisizione, posizionando per primo il gruppo più specifico (più lungo):
modello: ([AZ] [az] [az]+)|([AZ]?[az]?) 
stringa:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
corrispondenze: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
gruppo:    222 1111 22 11111 1111 222 1111    
( Esempio )

Compito... questa volta solo uno :)

Un modello "più specifico" significa quasi sempre "più lungo". Diciamo che vogliamo trovare due tipi di "parole": prima quelle che iniziano con vocali (più specificatamente), poi quelle che non iniziano con vocali (qualsiasi altra parola). Prova a scrivere un'espressione regolare per acquisire e identificare le stringhe che corrispondono a questi due gruppi. (I gruppi seguenti sono scritti con lettere anziché numerati. È necessario determinare quale gruppo deve corrispondere al primo e quale al secondo.)
modello:
stringa:   pds6f uub 24r2gp ewqrty l ui_op 
corrisponde a: ^^^^^ ^^^ ^^^^^^ ^^^^^^ ^ ^^^^^ 
gruppo:    NNNNN VVV NNNNNN VVVVVV N VVVVV
( Soluzione ) In generale, più precisa è la tua espressione regolare, più lunga sarà la sua durata. E più è accurato, meno è probabile che catturi qualcosa di cui non hai bisogno. Quindi, anche se possono sembrare spaventosi, le espressioni regolari più lunghe ~= espressioni regolari migliori. Purtroppo .

Passaggio 13: parentesi graffe {}per un numero specifico di ripetizioni

20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 4Nell'esempio con i cognomi del passaggio precedente, avevamo 2 gruppi quasi ripetitivi in ​​uno schema:
modello: ([AZ] [az] [az]+)|([AZ]?[az]?) 
stringa:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
corrispondenze: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
gruppo:    222 1111 22 11111 1111 222 1111    
Per il primo gruppo avevamo bisogno di cognomi con quattro o più lettere. Il secondo gruppo doveva catturare cognomi con tre o meno lettere. Esiste un modo più semplice per scrivere questo che ripetere questi [a-z]gruppi ancora e ancora? Esiste se si utilizzano le parentesi graffe per questo {}. Le parentesi graffe {}ci consentono di specificare il numero minimo e (facoltativo) massimo di corrispondenze del carattere o gruppo di cattura precedente. I casi d'uso sono tre {}:
{X} // corrisponde esattamente X volte
{X,} // corrisponde >= X volte
{X,Y} // corrisponde a >= X e <= Y volte
Ecco alcuni esempi di queste tre diverse sintassi:
modello: [az]{11} 
stringa:   humuhumunuk unukuapua'a.
corrispondenze: ^^^^^^^^^^^   
( Esempio )
modello: [az]{18,} 
stringa:   humuhumunukunukuapua 'a.
corrispondenze: ^^^^^^^^^^^^^^^^^^^^^^    
( Esempio )
modello: [az]{11,18} 
stringa:   humuhumunukunukuap ua'a.
corrispondenze: ^^^^^^^^^^^^^^^^^^^    
( Esempio ) Ci sono diversi punti da notare negli esempi precedenti.Nota:. Innanzitutto, utilizzando la notazione {X}, il carattere o il gruppo precedente corrisponderà esattamente a quel numero (X) di volte. Se ci sono più caratteri nella "parola" (rispetto al numero X) che potrebbero corrispondere al modello (come mostrato nel primo esempio), allora non verranno inclusi nella corrispondenza. Se il numero di caratteri è inferiore a X, la corrispondenza completa fallirà (prova a modificare 11 in 99 nel primo esempio). In secondo luogo, le notazioni {X,} e {X,Y} sono avide. Cercheranno di abbinare quanti più caratteri possibile pur soddisfacendo l'espressione regolare data. Se specifichi {3,7} allora possono essere abbinati da 3 a 7 caratteri e se i successivi 7 caratteri sono validi allora verranno abbinati tutti e 7 i caratteri. Se specifichi {1,} e tutti i successivi 14.000 caratteri corrispondono, tutti i 14.000 caratteri verranno inclusi nella stringa corrispondente. Come possiamo usare questa conoscenza per riscrivere la nostra espressione sopra? Il miglioramento più semplice potrebbe essere quello di sostituire i gruppi vicini [a-z]con [a-z]{N}, dove N viene scelto di conseguenza:
modello: ([AZ][az]{2}[az]+)|([AZ][az]?[az]?)  
...ma questo non migliora molto le cose. Osserva il primo gruppo di acquisizione: abbiamo [a-z]{2}(che corrisponde esattamente a 2 lettere minuscole) seguito da [a-z]+(che corrisponde a 1 o più lettere minuscole). Possiamo semplificarlo chiedendo 3 o più lettere minuscole utilizzando le parentesi graffe:
modello: ([AZ][az]{3,})|([AZ][az]?[az]?) 
Il secondo gruppo di acquisizione è diverso. Non abbiamo bisogno di più di tre caratteri in questi cognomi, il che significa che abbiamo un limite superiore, ma il nostro limite inferiore è zero:
modello: ([AZ][az]{3,})|([AZ][az]{0,2}) 
La specificità è sempre migliore quando si utilizzano le espressioni regolari, quindi sarebbe saggio fermarsi qui, ma non posso fare a meno di notare che questi due intervalli di caratteri ( [AZ]e [az]) uno accanto all'altro sembrano quasi una classe di "caratteri di parole", \w( [A-Za-z0-9_]) . Se fossimo sicuri che i nostri dati contenessero solo cognomi ben formattati, allora potremmo semplificare la nostra espressione regolare e scrivere semplicemente:
modello: (\w{4,})|(\w{1,3}) 
Il primo gruppo cattura qualsiasi sequenza di 4 o più "caratteri verbali" ( [A-Za-z0-9_]), mentre il secondo gruppo cattura qualsiasi sequenza da 1 a 3 "caratteri verbali" (inclusi). Funzionerà?
modello: (\w{4,})|(\w{1,3}) 
stringa:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
corrispondenze: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
gruppo:    222 1111 22 11111 1111 222 1111    
( Esempio ) Ha funzionato! Che ne dici di questo approccio? Ed è molto più pulito del nostro esempio precedente. Poiché il primo gruppo di cattura corrisponde a tutti i cognomi con quattro o più caratteri, potremmo anche modificare il secondo gruppo di cattura semplicemente in \w+, poiché ciò ci consentirebbe di catturare tutti i cognomi rimanenti (con 1, 2 o 3 caratteri):
modello: (\w{4,})|(\w+) 
stringa:   Kim Jobs Xu Cloyd Mohr Ngo Rock .
corrispondenze: ^^^ ^^^^ ^^ ^^^^^ ^^^^ ^^^ ^^^^ 
gruppo:    222 1111 22 11111 1111 222 1111    
( Esempio )

Aiutiamo il cervello a imparare questo e a risolvere i seguenti 2 problemi:

Utilizza le parentesi graffe {}per riscrivere l'espressione regolare di ricerca del numero di previdenza sociale del passaggio 7:
modello:
stringa: 113-25=1902 182-82-0192 H23-_3-9982 1I1-O0-E38B
corrispondenze:              ^^^^^^^^^^^
( Soluzione ) Si supponga che il controllo della sicurezza della password di un sito Web richieda che le password degli utenti contengano tra 6 e 12 caratteri. Scrivi un'espressione regolare che contrassegni le password non valide nell'elenco seguente. Ogni password è racchiusa tra parentesi ()per facilitarne la corrispondenza, quindi assicurati che l'espressione regolare inizi e termini con caratteri letterali (e )simbolici. Suggerimento: assicurati di non consentire parentesi letterali nelle password con [^()]o simili, altrimenti finirai per far corrispondere l'intera stringa!
modello:
stringa:   (12345) (la mia password) (Xanadu.2112) (su_do) (OfSalesmen!)
corrispondenze: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^  
( Soluzione )

Passaggio 14: \bsimbolo del bordo con larghezza zero

20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 5L'ultimo compito è stato piuttosto difficile. Ma cosa succederebbe se rendessimo il tutto un po' più complicato racchiudendo le password tra virgolette ""invece che tra parentesi ()? Possiamo scrivere una soluzione simile semplicemente sostituendo tutti i caratteri tra parentesi con caratteri di virgolette?
pattern: \"[^"]{0.5}\"|\"[^"]+\s[^"]*\" 
stringa:   "12345" "la mia password" " Xanadu.2112 " " su_do" " OfSalesmen! "
corrispondenze: ^^^^^^^ ^^^^^^^^^^^^^ ^^^ ^^^  
( Esempio ) Il risultato non è stato molto impressionante. Hai già indovinato perché? Il problema è che qui stiamo cercando password errate. "Xanadu.2112" è una buona password, quindi quando la regex si rende conto che questa sequenza non contiene spazi o caratteri letterali ", restituisce subito prima del carattere "che qualifica la password sul lato destro. (Perché abbiamo specificato che i caratteri "non possono essere trovati all'interno delle password utilizzando [^"].) Una volta che il motore delle espressioni regolari si accerta che quei caratteri non corrispondono a una particolare espressione regolare, viene eseguito di nuovo, esattamente da dove si era interrotto, ovvero dove si trovava il carattere ". che limita " Xanadu.2112" sulla destra. Da lì vede uno spazio e un altro carattere ": per lui questa è la password sbagliata! Fondamentalmente, trova questa sequenza " "e va avanti. Questo non è affatto quello che vorremmo ottenere... Sarebbe fantastico se potessimo specificare che il primo carattere della password non dovrebbe essere uno spazio. C'è un modo per fare questo? (Ormai probabilmente avrai capito che la risposta a tutte le mie domande retoriche è "sì".) Sì! Esiste un modo! Molti motori di espressioni regolari forniscono una sequenza di escape come "word border" \b. "Confine di parola" \bè una sequenza di escape di larghezza zero che, stranamente, corrisponde a un confine di parola. Ricorda che quando diciamo "parola", intendiamo qualsiasi sequenza di caratteri nella classe \wo [A-Za-z0-9_]. Una corrispondenza dei limiti di parola significa che il carattere immediatamente prima o immediatamente dopo la sequenza \bdeve essere неun carattere di parola. Tuttavia, durante la corrispondenza, non includiamo questo carattere nella nostra sottostringa catturata. Questa è larghezza zero. Per vedere come funziona, diamo un'occhiata a un piccolo esempio:
modello: \b[^ ]+\b 
stringa:   Ve still vant ze money , Lebowski .
corrispondenze: ^^ ^^^^^ ^^^^ ^^ ^^^^^ ^^^^^^^^  
( Esempio ) La sequenza [^ ]deve corrispondere a qualsiasi carattere che non sia uno spazio letterale. Allora perché questo non corrisponde alla virgola ,dopo il denaro o al punto " .dopo Lebowski? Questo perché la virgola ,e il punto .non sono caratteri verbali, quindi vengono creati dei confini tra i caratteri verbali e i caratteri non verbali. Appaiono tra yla fine del simbolo la parola denaro e la virgola ,che la segue. e tra " ila parola Lebowski e il punto .che la segue. L'espressione regolare corrisponde ai confini di queste parole (ma non ai caratteri non verbali che aiutano solo a definirle). Ma cosa succede se non includiamo la coerenza \bnel nostro modello?
modello: [^ ]+ 
stringa:   Ve still vant ze money, Lebowski. 
corrispondenze: ^^ ^^^^^ ^^^^ ^^ ^^^^^^ ^^^^^^^^^  
( Esempio ) Sì, ora troviamo anche questi segni di punteggiatura. Ora utilizziamo i limiti delle parole per correggere la regex per le password tra virgolette:
modello: \"\b[^"]{0.5}\b\"|\"\b[^"]+\s[^"]*\b\" 
stringa:   "12345" "la mia password" " Xanadu. 2112" "su_do" "DiVenditori!"
corrispondenze: ^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^  
( Esempio ) Inserendo i limiti delle parole tra virgolette ("\b ... \b"), stiamo effettivamente dicendo che il primo e l'ultimo carattere delle password corrispondenti devono essere "caratteri di parole". Quindi funziona bene qui, ma non funzionerà altrettanto bene se il primo o l'ultimo carattere della password dell'utente non è un carattere di parola:
modello: \"\b[^"]{0.5}\b\"|\"\b[^"]+\s[^"]*\b\"
stringa: "lapasswordseguenteètroppocorta" "C++"
partite:   
( Esempio ) Osserva come la seconda password non è contrassegnata come "non valida" anche se è chiaramente troppo corta. Devi essereattentocon le sequenze \b, poiché corrispondono solo ai confini tra i personaggi \we non \w. Nell'esempio sopra, poiché abbiamo consentito caratteri non , nelle password \w, non è garantito che il confine tra \il primo/ultimo carattere della password sia un confine di parola \b.

Per completare questo passaggio, risolveremo solo un semplice problema:

I confini delle parole sono utili nei motori di evidenziazione della sintassi quando vogliamo abbinare una sequenza specifica di caratteri, ma vogliamo assicurarci che si trovino solo all'inizio o alla fine di una parola (o da soli). Diciamo che stiamo scrivendo l'evidenziazione della sintassi e vogliamo evidenziare la parola var, ma solo quando appare da sola (senza toccare altri caratteri nella parola). Puoi scrivere un'espressione regolare per questo? Certo che puoi, è un compito molto semplice ;)
modello:
stringa:   var varx _var ( var j) barvarcar * var var -> { var }
corrispondenze: ^^^ ^^^ ^^^ ^^^ ^^^  
( Soluzione )

Passaggio 15: "accento circonflesso" ^come "inizio riga" e simbolo del dollaro $come "fine riga"

20 brevi passaggi per padroneggiare le espressioni regolari.  Parte 3 - 6La sequenza limite delle parole \b(dall'ultimo passaggio della parte precedente dell'articolo) non è l'unica sequenza speciale a larghezza zero disponibile per l'uso nelle espressioni regolari. I due più popolari sono "accento circonflesso" ^- "inizio riga" e simbolo del dollaro $- "fine riga". Includere uno di questi nelle tue espressioni regolari significa che la corrispondenza deve apparire all'inizio o alla fine della stringa di origine:
modello: ^start|end$ 
stringa:   start end start end start end start end 
corrispondenze: ^^^^^ ^^^  
( Esempio ) Se la stringa contiene interruzioni di riga, corrisponderà ^startalla sequenza "start" all'inizio di qualsiasi riga e end$corrisponderà alla sequenza "end" alla fine di qualsiasi riga (sebbene sia difficile da mostrare qui). Questi simboli sono particolarmente utili quando si lavora con dati che contengono delimitatori. Torniamo al problema della "dimensione del file" dal passaggio 9 utilizzando ^"inizio riga". In questo esempio, le dimensioni dei nostri file sono separate da spazi " ". Quindi vogliamo che ogni dimensione del file inizi con un numero, preceduto da uno spazio o dall'inizio di una riga:
modello: (^| )(\d+|\d+\.\d+)[KMGT]B 
stringa:   6.6KB 1..3KB 12KB 5G 3.3MB KB .6.2TB 9MB .
corrispondenze: ^^^^^ ^^^^^ ^^^^^^ ^^^^ 
gruppo:    222 122 1222 12    
( Esempio ) Siamo già così vicini all'obiettivo! Ma potresti notare che abbiamo ancora un piccolo problema: stiamo facendo corrispondere il carattere spazio prima della dimensione valida del file. Ora possiamo semplicemente ignorare questo gruppo di acquisizione (1) quando il nostro motore delle espressioni regolari lo trova oppure possiamo utilizzare un gruppo non di acquisizione, che vedremo nel passaggio successivo.

Nel frattempo, risolviamo altri 2 problemi relativi al tono:

Continuando con l'esempio di evidenziazione della sintassi dell'ultimo passaggio, alcune evidenziazioni della sintassi contrassegneranno gli spazi finali, ovvero tutti gli spazi che si trovano tra un carattere diverso da uno spazio bianco e la fine della riga. Puoi scrivere una regex per evidenziare solo gli spazi finali?
modello:
stringa: myvec <- c(1, 2, 3, 4, 5)  
corrispondenze:                          ^^^^^^^  
( Soluzione ) Un semplice parser con valori separati da virgole (CSV) cercherà "token" separati da virgole. Generalmente lo spazio non ha significato a meno che non sia racchiuso tra virgolette "". Scrivi una semplice espressione regolare di analisi CSV che corrisponda ai token tra virgole, ma ignori (non catturi) lo spazio vuoto che non si trova tra virgolette.
modello:
stringa:   a, "b", "c d",e,f, "g h", dfgi,, k, "", l 
corrisponde a: ^^ ^^^^ ^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^ ^^^ ^ 
gruppo:    21 2221 2222212121 222221 222211 21 221 2    
( Soluzione ) RegEx: 20 brevi passaggi per padroneggiare le espressioni regolari. Parte 4.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION