Go to Previous Page Go to Contents Go to Java Page Go to Next Page
New Features of Java2 SDK, Standard Edition, v1.4
 
 

New I/O 正規表現

 
 

正規表現

 
 

正規表現は UNIX を使ったことのある方ならば、何らかの形で使ったことがあると思います。筆者も以前は UNIX をメインに使っていたので、正規表現はよく使っていました。

最近では、Perl で正規表現を使われている方も多いと思います。

正規表現というのは、簡単に言ってしまえば「文字列を表すためのパターン」といえると思います。正規表現を使用するアプリケーションは、そのパターンに適合しているか調べたり、パターンにあっている部分を抜き出すなどに正規表現を使用しているのです。

ここでは、正規表現の説明は特に行いませんが、興味のある方のために参考書をあげておくだけにとどめておきます。

"Mastering Regular Expressions", Jeffrey E. F. Friedl, O'Railly, ISBN 1-56592-257-3

日本語版は

詳細正規表現」 歌代 和正 監訳, オライリー・ジャパン, ISBN 4-900900-45-1

いわゆる「ふくろう本」です。

今まで Java で正規表現を扱おうとすると、次のようなライブラリを使用する必要がありました。

Xerces は XML のパーサなのですが、パースに正規表現が必要なためにライブラリが含まれているようです。

いろいろなライブラリがあるということは、それだけ望まれているものであるということなのでしょう。ですから、満を持して Java 2 SE に取り入れられたというわけですね。

 

 
  パターン  
 

正規表現を扱うには、正規表現のパターンを扱う必要があります。これには java.util.regex.Pattern クラスを使用します。Pattern クラスで扱える正規表現や Perl の正規表現との違いなどは Pattern クラスの JavaDoc を参照してください。

Pattern オブジェクトを生成するには new を使用するのではなく、static メソッドの compile メソッドを使用します。

    Pattern pattern = Pattern.compile("a*");

パターンが表されれば、ある文字列がパターンにマッチしているかどうかを調べることができます。

アプリケーションのソース PatternMatcher.java

マッチしているかどうかには java.util.regex.Matcher クラスを使用します。

 1:    public PatternMatcher(String regex, String input) {
 2:        try {
 3:            Pattern pattern = Pattern.compile(regex);
 4:            Matcher matcher = pattern.matcher(input);
 5: 
 6:            if (matcher.matches()) {
 7:                System.out.println("[" + input 
 8:                                  + "] matches [" + pattern.pattern() + "]");
 9:            } else {
10:                System.out.println("[" + input 
11:                                  + "] DON'T matches [" + pattern.pattern() + "]");
12:            }
13:        } catch (PatternSyntaxException ex) {
14:            System.err.println(ex.getMessage());
15:            ex.printStackTrace();
16:        }
17:    }

Matcher オブジェクトの生成には Pattern クラスの matcher メソッドを使用します (4 行目)。matcher メソッドの引数はマッチしているかどうかを調べる文字列です。

Matcher オブジェクトが生成できれば、後は簡単。Matcher クラスの matches メソッドでマッチしているかどうかが判定できます(6 行目)。

正規表現を扱うときに発生する例外は PatternSyntaxException です。例えば、compile メソッドの引数の正規表現の文法が間違ったときなどに発生します。

さて、実行してみましょう。プログラムの第 1 引数が正規表現のパターン、第 2 引数がマッチしているかどうかを調べる文字列です。

C:\temp>java PatternMatcher abc abc
[abc] matches [abc]

C:\temp>java PatternMatcher a[a-z]* abc
[abc] matches [a[a-z]*]

C:\temp>java PatternMatcher a* abc
[abc] DON'T matches [a*]

パターンとのマッチには、Pattern クラスの matches メソッドを使用するという方法もあります。これは、Java 2 SDK のソース (Java 2 SDK に付属している src.zip を展開すると見ることができます) を見てみると Matcher クラスの matches メソッドを呼び出しているだけなので、結局は同じことになります。

    public static boolean matches(String regex, CharSequence input) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        return m.matches();
    }

ここで使用されている CharSequence は v1.4 で新たに導入されたインタフェースで String などの文字列を扱うクラスの統一的インタフェースを提供しています。

もう 1 つの方法として、Matcher クラスの lookingAt メソッドを使用する方法もあります。matches メソッドの違いは、matches メソッドが入力文字列全体を調べますが、lookingAt メソッドは先頭から調べていきマッチしていたらそこで終わってしまうというところです。

 

 
  置換  
 

matches メソッドを使うと正規表現と入力が完全にマッチするかどうかを調べます。しかし、実際に使うとしたら、入力中に正規表現とマッチする部分が複数あり、それを探し出すということの方が多いのではないでしょうか。

この使い方の例といえば、ファイルなどの検索や置換があります。ここでは、まず置換を行ってみましょう。

マッチした部分で一番はじめに見つかったものを置換することから行って、次にすべての置換を行ってみたいと思います。

アプリケーションのソース PatternReplacer1.java

一番はじめに見つかったものを置換するには、Matcher クラスの replaceFirst メソッドを使用します。次に示すのは置換のメインの処理を行っている部分です。

 1:    private void replaceFirst(String regex, String replaceText,
 2:                             String input, PrintWriter writer){
 3:        try {
 4:            Pattern pattern = Pattern.compile(regex);
 5:            Matcher matcher = pattern.matcher(input);
 6:
 7:            String result = matcher.replaceFirst(replaceText);
 8:
 9:            writer.print(result);
10:        } catch (PatternSyntaxException ex) {
11:            System.err.println(ex.getMessage());
12:            ex.printStackTrace();
13:            return;
14:        }
15:    }

PatternReplacer1 は入力ファイルから読み込んだ入力を置換して、出力ファイルに書き込みます。ファイル名が省略されていたら、標準入出力を対象にしています。このメソッドは入力ファイルから読み込んだ内容が input で表されています。

Pattern オブジェクトと Matcher オブジェクトを生成する方法は PatternMatcher と同じです (4, 5 行目)。

その後に replaceFirst をコールして置換を行っています。replaceFirst メソッドの戻り値が置換した結果です。

さっそく実行してみます。abcdef... というアルファベットが 5 行かかれている test.txt というファイルを入力としました。出力ファイルは省略したので、標準出力に出力しています。

この正規表現は文字 d で始まって、英数字が 3 文字つながっているものを示しています。これを 123 に置換します。

C:\temp>java PatternReplacer1 d\w{3} 123 test.txt
abc123hjiklmnopqrstuvwxyz
abcdefghjiklmnopqrstuvwxyz
abcdefghjiklmnopqrstuvwxyz
abcdefghjiklmnopqrstuvwxyz
abcdefghjiklmnopqrstuvwxyz

実行結果では 1 行目の defg の部分が正規表現にマッチしているので、そこが 123 になっています。replaceFirst ですから、2 行目以降は置換されていません。

次に、入力全体を置換してみます。はじめだけ置換するには replaceFirst メソッドを使用しましたが、全体を置換するには replaceAll メソッドを使用します。残り部分はすべての前のサンプルと同じです。

アプリケーションのソース PatternReplacer2.java

 1:    private void replaceAll(String regex, String replaceText,
 2:                            String input, PrintWriter writer){
 3:        try {
 4:            Pattern pattern = Pattern.compile(regex);
 5:            Matcher matcher = pattern.matcher(input);
 6:
 7:            String result = matcher.replaceAll(replaceText);
 8:
 9:            writer.print(result);
10:        } catch (PatternSyntaxException ex) {
11:            System.err.println(ex.getMessage());
12:            ex.printStackTrace();
13:            return;
14:        }
15:    }

先ほどと同じ条件で実行してみると次のようになりました。

C:\temp>java PatternReplacer2 d\w{3} 123 test.txt
abc123hjiklmnopqrstuvwxyz
abc123hjiklmnopqrstuvwxyz
abc123hjiklmnopqrstuvwxyz
abc123hjiklmnopqrstuvwxyz
abc123hjiklmnopqrstuvwxyz

最後まで正しく置換されました。

 

 
  検索  
 

置換の後に検索というのも変なのですが、簡易版 grep を作ってみましょう。

検索には replaceFirst/replaceAll メソッドのようなお手軽メソッドはないので、自分である程度処理を記述する必要があります。とはいうものの、それほど大変なわけではないです。

アプリケーションのソース Grep1.java

grep はファイルの行単位でパターンにマッチしている部分があるかどうかを調べ、それを出力します。

入力文字列の中に正規表現にマッチしている部分があるかどうかを調べるには Matcher クラスの find メソッドを使用します。Grep1 の中では次に示すようになっています。少し長いのですが、関連のある部分を示しました。

 1:    private Pattern pattern;
 2:    private Matcher matcher;
 3:
 4:    public Grep1(String regex) throws PatternSyntaxException {
 5:        pattern = Pattern.compile(regex);
 6:    }
 7:
 8:    public void grep(BufferedReader reader) throws IOException {
 9:        int lineNumber = 0;
10: 
11:        while (reader.ready()) {
12:            String line = reader.readLine();
13: 
14:            if (line == null) {
15:                return;
16:            }
17:            
18:            lineNumber++;
19:            grep(line, lineNumber);
20:        }
21:    }
22: 
22:    public void grep(String input, int lineNumber) {
23:        try {
24:            if (matcher == null) {
25:                matcher = pattern.matcher(input);
26:            } else {
27:                matcher.reset(input);
28:            }
29: 
30:            if (matcher.find()) {
31:                String path = getFilePath();
32:                if (path != null) {
33:                    System.out.print(path + ":");
34:                }
35:                System.out.println(lineNumber + ":" + input);
36:            }
37: 
38:        } catch (PatternSyntaxException ex) {
39:            System.err.println(ex.getMessage());
40:            ex.printStackTrace();
41:            return;
42:        }
43:    }

処理の流れとしては、まず Grep1 を生成して、引数の個数によって入力をファイルか標準入力に決め、BufferedReader オブジェクトを生成します。そして、それを引数にして grep メソッドをコールしています。

Grep1 クラスのコンストラクタでは Pattern オブジェクトを生成しています (5 行目)。

次に実際の grep メソッドの処理です。grep は行単位に正規表現とマッチさせるため、readLine メソッドを使用して 1 行づつ読み込みを行います (12 行目)。読み込んだ 1 行の文字列とのマッチングは、もう 1 つの grep メソッドで行っています。

24 から 28 行目では Matcher メソッドを準備しています。一番初めのマッチングを行うときには matcher はまだ null なので、pattern の compile メソッドを使用して Matcher オブジェクトを生成しています (25 行目)。すでにマッチングをしている場合は、前回のマッチングに使用したものが残っているため、これをリセットします (27 行目)。リセットには reset メソッドを使用します。reset メソッドは引数がなければ単にリセットを行いますが、CharSequence オブジェクトの引数がある場合はリセットを行ってから、新たに引数の文字列で初期化します。

次に find メソッドを使用して、入力文字列中に正規表現にマッチした部分があるかどうかを調べています (30 行目)。マッチした部分があれば、ファイル名と行数をつけて入力文字列を出力しています (31 から 35 行目)。

 

 
  もう 1 つの grep  
 

前のサンプルでは find メソッドを使用して、grep を作成してみました。このサンプルでは入力中に正規表現にマッチするかどうかを調べるために find メソッドを使用しましたが、find メソッドにはもう少し高度な使い方があります。

find メソッドは入力文字列の先頭から調べていき、マッチした部分があると、そこで走査を終了して戻ります。もう一度 find メソッドをコールすると、入力文字列の前回調べなかった部分を再び調べていきます。入力文字列に正規表現にマッチする部分があれば、何度でも find メソッドをコールすることができます。n 個のマッチする部分があれば、n 回 find メソッドをコールしてもすべての戻り値は true になり、n + 1 回目で false になります。

この性質を利用して、grep をもう一度作ってみましょう。Grep1 では、ストリームからの 1 行づつ読み込んで処理していましたが、1 行の切り出しも正規表現を使用して行ってみました。

1 行を切り出すためには ".*\r?\n" という正規表現を使用します。

\r は Unicode では \u000D" で復帰文字を表します。\n は\u000A で改行文字です。

.*" は改行以外の任意の文字が 0 回以上繰り返すことを表します。"\r?" は "\r" (\u000D) が 0 もしくは 1 回ということなので、"\r?\n" は "\n", "\r\n" のどちらかになります。

普通は UNIX では改行には \u000A、Windows では \u000D\u000A が使用されるので、"\r?\n" がどちらのパターンも表すことができるのです。

この 1 行切り出しの正規表現を使用したサンプルが Grep2 です。

アプリケーションのソース Grep2.java

Grep1 と異なる 1 行の切り出しの部分のソースを示します。

 1:    public Grep2(String regex) throws PatternSyntaxException {
 2:        pattern = Pattern.compile(regex);
 3:        linePattern = Pattern.compile(".*\r?\n");
 4:    }
 5: 
 6:    public void grep(Reader reader) throws IOException {
 7: 
 8:        String contents = readContents(reader);
 9: 
10:        int lineNumber = 0;
11:        Matcher lineMatcher = linePattern.matcher(contents);
12: 
13:        while (lineMatcher.find()) {
14:            lineNumber++;
15:            String line = lineMatcher.group();
16: 
17:            grep(line, lineNumber);
18:        }
19:    }

1 行切り出しのための正規表現を表すために linePattern というプロパティを使用しました。

切り出し処理は 6 行目からの grep メソッドで行っています。8 行目の readContents メソッドは Reader からの読み込みを行うメソッドです。入力文字列に対応する Matcher オブジェクトを生成して (11 行目)、13 行目からのループで行の切り出しを行います。

13 行目で find を用いて行を探します。あれば、行数を表す line 変数をインクリメントします (14 行目)。15 行目の group メソッドは正規表現にマッチした文字列を返します。行を表す正規表現ですから、group メソッドの戻り値は 1 行が切り出されたものになります。

後は、Grep1 の時と同じ処理になります。

このループは正規表現にマッチする部分がなくなるまで行われます。

Grep1 と Grep2 を同じ条件で動作させてみて、同じ結果になるかどうか確かめてみてください。

 

 
  もう 1 度、置換  
 

エディタやワープロソフトで検索をすると、検索した文字列を示して、まだ検索を続けるかどうかのダイアログが出てきたことはないですか。同じように、置換を行うと、置換される文字列を表示して、置換するかどうかを聞いてくるダイアログが出てきたことはないですか。

前述した PatternReplacer1/PatternReplacer2 では replaceFirst/replaceAll を使用していたため、このようなインタラクティブに置換を行うことはできませんでした。

しかし、find メソッドを使用すればできそうなきがしませんか。find メソッドだけでは実際にはできないのですが、これと他のメソッドを組み合わせれば、同じようなことができそうです。

そこで作ってみたのが、次のサンプル PatternReplacer3 です。

アプリケーションのソース PatternReplacer3.java

PatternReplacer3 では置換だけでなく、検索もインタラクティブに行えるようにしてみました。GUI の部分があるので、ソースは少し長めですが、GUI の部分を除いたロジックの部分はそれほど長いわけではありません。

PatternReplacer3 を立ち上げると、ウィンドウが表示されます。メニューバーの [File] - [Open] でオープンするファイルを決められます。ファイルをオープンしたら、メニューバーの [Edit] - [Search] で検索、[Edit] - [Replace] で置換を行うことができます。

処理の簡単な検索から、どうやっているのか見ていきましょう。検索を行っているのは search メソッドです。

 1:    private void search(String regex){
 2:        try {
 3:            Pattern pattern = Pattern.compile(regex);
 4:            Matcher matcher = pattern.matcher(contents);
 5:
 6:            while (matcher.find()) {
 7:                area.select(matcher.start(), matcher.end());
 8:
 9:                int answer = JOptionPane.showConfirmDialog(frame,
10:                                          "次を検索しますか?",
11:                                          "PatternReplacer",
12:                                          JOptionPane.YES_NO_OPTION);
13:                if (answer != JOptionPane.YES_OPTION){
14:                    break;
15:                }
16:            }
17:
18:            JOptionPane.showMessageDialog(frame, "検索が終了しました");
19:        } catch (PatternSyntaxException ex) {
20:            System.err.println(ex.getMessage());
21:            ex.printStackTrace();
22:            return;
23:        }
24:    }

引数の regex は検索のダイアログで入力された検索する正規表現です。また、contents はウィンドウに表示されているファイルから読み込んだ文字列です。

search メソッドの中で今まで出てこなかったものに Matcher クラスの start, end メソッドがあります。これらのメソッドは直前にコールされた find メソッドでマッチした部分のインデックスを返します。start メソッドがマッチした部分の最初の文字のインデックス、end がマッチした部分の最後のインデックスに 1 を足したものを表します。

そこで、JTextArea クラス型の変数 area の select メソッドをコールして、マッチした部分を選択状態 (文字が反転して表示される) にしています。

選択状態にしたら、まだ検索を行うかどうかのダイアログを出力します。

最後まできたら、終了のダイアログを出力します。

置換はもう少し、複雑になります。というのも逐次的に文字列も置換して表示する必要があるためです。start/end メソッドでマッチした部分の位置は分かりますが、これは入力の文字列に対してです。マッチした部分と置換する文字列の長さが同じであればずれがないのですが、長さが異なると表示にずれが生じてしまいます。これをなくすためにすこし工夫をしています。

 1:    private void replace(String searchText, String replaceText){
 2:        try {
 3:            Pattern pattern = Pattern.compile(searchText);
 4:            Matcher matcher = pattern.matcher(contents);
 5:
 6:            StringBuffer buffer = new StringBuffer();
 7:
 8:            int lastIndex = 0;
 9:
10:            while(matcher.find()) {
12:                int start = buffer.length() + (matcher.start() -lastIndex);
13:                int end = buffer.length() + (matcher.end() - lastIndex);
14:
15:                area.select(start, end);
16:
17:                int answer = JOptionPane.showConfirmDialog(frame,
18:                                    "置換しますか?",
19:                                    "PatternReplacer",
20:                                    JOptionPane.YES_NO_CANCEL_OPTION);
21:
22:                if (answer == JOptionPane.CANCEL_OPTION) {
23:                    break;
24:                } else if (answer == JOptionPane.NO_OPTION) {
25:                    continue;
26:                }
27:
28:                matcher.appendReplacement(buffer, replaceText);
29:                StringBuffer buffer1 = new StringBuffer(buffer.toString());
30:
31:                matcher.appendTail(buffer1);
32:                area.setText(buffer1.toString());
33:
34:                lastIndex = matcher.end();
36:            }
37:
38:            matcher.appendTail(buffer);
39:            area.setText(buffer.toString());
40:
41:            JOptionPane.showMessageDialog(frame, "置換を終了しました");
42:        } catch (PatternSyntaxException ex) {
43:            System.err.println(ex.getMessage());
44:            ex.printStackTrace();
45:        }
46:    }

Pattern オブジェクトや Matcher オブジェクトを生成する方法は同じです。次の、buffer は置換した結果を入れるための変数です (6 行目)。そして、lastIndex が前回置換をした部分の最後に 1 を足したインデックスを保持しています。

この lastIndex を使用して、ずれを修正しているのが 12、13 行目です。前回の置換した部分と、今回置換する部分のインデックスの差をとっています。文字列のはじめからから数えるとずれてしまうので、前回から今回の置換までのインデックスを使用してマッチした部分を反転して表示させているのです。

確認のダイアログを表示して (17 から 20 行目)、YES であれば置換を行います。これを行うのが Matcher クラスの appendReplacement メソッドです。このメソッドの引数は 2 つあり、第 1 引数に第 2 引数の置換文字列で前回の置換から今回置換する部分までを追加してくれます。

そのため、buffer は現時点での置換を終えたところまでしか保持していません。これだと buffer を使うと表示するのに途中までしか行われなくなってしまいます。そこで、まだ走査されていない入力文字列を追加するために appendTail メソッドを使用します。ここでは表示のためにテンポラリに buffer をコピーした buffer1 を使用し、これに残りの入力文字列を加えています。

表示には buffer1 を使用することで、現時点での置換を施した文字列が表示できます。

ループの最後に置換を終えたところに lastIndex を更新します (34 行目)。

最後までいって、ループが抜けたら、あらためて appendTail メソッドを使用して buffer に最後に残った文字列を追加し、それを表示しています (45, 46 行目)。

ぜひ、このサンプルを実行してみて、ちゃんと検索や置換ができることを確かめてみてください。

 

 
 

最後に

 
 

今まで、文字列を扱う必要があると、なんとなく Perl を使ってしまうということはなかったですか。確かに Perl は気軽に使えますし、正規表現もバックリファレンスが使えるなどの拡張がされていたりして便利です。

とはいうものの筆者も、普通は Java を使っているのに、文字列を扱うという理由だけで惰性的に Perl を使ってしまうことが何度かありました。

そんな風に Perl を使っていた何割かは、Java 2 SE, v1.4 で導入された正規表現を使用することで、Java を使おうと思えるかもしれません。Java の正規表現も Perl の正規表現の拡張も使用することができることですし。

ただし、Perl のプログラムをすべてを Java に置き換えることはないとはありえないとは思います。Perl には Perl なりのよさがありますし、Java には Java なりのよさがあるので、どちらか一方ということはないと思います。

しかし、筆者などそうなのですが、なれない Perl を使うより、勝手知ったる Java を使うことが多くなりそうです。

たとえば、今までだったら Perl を使って CGI を書いていたと思われるものも、Servlet を使って書いてみようと思うかもしれないということです。

正規表現は奥が深いので、なかなか使いでがありそうですね。

今回使用したサンプルはここからダウンロードできます。

参考 URL

(Dec. 2001)

 

 
 
Go to Previous Page Go to Contents Go to Java Page Go to Next Page