Javaで正規表現を使用して文字列のトークンを置き換える方法
1. 概要
Javaで文字列内の値を検索または置換する必要がある場合、通常は正規表現を使用します。 これらにより、文字列の一部またはすべてがパターンに一致するかどうかを判断できます。 MatcherとStringの両方で、replaceAllメソッドを使用して、文字列内の複数のトークンに同じ置換を簡単に適用できます。
このチュートリアルでは、文字列で見つかったトークンごとに異なる置換を適用する方法について説明します。 これにより、特定の文字をエスケープしたり、プレースホルダー値を置き換えたりするなどのユースケースを簡単に満たすことができます。
また、トークンを正しく識別するために正規表現を調整するためのいくつかのトリックについても見ていきます。
2. マッチを個別に処理する
トークンごとの置換アルゴリズムを構築する前に、正規表現に関するJavaAPIを理解する必要があります。 キャプチャグループと非キャプチャグループを使用して、トリッキーなマッチングの問題を解決しましょう。
2.1. タイトルケースの例
文字列内のすべてのタイトルワードを処理するアルゴリズムを構築したいとします。 これらの単語は、大文字の1文字で始まり、小文字のみで終わるか、続きます。
私たちの入力は次のようになります:
"First 3 Capital Words! then 10 TLAs, I Found"
タイトルワードの定義から、これには一致が含まれます。
- 初め
- 資本
- 言葉
- I
- 見つかった
そして、このパターンを認識する正規表現は次のようになります。
"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"
これを理解するために、それを構成要素に分解してみましょう。 真ん中から始めましょう:
[A-Z]
単一の大文字を認識します。
1文字の単語または小文字が続く単語を許可するため、次のようになります。
[a-z]*
0個以上の小文字を認識します。
場合によっては、上記の2つの文字クラスでトークンを認識できます。 残念ながら、この例のテキストには、複数の大文字で始まる単語があります。 したがって、見つけた単一の大文字は、文字以外の文字の後に最初に表示される必要があることを表現する必要があります。
同様に、単一の大文字の単語を許可するので、見つけた単一の大文字が複数大文字の単語の最初であってはならないことを表現する必要があります。
[^ A-Za-z] という表現は、「文字なし」を意味します。 キャプチャしないグループの式の先頭に、これらの1つを配置しました。
(?<=^|[^A-Za-z])
非キャプチャグループ、で始まる (?<=、 ありません後ろを振り返って、一致が正しい境界に表示されることを確認します。 最後の対応するものは、後続の文字に対して同じ仕事をします。
ただし、単語が文字列の最初または最後に触れる場合は、それを考慮する必要があります。これを追加しました^ | 最初のグループに「文字列または文字以外の文字の先頭」を意味するようにし、最後の非キャプチャグループの末尾に| $を追加して、文字列の末尾を境界にすることができます。 。
find を使用すると、キャプチャされていないグループで見つかった文字がマッチに表示されません。
このような単純なユースケースでも多くのエッジケースが存在する可能性があるため、正規表現をテストすることが重要であることに注意してください。 このために、単体テストを記述したり、IDEの組み込みツールを使用したり、Regexrなどのオンラインツールを使用したりできます。
2.2. 例のテスト
EXAMPLE_INPUT という定数のサンプルテキストと、 TITLE_CASE_PATTERNというPattern の正規表現を使用して、でfindを使用しましょう。 ] Matcher クラスを使用して、単体テストですべての一致を抽出します。
Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List<String> matches = new ArrayList<>();
while (matcher.find()) {
matches.add(matcher.group(1));
}
assertThat(matches)
.containsExactly("First", "Capital", "Words", "I", "Found");
ここでは、Patternのmatcher関数を使用して、Matcherを生成します。 次に、 find メソッドをループで使用して、 true の戻りを停止し、すべての一致を繰り返します。
findがtrueを返すたびに、Matcherオブジェクトの状態が現在の一致を表すように設定されます。 group(0) を使用して一致全体を検査したり、1ベースのインデックスを使用して特定のキャプチャグループを検査したりできます。 この場合、必要なピースの周囲にキャプチャグループがあるため、 group(1)を使用して一致をリストに追加します。
2.3. マッチャーをもう少し調べます
これまでのところ、処理したい単語を見つけることができました。
ただし、これらの各単語が置き換えたいトークンである場合、結果の文字列を作成するには、一致に関する詳細情報が必要になります。 Matcherの他のいくつかのプロパティを見てみましょう。
while (matcher.find()) {
System.out.println("Match: " + matcher.group(0));
System.out.println("Start: " + matcher.start());
System.out.println("End: " + matcher.end());
}
このコードは、各一致がどこにあるかを示します。 また、 group(0)の一致も表示されます。これは、キャプチャされたすべてのものです。
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
ここでは、各一致に期待する単語のみが含まれていることがわかります。 startプロパティは、文字列内のmatchのゼロベースのインデックスを示します。 end は、直後の文字のインデックスを表示します。 これは、 substring(start、end-start)を使用して、元の文字列から各一致を抽出できることを意味します。 これは基本的に、groupメソッドが私たちのためにそれを行う方法です。
find を使用して一致を反復処理できるようになったので、トークンを処理してみましょう。
3. マッチを1つずつ置き換える
アルゴリズムを使用して、元の文字列の各タイトルワードを同等の小文字に置き換えて、例を続けましょう。 これは、テスト文字列が次のように変換されることを意味します。
"first 3 capital words! then 10 TLAs, i found"
PatternおよびMatcherクラスではこれを実行できないため、アルゴリズムを構築する必要があります。
3.1. 置換アルゴリズム
アルゴリズムの擬似コードは次のとおりです。
- 空の出力文字列から開始します
- 試合ごとに:
- 試合の前と前の試合の後に来たものをすべて出力に追加します
- この一致を処理し、それを出力に追加します
- すべての一致が処理されるまで続行します
- 最後の一致の後に残っているものを出力に追加します
このアルゴリズムの目的は、一致しない領域をすべて見つけて、それらを出力に追加し、処理された一致を追加することであることに注意してください。
3.2. Javaのトークンリプレースメント
各単語を小文字に変換したいので、簡単な変換メソッドを記述できます。
private static String convert(String token) {
return token.toLowerCase();
}
これで、一致を繰り返すアルゴリズムを作成できます。 これは、出力にStringBuilderを使用できます。
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();
StringBuilderは、サブストリングを抽出できる便利なバージョンのappendを提供することに注意してください。 これは、Matcherのendプロパティとうまく連携して、最後の一致以降に一致しなかったすべての文字を取得できるようにします。
4. アルゴリズムの一般化
特定のトークンを置き換える問題を解決したので、コードを一般的なケースで使用できる形式に変換してみませんか? 実装ごとに異なるのは、使用する正規表現と、各一致をその置換に変換するためのロジックだけです。
4.1. 関数とパターン入力を使用する
Javaを使用できます関数
// same as before
while (matcher.find()) {
output.append(original, lastIndex, matcher.start())
.append(converter.apply(matcher));
// same as before
ここで、正規表現はハードコーディングされなくなりました。 代わりに、 converter 関数は呼び出し元によって提供され、findループ内の各一致に適用されます。
4.2. 一般バージョンのテスト
一般的な方法が元の方法と同様に機能するかどうかを見てみましょう。
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");
ここでは、コードの呼び出しが簡単であることがわかります。 変換関数はラムダとして簡単に表現できます。 そして、テストに合格します。
これでトークンリプレースメントができたので、他のいくつかのユースケースを試してみましょう。
5. いくつかのユースケース
5.1. 特殊文字のエスケープ
引用メソッドを使用するのではなく、正規表現エスケープ文字\を使用して正規表現の各文字を手動で引用したいとします。 おそらく、別のライブラリまたはサービスに渡す正規表現の作成の一部として文字列を引用しているため、式の引用をブロックするだけでは不十分です。
「正規表現文字」を意味するパターンを表現できれば、アルゴリズムを使用してそれらすべてを簡単にエスケープできます。
Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");
assertThat(replaceTokens("A regex character like [",
regexCharacters,
match -> "\\" + match.group()))
.isEqualTo("A regex character like \\[");
一致するたびに、\文字のプレフィックスを付けます。 \ はJava文字列の特殊文字であるため、別の\でエスケープされます。
実際、 regexCharacters のパターンの文字クラスは多くの特殊文字を引用する必要があるため、この例は余分な\文字でカバーされています。 これは、正規表現構文としてではなく、リテラルを意味するために使用している正規表現パーサーを示しています。
5.2. プレースホルダーの置き換え
プレースホルダーを表現する一般的な方法は、 ${name}のような構文を使用することです。 テンプレート“ Hi $ {name} at $ {company}”がplaceholderValuesというマップから入力する必要があるユースケースを考えてみましょう。
Map<String, String> placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");
必要なのは、${…}トークンを見つけるための適切な正規表現です。
"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}"
1つのオプションです。 $ と最初の中括弧は、正規表現構文として扱われるため、引用符で囲む必要があります。
このパターンの中心には、プレースホルダーの名前のキャプチャグループがあります。 英数字、ダッシュ、アンダースコアを使用できる文字クラスを使用しました。これは、ほとんどのユースケースに適合します。
ただし、コードを読みやすくするために、このキャプチャグループにプレースホルダーという名前を付けました。 その名前付きキャプチャグループの使用方法を見てみましょう。
assertThat(replaceTokens("Hi ${name} at ${company}",
"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}",
match -> placeholderValues.get(match.group("placeholder"))))
.isEqualTo("Hi Bill at Baeldung");
ここで、 Matcher から名前付きグループの値を取得するには、番号ではなく名前を入力としてgroupを使用する必要があることがわかります。
6. 結論
この記事では、強力な正規表現を使用して文字列内のトークンを見つける方法について説明しました。 findメソッドがMatcherとどのように連携して、一致を表示するかを学びました。
次に、トークンごとの置換を実行できるようにするアルゴリズムを作成して一般化しました。
最後に、文字をエスケープしてテンプレートにデータを入力するための一般的なユースケースをいくつか見てきました。
いつものように、コード例はGitHubのにあります。