クラスライブラリ応用

正規表現

正規表現とは?

正規表現 (regular expression) とは、文字列の集合(パターン)を表す汎用的な記法です。 例えば、文字列「book または cook」のことを正規表現で [bc]ook と書くことができます。 [bc] は 「b か c かどちらか 1 文字」という意味です。

正規表現などで表される文字列パターンに合致する文字列を探すことを、 パターンマッチと言います。 Java、Perl、Ruby、Python などの言語は正規表現を扱うことができるため、 パターンマッチを行うプログラムを容易に作成することができます。

正規表現は Java が生まれる前からあるものです。 興味のある人は、UNIX のコマンド grep、sed、awk を調べてみましょう。

正規表現の記法

記法については、正規表現のパターンを表すクラス java.util.regex.Pattern の説明文を参照してください。 ここではよく使う記法について例を挙げます。

文字

正規表現の例意味マッチする例
通常の文字そのままの文字「abc」なら「abc」
.任意の1文字「a」でも「b」でも何でも
\n改行
\tタブ
\. \( \) \[ \] \^ \$ \\記号ではなく文字「.」「(」「)」「[」「]」「^」「$」「\」

Java のソースコード内の文字列定数(""内)では、 ただ「\」と書くと文字列定数としての特殊記号として扱われてしまうため、 そもそも文字「\」を「\\」と書く必要があります。 よって、正規表現としての「\.」は「\\.」、「\\」は「\\\\」などと書く必要があります。

文字クラス

正規表現の例意味マッチする例
[abc]abcのうち1文字「a」「b」「c」のいずれか
[^abc]abc以外の1文字「d」など
[A-Za-z]AからZ、aからzのうちの1文字「d」など
\d数字1文字「0」から「9」までのいずれか
\w英数字1文字「a」「B」「7」など
\s空白文字1文字空白、改行、タブなど
\p{InBasicLatin}ラテン文字1文字「a」「B」記号など
\p{InHiragana}平仮名1文字「あ」「ん」など
\p{InKatakana}片仮名1文字「ア」「ン」など
\p{InCJKUnifiedIdeographs}漢字1文字「漢」「字」など

上記の \p{...} はどれも Unicodeブロックと呼ばれるものです。

境界

正規表現の例意味マッチする例
^行頭「^un」は「un」で始まる文字列
$行末「ing$」は「ing」で終わる文字列

数量子

正規表現の例意味マッチする例
?直前の文字が0個か1個「e?grep」は「grep」か「egrep」
*直前の文字が0個以上「.*」は 0文字以上の任意の文字列
+直前の文字が1個以上「.+」は 1文字以上の任意の文字列
{n}直前の文字がn個「a{2}」は「aa」
{n,}直前の文字がn個以上「a{2,}」は「aa」「aaa」...
{n,m}直前の文字がn個以上,m個以下「a{2,3}」は「aa」「aaa」
+?直前の文字が1個以上,ただしマッチする最小の範囲

論理演算子

正規表現の例意味備考
tdu|dendaitdu または dendai範囲を区切るため全体を()で囲むことが多い
()グループ分けgroup(n)でn番目のグループにマッチした文字列を参照できる

問題 Google の検索結果のページの下部に、 検索結果ページの何ページ目かを示す Goooooogle といった文字列がある。 文字 o の数は検索結果のページ数によって変化するが、2個以上、20個以下であるように見受けられる(2011年10月現在)。 この文字列を表す正規表現を答えよ。

問題 与えられたファイルが HTMLファイルであるか、 ファイル名から判断することにする。 HTML ファイルであればマッチする正規表現を答えよ。 HTML ファイルの拡張子は、.html と .htm の2種類とする。

Javaにおける正規表現

Javaには正規表現に関する機能を実現するためのパッケージ java.util.regex があります (Java SDK 1.4 で導入)。 このパッケージにより、強力な正規表現の機能を持つ Perl とほぼ同等の機能が実現されています。

Java では正規表現を扱う際に PatternMatcher の2つのクラスを用います。どちらも java.util.regex パッケージに属するクラスです。

クラス Pattern のメソッド

static Pattern compile(String regex)
指定された正規表現 regex をパターンにコンパイルし、Pattern クラスのインスタンスとして返す。
Matcher matcher(CharSequence input)
指定された入力 input と、このパターンとをマッチする正規表現エンジンを作成する。 String や StringBuffer クラスは CharSequence インタフェースを実装しているので、引数として与えることができる。
static boolean matches(String regex, CharSequence input)
指定された正規表現をコンパイルして、指定された入力とその正規表現をマッチする。
String[] split(CharSequence input)
このパターンを区切り文字列として、指定された入力シーケンスを分割する。

クラス Matcher のメソッド

int end()
最後にマッチした文字のインデックスに 1 を加えたものを返す。
int end(int group)
前回のマッチ操作で指定されたグループによって前方参照された部分シーケンスの、最後の文字のインデックスに 1 を加えたものを返す。
boolean find()
入力シーケンスからこのパターンとマッチする次の部分シーケンスを検索する。
boolean find(int start)
この正規表現エンジンをリセットし、指定されたインデックス以降の入力シーケンスから、このパターンとマッチする次の部分シーケンスを検索する。
String group()
前回のマッチで一致した入力部分シーケンスを返す。
String group(int group)
前回のマッチ操作で指定されたグループによって前方参照された入力部分シーケンスを返す。
int groupCount()
この正規表現エンジンのパターンに指定されている前方参照を行う正規表現グループの数を返す。
boolean lookingAt()
入力シーケンスの先頭からパターンとマッチする。
boolean matches()
入力シーケンス全体をこのパターンとマッチする。
String replaceAll(String replacement)
パターンとマッチする入力シーケンスの部分シーケンスを、指定された置換文字列に置き換える。
String replaceFirst(String replacement)
パターンとマッチする入力シーケンスの部分シーケンスのうち、最初の部分シーケンスを指定された置換文字列に置き換える。
Matcher reset(CharSequence input)
新しい入力シーケンスを使用してこの正規表現エンジンをリセットする。
int start()
前回のマッチの開始インデックスを返す。
int start(int group)
前回のマッチ操作で指定されたグループによって前方参照された部分シーケンスの、開始インデックスを返す。

テスト用プログラム

キーボードから入力して確認できるプログラムを示します。

import java.util.*;
import java.util.regex.*;

class PatternTester {
    public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);

	System.out.println("テストする正規表現を入力してください。");
	Pattern pattern = Pattern.compile(scanner.nextLine());

	System.out.println("マッチさせる文字列を入力してください(Ctrl-dで終了)。");
	while (scanner.hasNext()) {     // Ctrl-d が来ない限り繰り返す
	    // 1行分の文字列を読み込む
	    String nextLine = scanner.nextLine();
	    // nextLine に対してパターンマッチをする Matcher インスタンスを生成
	    Matcher matcher = pattern.matcher(nextLine);
	    // 先頭から探せる限り探す
	    while (matcher.find()) {
		System.out.println("マッチしました: " + matcher.group());
	    }
	}
    }
}

PatternTester.java

なお、Scanner クラスはそれ自体が正規表現に対応しているため、 Matcher を明示的に使わない書き方もできます。

	System.out.println("マッチさせるテキストを入力してください(Ctrl-dで入力終了)。");
	while(true) {
	    String string = scanner.findInLine(pattern);
	    if(string == null)
		break;
	    else
		System.out.println("マッチしました: " + string);
	}

コード例 - HTMLのタグ除去

HTMLなどのタグを除去します。<.+?> という正規表現で、 < > に囲まれた1文字以上の文字列を表します。 なお、.+? は最短のマッチを表します。 ちょっと考えると .+ (任意の文字の1文字以上の連続)でよさそうに思えますが、 そうすると以下のような場合、

<em>content</em>

.+ がマッチする範囲は「 em>content</em 」となってしまいます。

import java.util.*;
import java.util.regex.*;

public class TagRemover {
    public static void main(String[] args) {
	Pattern pattern = Pattern.compile("<.+?>", Pattern.DOTALL);
	Scanner scanner = new Scanner(System.in);

	while (scanner.hasNext()) {
	    // 1行分の文字列を読み込む
	    String nextLine = scanner.nextLine();
	    // nextLine に対してパターンマッチをする Matcher インスタンスを生成
	    Matcher matcher = pattern.matcher(nextLine);
	    // 行内のすべてのタグを取り除く
	    String string = matcher.replaceAll("");
	    System.out.println(string);
	}
    }
}

TagRemover.java

メソッド compile の第2引数 Pattern.DOTALL は、 「.」で改行もマッチさせるための指定です。 タグの途中で改行があった場合には有効です。

なお、<.+?> ではなく、<[^>]+> としてもよいはずです。 ここで、[^>] は > 以外の文字1文字を表しています。しかし、 実際には [^>] は > だけでなく漢字などにもマッチしない現象があるため、 うまく動作しません。

コード例 - リンク先の抽出

正規表現では () でグループを表し、 その部分にマッチした文字列を参照することができます。 グループには先頭から 1,2,... と番号がつけられていますので、 group(番号n) で n番目のグループにマッチした文字列が得られます。

import java.util.regex.*;

class LinkFinder {
    public static void main(String args[]) {
	Pattern pattern = Pattern.compile("href=\"(.+?)\"");
	Matcher matcher = pattern.matcher("<a href=\"index.html\">トップへ</a>");
	if(matcher.find())
	    System.out.println(matcher.group(1));
    }
}

LinkFinder.java

String の正規表現関連メソッド

文字列の置換と分割については、 String クラスから直接行えるメソッドが用意されています。

String replaceAll(String regex, String replacement)
指定された正規表現に一致する、この文字列の各部分文字列に対し、指定された置換を実行する。
String replaceFirst(String regex, String replacement)
指定された正規表現に一致する、この文字列の最初の部分文字列に対し、指定された置換を実行する。
String[] split(String regex)
この文字列を、指定された正規表現に一致する位置で分割する。

問題 コマンドライン引数にファイル名を与えると、 その拡張子を取り出すプログラムを作成しなさい。 拡張子とは、ファイル名が「cmd.exe」であれば「exe」である。 java.sun.exe のように「.」が2つ以上ある場合もあるので、 拡張子は、最後の「.」の直後から末尾までの文字列、 と定義することができる。