Sådan bruges regulære udtryk til at erstatte tokens i strenge i Java

1. Oversigt

Når vi skal finde eller erstatte værdier i en streng i Java, bruger vi normalt regulære udtryk. Disse giver os mulighed for at afgøre, om en eller flere af strengene matcher et mønster. Vi måske let anvende den samme erstatning på flere tokens i en streng med udskift alle metode i begge Matcher og Snor.

I denne vejledning undersøger vi, hvordan du anvender en anden erstatning for hvert token, der findes i en streng. Dette gør det let for os at tilfredsstille brugstilfælde som at undslippe bestemte tegn eller erstatte pladsholderværdier.

Vi ser også på et par tricks til at indstille vores regulære udtryk for at identificere tokens korrekt.

2. Individuelt behandling af matches

Inden vi kan opbygge vores token-for-token-erstatningsalgoritme, skal vi forstå Java API omkring regulære udtryk. Lad os løse et vanskeligt matchende problem ved hjælp af fangende og ikke-fangende grupper.

2.1. Titel Case Eksempel

Lad os forestille os, at vi vil bygge en algoritme til at behandle alle titelordene i en streng. Disse ord starter med et stort bogstav og slutter enten eller fortsætter kun med små bogstaver.

Vores input kan være:

"Først 3 hovedord! Derefter 10 TLA'er, jeg fandt"

Fra definitionen af ​​et titelord indeholder dette matchene:

  • Først
  • Kapital
  • Ord
  • jeg
  • Fundet

Og et regelmæssigt udtryk for at genkende dette mønster ville være:

"(? <= ^ | [^ A-Za-z]) ([A-Z] [a-z] *) (? = [^ A-Za-z] | $)"

For at forstå dette, lad os opdele det i dets komponentdele. Vi starter i midten:

[A-Z]

genkender et enkelt stort bogstav.

Vi tillader enkelttegnsord eller ord efterfulgt af små bogstaver, så:

[a-z] *

genkender nul eller flere små bogstaver.

I nogle tilfælde vil de ovennævnte to karakterklasser være tilstrækkelige til at genkende vores tokens. Desværre er der i vores eksempletekst et ord, der starter med flere store bogstaver. Derfor, vi er nødt til at udtrykke, at det store bogstav, vi finder, skal være det første, der vises efter ikke-bogstaver.

På samme måde, da vi tillader et enkelt stort bogstavord, er vi nødt til at udtrykke, at det store bogstav, vi finder, ikke må være det første af et ord med flere store bogstaver.

Udtrykket [^ A-Za-z] betyder "ingen bogstaver". Vi har sat en af ​​disse i starten af ​​udtrykket i en gruppe, der ikke er fanget:

(? <= ^ | [^ A-Za-z])

Den ikke-fangende gruppe, startende med (?<=, gør en se bagud for at sikre, at kampen vises ved den korrekte grænse. Dens modstykke i slutningen gør det samme job for de tegn, der følger.

Men hvis ord rører ved begyndelsen eller slutningen af ​​strengen, skal vi tage højde for det, det er her vi har tilføjet ^ | til den første gruppe for at få det til at betyde "starten på strengen eller andre tegn end bogstaverne", og vi har tilføjet | $ i slutningen af ​​den sidste ikke-fangende gruppe for at tillade slutningen af ​​strengen at være en grænse .

Tegn, der findes i grupper, der ikke fanges, vises ikke i kampen når vi bruger finde.

Vi skal bemærke, at selv en simpel brugssag som denne kan have mange kanttilfælde, så det er vigtigt at teste vores regelmæssige udtryk. Til dette kan vi skrive enhedstest, bruge vores IDE's indbyggede værktøjer eller bruge et online værktøj som Regexr.

2.2. Test af vores eksempel

Med vores eksempeltekst kaldes en konstant EKSEMPEL_INPUT og vores regelmæssige udtryk i en Mønster hedder TITLE_CASE_PATTERN, lad os bruge finde på den Matcher klasse for at udtrække alle vores kampe i en enhedstest:

Matcher matcher = TITLE_CASE_PATTERN.matcher (EXAMPLE_INPUT); Liste matches = ny ArrayList (); mens (matcher.find ()) {matches.add (matcher.group (1)); } assertThat (matches) .containsExactly ("First", "Capital", "Words", "I", "Found");

Her bruger vi matcher funktion til Mønster at producere en Matcher. Så bruger vi finde metode i en løkke, indtil den holder op med at vende tilbage rigtigt at gentage alle kampe.

Hver gang finde vender tilbage rigtigt, det Matcher objektets tilstand er indstillet til at repræsentere det aktuelle match. Vi kan inspicere hele kampen med gruppe (0)eller inspicere bestemte indfangningsgrupper med deres 1-baserede indeks. I dette tilfælde er der en fangegruppe omkring det stykke, vi ønsker, så vi bruger det gruppe (1) for at føje kampen til vores liste.

2.3. Inspektion Matcher lidt mere

Vi har indtil videre formået at finde de ord, vi vil behandle.

Men hvis hvert af disse ord var et token, som vi ønskede at erstatte, skulle vi have flere oplysninger om kampen for at opbygge den resulterende streng. Lad os se på nogle andre egenskaber ved Matcher der kan hjælpe os:

mens (matcher.find ()) {System.out.println ("Match:" + matcher.group (0)); System.out.println ("Start:" + matcher.start ()); System.out.println ("End:" + matcher.end ()); }

Denne kode viser os, hvor hver kamp er. Det viser os også gruppe (0) match, som er alt fanget:

Kamp: Første start: 0 slutning: 5 kamp: hovedstart: 8 slutning: 15 kamp: ord start: 16 slutning: 21 kamp: I start: 37 slutning: 38 ... mere

Her kan vi se, at hver kamp kun indeholder de ord, vi forventer. Det Start egenskab viser det nulbaserede indeks for matchet inden for strengen. Det ende viser tegnets indeks lige efter. Det betyder, at vi kunne bruge substring (start, slut-start) for at udtrække hvert match fra den originale streng. Dette er i det væsentlige, hvordan gruppe metode gør det for os.

Nu hvor vi kan bruge finde For at gentage over kampe, lad os behandle vores tokens.

3. Udskiftning af tændstikker en efter en

Lad os fortsætte vores eksempel ved at bruge vores algoritme til at erstatte hvert titelord i den originale streng med dets små bogstaver. Dette betyder, at vores teststreng konverteres til:

"først 3 store ord! derefter 10 TLA'er, jeg fandt"

Det Mønster og Matcher klasse kan ikke gøre dette for os, så vi er nødt til at konstruere en algoritme.

3.1. Erstatningsalgoritmen

Her er pseudokoden til algoritmen:

  • Start med en tom outputstreng
  • For hver kamp:
    • Føj til output alt, hvad der kom før kampen og efter en tidligere kamp
    • Behandl dette match og tilføj det til output
    • Fortsæt, indtil alle matches er behandlet
    • Føj noget tilbage efter den sidste kamp til output

Vi skal bemærke, at formålet med denne algoritme er at find alle områder, der ikke matcher, og tilføj dem til outputsamt tilføjelse af de behandlede matches.

3.2. Token Replacer i Java

Vi vil konvertere hvert ord til små bogstaver, så vi kan skrive en simpel konverteringsmetode:

privat statisk strengkonvertering (streng token) {return token.toLowerCase (); }

Nu kan vi skrive algoritmen for at gentage over kampene. Dette kan bruge en StringBuilder til output:

int lastIndex = 0; StringBuilder output = ny StringBuilder (); Matcher matcher = TITLE_CASE_PATTERN.matcher (original); mens (matcher.find ()) {output.append (original, lastIndex, matcher.start ()) .append (convert (matcher.group (1))); lastIndex = matcher.end (); } hvis (lastIndex <original.length ()) {output.append (original, lastIndex, original.length ()); } returnere output.toString ();

Det skal vi bemærke StringBuilder giver en praktisk version af Tilføj der kan udtrække underlag. Dette fungerer godt med ende ejendom af Matcher for at lade os samle alle ikke-matchede tegn siden sidste kamp.

4. Generalisering af algoritmen

Nu hvor vi har løst problemet med at erstatte nogle specifikke tokens, hvorfor konverterer vi ikke koden til en form, hvor den kan bruges i det generelle tilfælde? Det eneste der varierer fra en implementering til den næste er det regulære udtryk, der skal bruges, og logikken til at konvertere hver kamp til dens erstatning.

4.1. Brug en funktion og mønsterinput

Vi kan bruge en Java Fungere gør indsigelse mod at give den, der ringer op, logikken til at behandle hver kamp. Og vi kan tage et input kaldet tokenPattern for at finde alle tokens:

// samme som før mens (matcher.find ()) {output.append (original, lastIndex, matcher.start ()) .append (converter.apply (matcher)); // samme som før

Her er det regulære udtryk ikke længere hårdkodet. I stedet for konverter funktionen leveres af den, der ringer op, og anvendes til hver kamp inden for finde løkke.

4.2. Test af den generelle version

Lad os se, om den generelle metode fungerer så godt som originalen:

assertThat (erstatteTokens ("Første 3 hovedord! derefter 10 TLA'er, jeg fandt", TITLE_CASE_PATTERN, match -> match.group (1) .toLowerCase ())) .isEqualTo ("første 3 store ord! derefter 10 TLA'er, fandt jeg ");

Her ser vi, at kalde koden er ligetil. Konverteringsfunktionen er let at udtrykke som en lambda. Og testen består.

Nu har vi en token-erstatning, så lad os prøve nogle andre brugssager.

5. Nogle brugssager

5.1. Undslipper specialtegn

Lad os forestille os, at vi ønskede at bruge det regulære udtryk escape-karakter \ at manuelt citere hvert tegn i et regulært udtryk i stedet for at bruge citere metode. Måske citerer vi en streng som en del af oprettelsen af ​​et regulært udtryk, der skal overføres til et andet bibliotek eller en anden tjeneste, så blokering med citering af udtrykket er ikke tilstrækkelig.

Hvis vi kan udtrykke det mønster, der betyder "et regulært udtrykskarakter", er det let at bruge vores algoritme til at undslippe dem alle:

Mønster regexCharacters = Pattern.compile ("[]"); assertThat (erstatte Tokens ("Et regex-tegn som [", regexCharacters, match -> "\" + match.group ())) .isEqualTo ("Et regex-tegn som \ [");

For hver kamp præfikrer vi \ Karakter. Som \ er et specialtegn i Java-strenge, det er undsluppet med et andet \.

Faktisk er dette eksempel dækket ekstra \ tegn som karakterklasse i mønsteret for regexCharacters er nødt til at citere mange af specialtegnene. Dette viser den regelmæssige udtryksparser, som vi bruger dem til at betyde deres bogstaver, ikke som syntaks for regulært udtryk.

5.2. Udskiftning af pladsholdere

En almindelig måde at udtrykke en pladsholder på er at bruge en syntaks som $ {navn}. Lad os overveje en brugssag, hvor skabelonen "Hej $ {name} hos $ {company}" skal udfyldes fra et kort kaldet placeholderValues:

Kort placeholderValues ​​= nyt HashMap (); placeholderValues.put ("navn", "Bill"); placeholderValues.put ("firma", "Baeldung");

Alt, hvad vi har brug for, er et godt regelmæssigt udtryk at finde ${…} poletter:

"\ $ \ {(? [A-Za-z0-9 -_] +)}"

er en mulighed. Det skal citere $ og den oprindelige krøllede bøjle, da de ellers ville blive behandlet som syntaks med regulært udtryk.

Kernen i dette mønster er en fangende gruppe for pladsholderens navn. Vi har brugt en karakterklasse, der tillader alfanumeriske, bindestreger og understregninger, som skal passe til de fleste brugssager.

Imidlertid, For at gøre koden mere læselig har vi navngivet denne fangstgruppepladsholder. Lad os se, hvordan man bruger den navngivne fangstgruppe:

assertThat (erstatte Tokens ("Hej $ {navn} på $ {firma}", "\ $ \ {(? [A-Za-z0-9 -_] +)}", match -> pladsholderValues.get (match .gruppe ("pladsholder"))) .isEqualTo ("Hej Bill i Baeldung");

Her kan vi se at få værdien af ​​den navngivne gruppe ud af Matcher involverer bare brug gruppe med navnet som input, snarere end nummeret.

6. Konklusion

I denne artikel så vi på, hvordan man bruger kraftige regulære udtryk til at finde tokens i vores strenge. Vi lærte, hvordan finde metode fungerer med Matcher for at vise os kampene.

Derefter oprettede og generaliserede vi en algoritme, der gjorde det muligt for os at udskifte token-for-token.

Endelig så vi på et par almindelige brugssager til at undslippe tegn og udfylde skabeloner.

Som altid kan kodeeksemplerne findes på GitHub.