Cara Menggunakan Ekspresi Reguler untuk Mengganti Token dalam String di Java

1. Ikhtisar

Saat kita perlu mencari atau mengganti nilai dalam sebuah string di Java, kita biasanya menggunakan ekspresi reguler. Ini memungkinkan kita untuk menentukan apakah beberapa atau semua string cocok dengan pola. Kita mungkin dengan mudah menerapkan penggantian yang sama ke beberapa token dalam sebuah string dengan metode replaceAll di Matcher dan String .

Dalam tutorial ini, kita akan mempelajari cara menerapkan pengganti yang berbeda untuk setiap token yang ditemukan dalam sebuah string. Ini akan memudahkan kami memenuhi kasus penggunaan seperti mengonversi karakter tertentu atau mengganti nilai placeholder.

Kami juga akan melihat beberapa trik untuk menyetel ekspresi reguler kami untuk mengidentifikasi token dengan benar.

2. Memproses Pertandingan Secara Individual

Sebelum kita bisa membangun algoritma penggantian token-by-token kita, kita perlu memahami Java API di sekitar ekspresi reguler. Mari kita selesaikan masalah pencocokan yang rumit menggunakan grup penangkap dan non-penangkap.

2.1. Contoh Kasus Judul

Bayangkan kita ingin membangun algoritma untuk memproses semua kata judul dalam sebuah string. Kata-kata ini dimulai dengan satu karakter huruf besar dan kemudian diakhiri atau dilanjutkan dengan hanya karakter huruf kecil.

Masukan kami mungkin:

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

Dari definisi kata judul, ini berisi kecocokan:

  • Pertama
  • Modal
  • Kata-kata
  • saya
  • Ditemukan

Dan ekspresi reguler untuk mengenali pola ini adalah:

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

Untuk memahami ini, mari kita pecahkan menjadi beberapa bagian. Kami akan mulai di tengah:

[A-Z]

akan mengenali satu huruf besar.

Kami mengizinkan kata atau kata berkarakter tunggal diikuti dengan huruf kecil, jadi:

[a-z]*

mengenali nol atau lebih huruf kecil.

Dalam beberapa kasus, dua kelas karakter di atas akan cukup untuk mengenali token kita. Sayangnya, dalam contoh teks kita, ada kata yang diawali dengan banyak huruf kapital. Oleh karena itu, kita perlu menyatakan bahwa satu huruf besar yang kita temukan harus menjadi yang pertama muncul setelah bukan huruf.

Demikian pula, karena kami mengizinkan satu kata huruf kapital, kami perlu menyatakan bahwa satu huruf besar yang kami temukan tidak boleh menjadi yang pertama dari kata banyak huruf kapital.

Ekspresi [^ A-Za-z] berarti “tanpa huruf”. Kami telah meletakkan salah satunya di awal ekspresi dalam grup non-capturing:

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

Kelompok non-menangkap, dimulai dengan (? <=, Melakukan look-balik untuk memastikan muncul pertandingan pada batas yang benar. Its counterpart di akhir melakukan pekerjaan yang sama untuk karakter yang follow.

Namun, jika kata-kata menyentuh awal atau akhir string, maka kita perlu memperhitungkannya, di mana kita telah menambahkan ^ | ke grup pertama untuk membuatnya berarti "awal string atau karakter non-huruf apa pun", dan kami telah menambahkan | $ di akhir grup non-penangkap terakhir untuk memungkinkan akhir string menjadi batas .

Karakter yang ditemukan di grup non-capturing tidak muncul dalam pertandingan saat kami menggunakan find .

Kita harus mencatat bahwa bahkan kasus penggunaan sederhana seperti ini dapat memiliki banyak kasus tepi, jadi penting untuk menguji ekspresi reguler kita . Untuk ini, kita dapat menulis pengujian unit, menggunakan alat bawaan IDE, atau menggunakan alat online seperti Regexr.

2.2. Menguji Teladan Kami

Dengan teks contoh kita dalam konstanta yang disebut EXAMPLE_INPUT dan ekspresi reguler kita dalam Pola yang disebut TITLE_CASE_PATTERN , mari gunakan find di kelas Matcher untuk mengekstrak semua kecocokan kita dalam pengujian 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 kita menggunakan metode find dalam satu lingkaran sampai berhenti mengembalikan true untuk mengulang semua kecocokan.

Setiap kali find mengembalikan nilai true , status objek Matcher diatur untuk mewakili kecocokan saat ini. Kami dapat memeriksa seluruh pertandingan dengan grup (0) atau memeriksa grup penangkap tertentu dengan indeks berbasis 1 mereka . Dalam hal ini, ada grup penangkap di sekitar bagian yang kami inginkan, jadi kami menggunakan grup (1) untuk menambahkan pertandingan ke daftar kami.

2.3. Memeriksa Matcher Sedikit Lagi

Kami sejauh ini berhasil menemukan kata-kata yang ingin kami proses.

Namun, jika setiap kata ini adalah token yang ingin kita ganti, kita perlu memiliki lebih banyak informasi tentang kecocokan untuk membuat string yang dihasilkan. Mari kita lihat beberapa properti Matcher lainnya yang mungkin dapat membantu kita:

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

Kode ini akan menunjukkan di mana setiap kecocokan. Itu juga menunjukkan kepada kita pertandingan grup (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

Di artikel ini, kami melihat cara menggunakan ekspresi reguler yang kuat untuk menemukan token di string kami. Kami belajar bagaimana metode find bekerja dengan Matcher untuk menunjukkan kepada kami kecocokan.

Kemudian kami membuat dan menggeneralisasi algoritme untuk memungkinkan kami melakukan penggantian token-demi-token.

Akhirnya, kami melihat beberapa kasus penggunaan umum untuk karakter melarikan diri dan mengisi template.

Seperti biasa, contoh kode dapat ditemukan di GitHub.