Cara Menggunakan Ungkapan Biasa untuk Mengganti Token dalam Rentetan di Java

1. Gambaran keseluruhan

Apabila kita perlu mencari atau mengganti nilai dalam rentetan di Java, kita biasanya menggunakan ungkapan biasa. Ini membolehkan kita menentukan sama ada sebilangan atau semua rentetan sesuai dengan corak. Kita mungkin mudah memohon penggantian yang sama kepada beberapa token dalam rentetan dengan replaceAll kaedah dalam kedua-dua Matcher dan tali .

Dalam tutorial ini, kita akan meneroka cara menerapkan penggantian yang berbeza untuk setiap token yang terdapat dalam rentetan. Ini akan memudahkan kita untuk memenuhi kes penggunaan seperti melarikan watak tertentu atau menggantikan nilai tempat letak.

Kami juga akan melihat beberapa helah untuk menyesuaikan ungkapan biasa kami untuk mengenal pasti token dengan betul.

2. Memproses Pertandingan Secara Individu

Sebelum kita dapat membuat algoritma penggantian token demi token, kita perlu memahami API Java di sekitar ungkapan biasa. Mari selesaikan masalah pemadanan yang sukar dengan menggunakan kumpulan menangkap dan tidak menangkap.

2.1. Contoh Kes Tajuk

Cuba bayangkan kita mahu membina algoritma untuk memproses semua perkataan tajuk dalam rentetan. Kata-kata ini bermula dengan satu huruf besar dan kemudian diakhiri atau dilanjutkan dengan hanya huruf kecil.

Input kami mungkin:

"First 3 Capital Words! then 10 TLAs, I Found"

Dari definisi kata tajuk, ini mengandungi padanan:

  • Pertama
  • Modal
  • Perkataan
  • Saya
  • Dijumpai

Dan ungkapan biasa untuk mengenali corak ini adalah:

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

Untuk memahami perkara ini, mari kita pecahkan kepada bahagian komponennya. Kami akan bermula di tengah:

[A-Z]

akan mengenali huruf besar tunggal.

Kami membenarkan kata-kata tunggal atau kata-kata diikuti dengan huruf kecil, jadi:

[a-z]*

mengenal huruf kecil sifar atau lebih.

Dalam beberapa kes, dua kelas watak di atas sudah cukup untuk mengenali token kami. Malangnya, dalam teks contoh kami, ada kata yang bermula dengan pelbagai huruf besar. Oleh itu, kita perlu menyatakan bahawa huruf besar tunggal yang kita dapati mestilah yang pertama muncul selepas huruf bukan.

Begitu juga, ketika kita mengizinkan kata huruf besar tunggal, kita harus menyatakan bahawa huruf besar tunggal yang kita dapati mestilah bukan yang pertama dari kata huruf besar.

Ungkapan [^ A-Za-z] bermaksud "tanpa huruf". Kami telah meletakkan salah satu dari ini pada awal ungkapan dalam kumpulan yang tidak menangkap:

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

Kumpulan yang tidak menangkap, bermula dengan (? <=, Melakukan pandangan ke belakang untuk memastikan perlawanan muncul di batas yang betul. Rakannya di akhir melakukan pekerjaan yang sama untuk watak-watak yang diikuti.

Namun, jika kata-kata menyentuh awal atau akhir rentetan, maka kita perlu memperhitungkannya, di sinilah kita telah menambahkan ^ | ke kumpulan pertama yang bermaksud "permulaan rentetan atau watak bukan huruf", dan kami telah menambahkan | $ pada akhir kumpulan terakhir yang tidak menangkap untuk membolehkan akhir rentetan menjadi sempadan .

Karakter yang terdapat dalam kumpulan yang tidak menangkap tidak muncul dalam perlawanan ketika kita menggunakan find .

Kita harus perhatikan bahawa walaupun kes penggunaan sederhana seperti ini boleh mempunyai banyak casing tepi, jadi penting untuk menguji ungkapan biasa kita . Untuk ini, kita boleh menulis ujian unit, menggunakan alat terbina dalam IDE kita, atau menggunakan alat dalam talian seperti Regexr.

2.2. Menguji Contoh Kita

Dengan teks contoh kita dalam pemalar yang dipanggil EXAMPLE_INPUT dan ungkapan biasa kita dalam corak dipanggil TITLE_CASE_PATTERN , mari penggunaan yang find pada Matcher kelas untuk mengeluarkan semua perlawanan kami dalam ujian unit:

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

Di sini kita menggunakan fungsi matcher pada Pattern untuk menghasilkan Matcher . Kemudian kami menggunakan kaedah cari dalam satu gelung sehingga berhenti kembali benar untuk mengulangi semua perlawanan.

Setiap kali find pulangan benar , yang Matcher negeri objek ditetapkan untuk mewakili perlawanan semasa. Kami dapat memeriksa keseluruhan perlawanan dengan kumpulan (0) atau memeriksa kumpulan penangkap tertentu dengan indeks berdasarkan 1 mereka . Dalam kes ini, ada kumpulan penangkap di sekitar potongan yang kami mahukan, jadi kami menggunakan kumpulan (1) untuk menambahkan padanan ke daftar kami.

2.3. Memeriksa Matcher Sedikit Lagi

Kami setakat ini berjaya menemui perkataan yang ingin kami proses.

Namun, jika masing-masing kata tersebut adalah tanda yang ingin kita ganti, kita perlu mempunyai lebih banyak maklumat mengenai pertandingan untuk membina rentetan yang dihasilkan. Mari lihat beberapa sifat lain dari Matcher yang mungkin dapat membantu kami:

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

Kod ini akan menunjukkan kepada kita di mana setiap perlawanan. Ini juga menunjukkan kepada kita perlawanan kumpulan (0) , yang semuanya ditangkap:

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

Here we can see that each match contains only the words we're expecting. The start property shows the zero-based index of the match within the string. The end shows the index of the character just after. This means we could use substring(start, end-start) to extract each match from the original string. This is essentially how the group method does that for us.

Now that we can use find to iterate over matches, let's process our tokens.

3. Replacing Matches One by One

Let's continue our example by using our algorithm to replace each title word in the original string with its lowercase equivalent. This means our test string will be converted to:

"first 3 capital words! then 10 TLAs, i found"

The Pattern and Matcher class can't do this for us, so we need to construct an algorithm.

3.1. The Replacement Algorithm

Here is the pseudo-code for the algorithm:

  • Start with an empty output string
  • For each match:
    • Add to the output anything that came before the match and after any previous match
    • Process this match and add that to the output
    • Continue until all matches are processed
    • Add anything left after the last match to the output

We should note that the aim of this algorithm is to find all non-matched areas and add them to the output, as well as adding the processed matches.

3.2. The Token Replacer in Java

We want to convert each word to lowercase, so we can write a simple conversion method:

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

Now we can write the algorithm to iterate over the matches. This can use a StringBuilder for the output:

int lastIndex = 0; StringBuilder output = new 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(); } if (lastIndex < original.length()) { output.append(original, lastIndex, original.length()); } return output.toString();

We should note that StringBuilder provides a handy version of append that can extract substrings. This works well with the end property of Matcher to let us pick up all non-matched characters since the last match.

4. Generalizing the Algorithm

Now that we've solved the problem of replacing some specific tokens, why don't we convert the code into a form where it can be used for the general case? The only thing that varies from one implementation to the next is the regular expression to use, and the logic for converting each match into its replacement.

4.1. Use a Function and Pattern Input

We can use a Java Function object to allow the caller to provide the logic to process each match. And we can take an input called tokenPattern to find all the tokens:

// same as before while (matcher.find()) { output.append(original, lastIndex, matcher.start()) .append(converter.apply(matcher)); // same as before

Here, the regular expression is no longer hard-coded. Instead, the converter function is provided by the caller and is applied to each match within the find loop.

4.2. Testing the General Version

Let's see if the general method works as well as the original:

assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found", TITLE_CASE_PATTERN, match -> match.group(1).toLowerCase())) .isEqualTo("first 3 capital words! then 10 TLAs, i found");

Here we see that calling the code is straightforward. The conversion function is easy to express as a lambda. And the test passes.

Now we have a token replacer, so let's try some other use cases.

5. Some Use Cases

5.1. Escaping Special Characters

Let's imagine we wanted to use the regular expression escape character \ to manually quote each character of a regular expression rather than use the quote method. Perhaps we are quoting a string as part of creating a regular expression to pass to another library or service, so block quoting the expression won't suffice.

If we can express the pattern that means “a regular expression character”, it's easy to use our algorithm to escape them all:

Pattern regexCharacters = Pattern.compile("[]"); assertThat(replaceTokens("A regex character like [", regexCharacters, match -> "\\" + match.group())) .isEqualTo("A regex character like \\[");

For each match, we prefix the \ character. As \ is a special character in Java strings, it's escaped with another \.

Indeed, this example is covered in extra \ characters as the character class in the pattern for regexCharacters has to quote many of the special characters. This shows the regular expression parser that we're using them to mean their literals, not as regular expression syntax.

5.2. Replacing Placeholders

A common way to express a placeholder is to use a syntax like ${name}. Let's consider a use case where the template “Hi ${name} at ${company}” needs to be populated from a map called placeholderValues:

Map placeholderValues = new HashMap(); placeholderValues.put("name", "Bill"); placeholderValues.put("company", "Baeldung");

All we need is a good regular expression to find the ${…} tokens:

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

is one option. It has to quote the $ and the initial curly brace as they would otherwise be treated as regular expression syntax.

At the heart of this pattern is a capturing group for the name of the placeholder. We've used a character class that allows alphanumeric, dashes, and underscores, which should fit most use-cases.

However, to make the code more readable, we've named this capturing groupplaceholder. Let's see how to use that named capturing group:

assertThat(replaceTokens("Hi ${name} at ${company}", "\\$\\{(?[A-Za-z0-9-_]+)}", match -> placeholderValues.get(match.group("placeholder")))) .isEqualTo("Hi Bill at Baeldung");

Here we can see that getting the value of the named group out of the Matcher just involves using group with the name as the input, rather than the number.

6. Conclusion

Dalam artikel ini, kami melihat bagaimana menggunakan ungkapan biasa yang kuat untuk mencari token dalam rentetan kami. Kami belajar bagaimana kaedah cari berfungsi dengan Matcher untuk menunjukkan padanannya kepada kami.

Kemudian kami membuat dan membuat generalisasi algoritma untuk membolehkan kami melakukan penggantian token demi token.

Akhirnya, kami melihat beberapa kes penggunaan umum untuk melarikan watak dan templat yang terisi.

Seperti biasa, contoh kod boleh didapati di GitHub.