Reguliere expressies gebruiken om tokens in strings in Java te vervangen

1. Overzicht

Als we waarden in een string in Java moeten zoeken of vervangen, gebruiken we meestal reguliere expressies. Hiermee kunnen we bepalen of een string of een deel van een string overeenkomt met een patroon. We zouden kunnen gemakkelijk pas dezelfde vervanging toe op meerdere tokens in een string met de vervang alles methode in beide Matcher en Draad.

In deze zelfstudie onderzoeken we hoe u een andere vervanging kunt toepassen voor elk token in een tekenreeks. Dit maakt het voor ons gemakkelijk om aan gebruiksgevallen te voldoen, zoals het ontsnappen aan bepaalde tekens of het vervangen van tijdelijke aanduidingen.

We zullen ook een paar trucs bekijken om onze reguliere expressies af te stemmen om tokens correct te identificeren.

2. Individueel verwerken van matches

Voordat we ons token-by-token-vervangingsalgoritme kunnen bouwen, moeten we de Java API rond reguliere expressies begrijpen. Laten we een lastig matchprobleem oplossen met behulp van vastleggende en niet-vastleggende groepen.

2.1. Titel voorbeeld

Laten we ons voorstellen dat we een algoritme willen bouwen om alle titelwoorden in een string te verwerken. Deze woorden beginnen met één hoofdletter en eindigen of gaan verder met alleen kleine letters.

Onze input zou kunnen zijn:

"Eerste 3 hoofdwoorden! Dan 10 TLA's, vond ik"

Uit de definitie van een titelwoord bevat dit de overeenkomsten:

  • Eerste
  • Kapitaal
  • Woorden
  • ik
  • Gevonden

En een reguliere expressie om dit patroon te herkennen zou zijn:

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

Om dit te begrijpen, laten we het opsplitsen in zijn samenstellende delen. We beginnen in het midden:

[A-Z]

herkent een enkele hoofdletter.

We staan ​​woorden van één teken toe of woorden gevolgd door kleine letters, dus:

[a-z] *

herkent nul of meer kleine letters.

In sommige gevallen zouden de bovenstaande twee karakterklassen voldoende zijn om onze tokens te herkennen. Helaas is er in onze voorbeeldtekst een woord dat begint met meerdere hoofdletters. Daarom we moeten uitdrukken dat de enkele hoofdletter die we vinden de eerste moet zijn die na niet-letters verschijnt.

Evenzo, aangezien we een woord met één hoofdletter toestaan, moeten we aangeven dat de enkele hoofdletter die we vinden niet de eerste mag zijn van een woord met meerdere hoofdletters.

De uitdrukking [^ A-Za-z] betekent "geen letters". We hebben een van deze aan het begin van de uitdrukking in een niet-vastleggende groep geplaatst:

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

De niet-veroverende groep, te beginnen met (?<=, doet een achterom kijken om ervoor te zorgen dat de wedstrijd op de juiste grens verschijnt. Zijn tegenhanger aan het einde doet hetzelfde voor de personages die volgen.

Als woorden echter het begin of het einde van de tekenreeks raken, moeten we daar rekening mee houden, en dat is waar we ^ | aan de eerste groep om ervoor te zorgen dat het "het begin van de tekenreeks of andere niet-lettertekens" betekent, en we hebben | $ toegevoegd aan het einde van de laatste niet-vastleggende groep om het einde van de tekenreeks een grens te laten zijn .

Tekens gevonden in niet-capturing groepen verschijnen niet in de match wanneer we gebruiken vind.

We moeten er rekening mee houden dat zelfs een eenvoudig gebruik als deze veel randgevallen kan hebben, dus het is belangrijk om onze reguliere expressies te testen. Hiervoor kunnen we unit-tests schrijven, de ingebouwde tools van onze IDE gebruiken of een online tool zoals Regexr gebruiken.

2.2. Ons voorbeeld testen

Met onze voorbeeldtekst in een constante genaamd EXAMPLE_INPUT en onze reguliere expressie in een Patroon gebeld TITLE_CASE_PATTERN, laten we gebruiken vind op de Matcher class om al onze matches te extraheren in een unit-test:

Matcher-matcher = TITLE_CASE_PATTERN.matcher (EXAMPLE_INPUT); Lijstovereenkomsten = nieuwe ArrayList (); while (matcher.find ()) {matches.add (matcher.group (1)); } assertThat (matches) .containsExactly ("First", "Capital", "Words", "I", "Found");

Hier gebruiken we de matcher functie aan Patroon om een Matcher. Dan gebruiken we de vind methode in een lus totdat deze niet meer terugkeert waar om alle overeenkomsten te herhalen.

Elke keer vind geeft terug waar, de Matcher de status van het object is ingesteld om de huidige overeenkomst weer te geven. We kunnen de hele wedstrijd bekijken met groep (0)of inspecteer bepaalde vastleggende groepen met hun 1-gebaseerde index. In dit geval is er een vastleggende groep rond het stuk dat we willen, dus gebruiken we groep 1) om de match aan onze lijst toe te voegen.

2.3. Inspecteren Matcher een beetje meer

We zijn er tot nu toe in geslaagd de woorden te vinden die we willen verwerken.

Als elk van deze woorden echter een token was dat we wilden vervangen, zouden we meer informatie over de overeenkomst nodig hebben om de resulterende string samen te stellen. Laten we eens kijken naar enkele andere eigenschappen van Matcher dat kan ons helpen:

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

Deze code laat ons zien waar elke match is. Het toont ons ook de groep (0) match, dat is alles wat is vastgelegd:

Match: First Start: 0 End: 5 Match: Capital Start: 8 End: 15 Match: Words Start: 16 End: 21 Match: I Start: 37 End: 38 ... more

Hier kunnen we zien dat elke match alleen de woorden bevat die we verwachten. De begin eigenschap toont de op nul gebaseerde index van de overeenkomst binnen de string. De einde toont de index van het karakter net erna. Dit betekent dat we kunnen gebruiken substring (start, end-start) om elke match uit de originele string te halen. Dit is in wezen hoe de groep method doet dat voor ons.

Nu we kunnen gebruiken vind Laten we, om overeenkomsten te herhalen, onze tokens verwerken.

3. Matches een voor een vervangen

Laten we doorgaan met ons voorbeeld door ons algoritme te gebruiken om elk titelwoord in de originele tekenreeks te vervangen door zijn equivalent in kleine letters. Dit betekent dat onze testreeks wordt geconverteerd naar:

"eerste 3 hoofdwoorden! daarna 10 TLA's, vond ik"

De Patroon en Matcher class kan dit niet voor ons doen, dus we moeten een algoritme construeren.

3.1. Het vervangende algoritme

Hier is de pseudo-code voor het algoritme:

  • Begin met een lege outputstring
  • Voor elke wedstrijd:
    • Voeg aan de uitvoer alles toe dat vóór de wedstrijd en na een eerdere wedstrijd kwam
    • Verwerk deze match en voeg die toe aan de output
    • Ga door totdat alle overeenkomsten zijn verwerkt
    • Voeg alles wat is overgebleven na de laatste match toe aan de uitvoer

We moeten opmerken dat het doel van dit algoritme is om vind alle niet-overeenkomende gebieden en voeg ze toe aan de uitvoer, evenals het toevoegen van de verwerkte overeenkomsten.

3.2. De tokenvervanger in Java

We willen elk woord naar kleine letters converteren, zodat we een eenvoudige conversiemethode kunnen schrijven:

private static String convert (String token) {return token.toLowerCase (); }

Nu kunnen we het algoritme schrijven om de overeenkomsten te herhalen. Dit kan een StringBuilder voor de output:

int lastIndex = 0; StringBuilder output = nieuwe StringBuilder (); Matcher matcher = TITLE_CASE_PATTERN.matcher (origineel); while (matcher.find ()) {output.append (origineel, lastIndex, matcher.start ()) .append (convert (matcher.group (1))); lastIndex = matcher.end (); } if (lastIndex <original.length ()) {output.append (origineel, lastIndex, original.length ()); } return output.toString ();

Dat moeten we opmerken StringBuilder biedt een handige versie van toevoegen die substrings kunnen extraheren. Dit werkt goed met de einde eigendom van Matcher om ons alle niet-overeenkomende karakters te laten ophalen sinds de laatste match.

4. Generaliseren van het algoritme

Nu we het probleem van het vervangen van enkele specifieke tokens hebben opgelost, waarom zetten we de code dan niet om in een vorm waarin deze kan worden gebruikt voor het algemene geval? Het enige dat van implementatie tot implementatie verschilt, is de te gebruiken reguliere expressie en de logica voor het omzetten van elke match in zijn vervanging.

4.1. Gebruik een functie en patrooninvoer

We kunnen een Java gebruiken Functie object om de beller in staat te stellen de logica te leveren om elke overeenkomst te verwerken. En we kunnen een input aannemen die wordt genoemd tokenPattern om alle tokens te vinden:

// hetzelfde als voorheen while (matcher.find ()) {output.append (origineel, lastIndex, matcher.start ()) .append (converter.apply (matcher)); // hetzelfde als voorheen

Hier is de reguliere expressie niet langer hard gecodeerd. In plaats daarvan is de omzetter functie wordt geleverd door de beller en wordt toegepast op elke match binnen de vind lus.

4.2. De algemene versie testen

Laten we eens kijken of de algemene methode net zo goed werkt als de originele:

assertThat (replaceTokens ("First 3 Capital Words! then 10 TLAs, I Found", TITLE_CASE_PATTERN, match -> match.group (1) .toLowerCase ())) .isEqualTo ("eerste 3 hoofdwoorden! daarna 10 TLA's, vond ik ");

Hier zien we dat het aanroepen van de code eenvoudig is. De conversiefunctie is eenvoudig uit te drukken als een lambda. En de test slaagt.

Nu hebben we een token-vervanger, dus laten we een paar andere use-cases proberen.

5. Enkele gebruiksscenario's

5.1. Ontsnappen aan speciale karakters

Laten we ons voorstellen dat we het escape-teken voor reguliere expressies wilden gebruiken \ om elk teken van een reguliere expressie handmatig te citeren in plaats van de citaat methode. Misschien citeren we een tekenreeks als onderdeel van het maken van een reguliere expressie om door te geven aan een andere bibliotheek of service, dus het aanhalen van de expressie in een blok is niet voldoende.

Als we het patroon kunnen uitdrukken dat "een reguliere-expressieteken" betekent, is het gemakkelijk om ons algoritme te gebruiken om ze allemaal te vermijden:

Pattern regexCharacters = Pattern.compile ("[]"); assertThat (replaceTokens ("Een regex-teken zoals [", regexCharacters, match -> "\" + match.group ())) .isEqualTo ("Een regex-teken zoals \ [");

Voor elke wedstrijd geven we het voorvoegsel \ karakter. Net zo \ is een speciaal teken in Java-strings, het is ontsnapt met een ander \.

Inderdaad, dit voorbeeld wordt in extra gedekt \ tekens als de tekenklasse in het patroon voor regexTekens moet veel van de speciale tekens citeren. Dit toont de reguliere expressie-parser dat we ze gebruiken om hun letterlijke waarden te betekenen, niet als syntaxis voor reguliere expressies.

5.2. Tijdelijke aanduidingen vervangen

Een veelgebruikte manier om een ​​tijdelijke aanduiding uit te drukken, is door een syntaxis te gebruiken zoals $ {naam}. Laten we eens kijken naar een use-case waarbij de template "Hallo $ {name} bij $ {company}" moet worden ingevuld vanuit een kaart met de naam placeholderValues:

Map placeholderValues ​​= nieuwe HashMap (); placeholderValues.put ("naam", "Bill"); placeholderValues.put ("bedrijf", "Baeldung");

Het enige wat we nodig hebben is een goede reguliere expressie om het ${…} Munten:

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

is een optie. Het moet de $ en de aanvankelijke accolade, omdat ze anders zouden worden behandeld als de syntaxis van reguliere expressies.

De kern van dit patroon is een vastleggroep voor de naam van de tijdelijke aanduiding. We hebben een tekenklasse gebruikt die alfanumeriek, streepjes en onderstrepingstekens toestaat, wat in de meeste gebruiksscenario's zou moeten passen.

Echter, om de code beter leesbaar te maken, hebben we deze capturing group genoemdtijdelijke aanduiding. Laten we eens kijken hoe we die genoemde vastleggroep kunnen gebruiken:

assertThat (replaceTokens ("Hallo $ {naam} bij $ {bedrijf}", "\ $ \ {(? [A-Za-z0-9 -_] +)}", match -> placeholderValues.get (match .group ("placeholder")))) .isEqualTo ("Hallo Bill bij Baeldung");

Hier kunnen we zien dat de waarde van de benoemde groep uit de Matcher omvat gewoon het gebruik groep met de naam als invoer, in plaats van het nummer.

6. Conclusie

In dit artikel hebben we gekeken hoe we krachtige reguliere expressies kunnen gebruiken om tokens in onze strings te vinden. We hebben geleerd hoe de vind methode werkt met Matcher om ons de wedstrijden te laten zien.

Vervolgens hebben we een algoritme gemaakt en gegeneraliseerd waarmee we token-voor-token-vervanging kunnen doen.

Ten slotte hebben we gekeken naar een aantal veelvoorkomende use-cases voor het ontsnappen van tekens en het vullen van sjablonen.

Zoals altijd zijn de codevoorbeelden te vinden op GitHub.