Hvordan bruke vanlige uttrykk for å erstatte poletter i strenger i Java

1. Oversikt

Når vi trenger å finne eller erstatte verdier i en streng i Java, bruker vi vanligvis regulære uttrykk. Disse lar oss bestemme om noen eller hele strengene samsvarer med et mønster. Vi skal kanskje Enkelt bruke den samme erstatningen på flere tokens i en streng med erstatte alle metode i begge Matcher og String.

I denne opplæringen vil vi utforske hvordan du bruker en annen erstatning for hvert token som finnes i en streng. Dette vil gjøre det enkelt for oss å tilfredsstille brukstilfeller som å unnslippe visse tegn eller erstatte plassholderverdier.

Vi vil også se på noen få triks for å justere våre vanlige uttrykk for å identifisere tokens riktig.

2. Individuelt behandlende treff

Før vi kan bygge vår token-for-token-erstatningsalgoritme, må vi forstå Java API rundt vanlige uttrykk. La oss løse et vanskelig matchingsproblem ved å fange grupper som ikke fanger.

2.1. Tittelsaksseksempel

La oss forestille oss at vi vil bygge en algoritme for å behandle alle tittelordene i en streng. Disse ordene begynner med ett stort tegn, og slutter eller fortsetter med bare små bokstaver.

Innspillene våre kan være:

"Først tre hovedord! Så 10 TLA, fant jeg"

Fra definisjonen av et tittelord inneholder dette treffene:

  • Først
  • Hovedstad
  • Ord
  • Jeg
  • Funnet

Og et vanlig uttrykk for å gjenkjenne dette mønsteret vil være:

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

For å forstå dette, la oss dele det opp i komponentene. Vi begynner i midten:

[A-Z]

vil gjenkjenne en enkelt stor bokstav.

Vi tillater enkelttegnsord eller ord etterfulgt av små bokstaver, så:

[a-z] *

gjenkjenner null eller flere små bokstaver.

I noen tilfeller vil de to karakterklassene ovenfor være nok til å gjenkjenne tokens. Dessverre er det i eksemplet vårt et ord som begynner med flere store bokstaver. Derfor, vi må uttrykke at den store bokstaven vi finner må være den første som vises etter ikke-bokstaver.

På samme måte, ettersom vi tillater et enkelt stort ord, må vi uttrykke at den store bokstaven vi finner ikke må være den første av et ord med flere store bokstaver.

Uttrykket [^ A-Za-z] betyr "ingen bokstaver". Vi har satt en av disse i begynnelsen av uttrykket i en gruppe som ikke fanges:

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

Gruppen som ikke er fangende, starter med (?<=, gjør en se bakover for å sikre at kampen vises på riktig grense. Motstykket til slutt gjør den samme jobben for karakterene som følger.

Imidlertid, hvis ord berører begynnelsen eller slutten av strengen, må vi redegjøre for det, det er der vi har lagt til ^ | til den første gruppen for å få det til å bety “starten på strengen eller tegn som ikke er bokstaver”, og vi har lagt til | $ på slutten av den siste gruppen som ikke fanges for å tillate slutten av strengen å være en grense .

Tegn som finnes i grupper som ikke fanges, vises ikke i kampen når vi bruker finne.

Vi bør merke oss at selv en enkel brukstilfelle som dette kan ha mange kantsaker, så det er viktig å teste våre vanlige uttrykk. For dette kan vi skrive enhetstester, bruke IDEs innebygde verktøy eller bruke et elektronisk verktøy som Regexr.

2.2. Testing vårt eksempel

Med eksempelteksten vår i en konstant kalt EXAMPLE_INPUT og vårt vanlige uttrykk i en Mønster kalt TITLE_CASE_PATTERN, la oss bruke finneMatcher klasse for å trekke ut alle kampene våre i en enhetstest:

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

Her bruker vi matcher funksjon på Mønster å produsere en Matcher. Så bruker vi finne metode i en løkke til den slutter å returnere ekte å gjenta over alle kampene.

Hver gang finne returnerer ekte, den Matcher objektets tilstand er satt til å representere den gjeldende kampen. Vi kan inspisere hele kampen med gruppe (0)eller inspiser bestemte fangegrupper med deres 1-baserte indeks. I dette tilfellet er det en fangegruppe rundt stykket vi vil ha, så vi bruker gruppe (1) for å legge kampen til listen vår.

2.3. Inspeksjon Matcher litt mer

Vi har så langt klart å finne ordene vi ønsker å behandle.

Imidlertid, hvis hvert av disse ordene var et symbol som vi ønsket å erstatte, måtte vi ha mer informasjon om kampen for å kunne bygge den resulterende strengen. La oss se på noen andre egenskaper av Matcher som kan hjelpe oss:

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

Denne koden vil vise oss hvor hver kamp er. Det viser oss også gruppe (0) match, som er alt fanget:

Kamp: Første start: 0 slutt: 5 kamp: hovedstad start: 8 slutt: 15 kamp: ord start: 16 slutt: 21 kamp: I start: 37 slutt: 38 ... mer

Her kan vi se at hver kamp bare inneholder ordene vi forventer. De start eiendommen viser den nullbaserte indeksen for kampen innenfor strengen. De slutt viser indeksen til tegnet like etter. Dette betyr at vi kan bruke substring (start, slutt-start) for å trekke ut hver kamp fra den opprinnelige strengen. Dette er egentlig hvordan gruppe metoden gjør det for oss.

Nå som vi kan bruke finne For å gjenta over treff, la oss behandle tokens.

3. Bytte ut fyrstikker en etter en

La oss fortsette eksemplet vårt ved å bruke algoritmen vår til å erstatte hvert tittelord i den opprinnelige strengen med dets små bokstaver. Dette betyr at teststrengen vår blir konvertert til:

"først 3 store ord! så 10 TLA, fant jeg"

De Mønster og Matcher klasse kan ikke gjøre dette for oss, så vi må lage en algoritme.

3.1. Erstatningsalgoritmen

Her er pseudokoden for algoritmen:

  • Start med en tom utgangsstreng
  • For hver kamp:
    • Legg til utdataene alt som kom før kampen og etter en tidligere kamp
    • Behandle denne kampen og legg den til utdataene
    • Fortsett til alle kampene er behandlet
    • Legg til noe som er igjen etter siste kamp i utgangen

Vi bør merke oss at målet med denne algoritmen er å finn alle områdene som ikke samsvarer, og legg dem til utdataene, samt å legge til de behandlede treffene.

3.2. Token Replacer i Java

Vi ønsker å konvertere hvert ord til små bokstaver, slik at vi kan skrive en enkel konverteringsmetode:

privat statisk strengkonvertering (strengtegn) {retur token.toLowerCase (); }

Nå kan vi skrive algoritmen for å gjenta over kampene. Dette kan bruke en StringBuilder for utgangen:

int lastIndex = 0; StringBuilder-utgang = ny StringBuilder (); Matcher matcher = TITLE_CASE_PATTERN.matcher (original); while (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 ()); } returner output.toString ();

Vi bør merke oss det StringBuilder gir en praktisk versjon av legge til som kan trekke ut underlag. Dette fungerer bra med slutt tilhører Matcher for å la oss plukke opp alle tegn som ikke samsvarer siden forrige kamp.

4. Generalisering av algoritmen

Nå som vi har løst problemet med å erstatte noen spesifikke tokens, hvorfor konverterer vi ikke koden til et skjema der den kan brukes i det generelle tilfellet? Det eneste som varierer fra en implementering til en annen, er det vanlige uttrykket som skal brukes, og logikken for å konvertere hver kamp til erstatning.

4.1. Bruk en funksjon og mønsterinngang

Vi kan bruke Java Funksjon innvende for å tillate den som ringer å gi logikken til å behandle hver kamp. Og vi kan ta et innspill som heter tokenPattern for å finne 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 ikke det vanlige uttrykket hardkodet lenger. I stedet for omformer funksjonen blir levert av den som ringer og brukes på hver kamp i finne Løkke.

4.2. Testing av den generelle versjonen

La oss se om den generelle metoden fungerer like bra som originalen:

assertThat (erstatt Tokens ("First 3 Capital Words! then 10 TLAs, I Found", TITLE_CASE_PATTERN, match -> match.group (1) .toLowerCase ())) .isEqualTo ("første 3 store ord! deretter 10 TLAs, fant jeg ");

Her ser vi at det er greit å ringe koden. Konverteringsfunksjonen er lett å uttrykke som en lambda. Og testen består.

Nå har vi en token-erstatning, så la oss prøve noen andre brukstilfeller.

5. Noen brukssaker

5.1. Unnslipper spesialtegn

La oss forestille oss at vi ønsket å bruke det regulære uttrykket escape karakter \ å sitere hvert tegn i et vanlig uttrykk manuelt i stedet for å bruke sitat metode. Kanskje siterer vi en streng som en del av å lage et vanlig uttrykk som skal overføres til et annet bibliotek eller en annen tjeneste, så det er ikke tilstrekkelig å blokkere sitering av uttrykket.

Hvis vi kan uttrykke mønsteret som betyr "et vanlig uttrykkskarakter", er det enkelt å bruke algoritmen vår for å unnslippe dem alle:

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

For hver kamp prefikser vi \ karakter. Som \ er et spesialtegn i Java-strenger, det slapp med en annen \.

Faktisk er dette eksemplet dekket av ekstra \ tegn som karakterklassen i mønsteret for regexCharacters må sitere mange av spesialtegnene. Dette viser reguleringsuttrykk-analysatoren som vi bruker dem for å bety bokstavene deres, ikke som syntaks for vanlig uttrykk.

5.2. Skifte plassholdere

En vanlig måte å uttrykke en plassholder på er å bruke en syntaks som $ {name}. La oss vurdere et brukstilfelle der malen "Hei $ {name} på $ {company}" må fylles ut fra et kart som heter placeholderValues:

Map placeholderValues ​​= new HashMap (); placeholderValues.put ("navn", "Bill"); placeholderValues.put ("selskap", "Baeldung");

Alt vi trenger er et godt regulært uttrykk å finne ${…} tokens:

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

er ett alternativ. Det må sitere $ og den opprinnelige krøllbøylen, da de ellers ville bli behandlet som syntaks med vanlig uttrykk.

Kjernen i dette mønsteret er en fangegruppe for navnet på plassholderen. Vi har brukt en karakterklasse som tillater alfanumeriske, bindestreker og understrekninger, som skal passe til de fleste brukssaker.

Derimot, For å gjøre koden mer lesbar har vi kalt denne fangegruppenplassholder. La oss se hvordan du bruker den navngitte fangegruppen:

assertThat (erstatt Tokens ("Hei $ {name} på $ {company}", "\ $ \ {(? [A-Za-z0-9 -_] +)}", match -> placeholderValues.get (match .group ("plassholder")))) .isEqualTo ("Hei Bill i Baeldung");

Her kan vi se at å få verdien av den navngitte gruppen ut av Matcher bare innebærer bruk gruppe med navnet som inngang, i stedet for nummeret.

6. Konklusjon

I denne artikkelen så vi på hvordan vi kan bruke kraftige regulære uttrykk for å finne tokens i strengene våre. Vi lærte hvordan finne metoden fungerer med Matcher for å vise oss kampene.

Så opprettet og generaliserte vi en algoritme for å tillate oss å utskifte token for token.

Til slutt så vi på et par vanlige brukssaker for å unnslippe tegn og fylle ut maler.

Som alltid kan kodeeksemplene finnes på GitHub.