Javaで正規表現を使った文字列置換

こんにちは。Nemoです。

今回、Javaで正規表現による文字列置換を行う際、String java.lang.String.replaceAll()を使った置換を実装を行いました。その時の云々を。

まずは、replaceAllメソッドを使った実装例です。

String str = “★$01234$★$98765$★”;
str = str.replaceAll(“\\$(\\d{5})\\$”, “Hello!”);
System.out.println(str);
★Hello!★Hello!★

という単純な実装ですが、このreplaceAllメソッドを利用した置換の場合、期待する結果が得られない場合があります。

String str = “★$01234$★$98765$★”;
str = str.replaceAll(“\\$(\\d{5})\\$”, “$1”);
System.out.println(str);

置換結果が★$1★$1★になると思われますが、実際には

★01234★98765★

という結果になります。また次のパターンの場合、

String str = “★$01234$★$98765$★”;
str = str.replaceAll(“\\$(\\d{5})\\$”, “\\”);
System.out.println(str);

java.lang.StringIndexOutOfBoundsExceptionが発生してしまいます。

これはreplaceAllメソッド内で、java.util.regex.MatcherクラスのreplaceAllメソッドを呼んでいます。
その中で\\はエスケープ文字、$は正規表現グループを参照しようとする為です。(実際はjava.util.regex.MatcherクラスのappendReplacementメソッド内で判定を行なっています。)
これだと「$」を含む文字列に置換したい場合などにはちょっと使えません。

少し話はそれますが、String java.lang.Stringクラスでは他にreplaceメソッドがあります。
もちろん、このreplaceメソッドは正規表現による置換は行いません。ですので、

String str = “★$01234$★$98765$★”;
str = str.replace(“\\$(\\d{5})\\$”, “$1”);
System.out.println(str);

の結果は、

★$01234$★$98765$★

です。

このreplaceメソッド自身も実際は、replaceAllメソッドと同様に、java.util.regex.MatcherクラスのreplaceAllメソッドを呼んでいます。

String java.lang.String.replace

public String replace(CharSequence target, CharSequence replacement) {return Pattern.compile(target.toString(),Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString()));}

しかし、replaceメソッドでは検索する文字列を、java.util.regex.Patternクラスのcompileを実施する際にリテラルで指定することで、正規表現による検索を回避し、また置換する文字列を、java.util.regex.MatcherクラスのquoteReplacementメソッドを通すことで、\\と$をエスケープしています。

このことから、replaceメソッドでは正規表現による置換を行なっていないため、置換したいパターンが複数ある場合には

String str = “★$01234$★$98765$★”;
str = str.replace(“$01234$”, “$1”);
str = str.replace(“$98765$”, “$1”);
System.out.println(str);

と、複数の置換処理を行う必要があります。

不便ですね。

ということは、replaceAllメソッドとreplaceメソッドを組み合わせることで、正規表現による置換が可能です。(java.util.regex.MatcherクラスのquoteReplacementメソッドを通した置換文字列を、String java.lang.String.replaceAllメソッドの置換文字列に渡す)

String str = “★$01234$★$98765$★”;
str = str.replaceAll(“\\$(\\d{5})\\$”, Matcher.quoteReplacement(“$1”));
System.out.println(str);

この置換を用いた場合、replaceメソッドを複数実施するパターンと比べても、十分なパフォーマンスが期待されます。以下が検証した結果です。

検証に利用する文字列は以下の20,000行の文字列です。

00001 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00000$abcdefghijklmnopqrstuvwxyz・・・00100 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00099$abcdefghijklmnopqrstuvwxyz
00101 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00000$abcdefghijklmnopqrstuvwxyz・・・20000 : BCDEFGHIJKLMNOPQRSTUVWXYZ$00099$abcdefghijklmnopqrstuvwxyz

置換処理内容は、「$で囲まれた5桁の数値を”Hello!”に置換」としましょう。

検証ロジック1(単純な正規表現での検索+置換ロジック)

String str = ※検証に利用する文字列※
String regex = “\\$(\\d{5})\\$”;
String replacement = “Hello!”;
long start = System.currentTimeMillis();
Matcher m = Pattern.compile(regex).matcher(str);
List<String> targets = new ArrayList<String>();
while (m.find()) {if (targets.contains(m.group(0))) {continue;}targets.add(m.group(0));}
for (String s : targets) {str = str.replace(s, replacement);}
long stop = System.currentTimeMillis();
System.out.println(String.format(“—[%dms]”, (stop – start));

検証ロジック2(replaceAllメソッドとreplaceメソッドの組み合わせ)

String str = ※検証に利用する文字列※
String regex = “\\$(\\d{5})\\$”;
String replacement = “Hello!”;
long start = System.currentTimeMillis();
String rt = str.replaceAll(regex, Matcher.quoteReplacement(replacement));
long stop = System.currentTimeMillis();
System.out.println(String.format(“—[%dms]”, (stop – start));

測定結果(ms)

 10回測定した平均
検証ロジック113,619ms
検証ロジック2263.9ms

歴然ですね。

折角なので、プロファイル結果を見てみました。

検証ロジック1では、最もCPUを使用しているメソッド(約40%強)は「java.lang.AbstractStringBuilder.append」。詳細を見てみると、呼び出し元は「java.util.regex.Matcher.replaceAll」でした。

TRACE 300279:java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:482)java.lang.StringBuffer.append(StringBuffer.java:309)java.util.regex.Matcher.appendReplacement(Matcher.java:838)java.util.regex.Matcher.replaceAll(Matcher.java:905)・・・
rank   self  accum   count trace method
1 42.38% 42.38%     353 300279 java.lang.AbstractStringBuilder.append

また、検証ロジック2のプロファイル結果はどうだったかというと、こちらも最もCPUを使用しているメソッド(約10%)も「java.util.regex.Matcher.replaceAll」でした。

TRACE 300262:java.util.regex.Matcher.replaceAll(Matcher.java:906)java.lang.String.replaceAll(String.java:2210)・・・
rank   self  accum   count trace method
1  9.09%  9.09%       5 300262 java.util.regex.Matcher.replaceAll

パフォーマンスの結果に差異はありますが、この結果から、どちらのロジックでもjava.util.regex.Matcher.replaceAllがボトルネックになっていることがわかります。

参考までに、java.util.regex.Matcher.replaceAllを利用しないパターンの実装です。

検証ロジック3

String str = ※検証に利用する文字列※
String regex = “\\$(\\d{5})\\$”;
String replacement = “Hello!”;
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
Matcher m = Pattern.compile(regex).matcher(str);
int s = 0;
while (m.find()) {sb.append(str.substring(s, m.start()));sb.append(replacement);s = m.end();}
if (s < str.length()) {sb.append(str.substring(s, str.length()));}
long stop = System.currentTimeMillis();
System.out.println(String.format(“—[%dms]”, (stop – start));

測定結果(ms)

 10回測定した平均
検証ロジック113,619ms
検証ロジック2263.9ms
検証ロジック3229.8ms

というように、java.util.regex.Matcher.replaceAllを利用しない場合、幾らかですがパフォーマンスの改善が見られました。

まとめ
・String java.lang.String.replaceAllはjava.util.regex.Matcher.quoteReplacementを組み合わせることで正規表現を使った置換が可能。
・java.util.regex.Matcher.replaceAllメソッドを使わない置換では、多少のパフォーマンス改善が見られる。

またね。 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です