第 2 回 正規表現、非決定性オートマトン

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

2-1. 正規文法

はじめに、入力データの意味や内容を解析するために、文法理論を学びます。 参考書を選ぶ時は、「コンパイラ論」のようなものが参考になります。

言語と文法

コンピュータで文字(記号)列を解釈するには、その文字列がどのようなルール で作られているかを知る必要があります。 文字列のルールを文法と言います。 そして、ルールに適合する文字列すべての集合を言語と言います。 なお、ルールから適合する文字列を導くことを生成すると言います。 特定の文法を記号 G で表す時、その文法で生成される言語を L(G) と書くこ とがあります。 なお、記号の全体集合をアルファベット と言います。 しばしばアルファベットはΣで表されます。 そして、そのアルファベットΣから作られる記号列全体の集合を Σ* で表します。 なお長さが0の記号列(空列)をεで表します。

アルファベット Σ
使用する記号の集合
Σ*
空列を含む生成可能な記号列すべての集合
文法 G
記号列を生成するルール
言語 L(G)
G にしたがって生成される記号列すべての集合

例2-1

Σ= 0 1
Σ* = ε 0 1 00 01 10 11 000 ...
G= 1 で終らないもの
LG = ε 0 00 10 000 010 ...

正規表現

正規表現を次のように定義します。

  1. ε は正規表現です。
  2. アルファベット一文字は正規表現です。
  3. R と S が正規表現なら、 R | S, RS, {R} は正規表現です({R} は R の 0 回以上の繰返し)。

正規表現は一つの文法になりうるので、正規表現で一つの言語を定義すること ができます。

例2-2

G1 = a
LG1 = ε a aa aaa ...
G2 = a | b | c
LG2 = a b c
G3 = 123456789 0123456789
LG3 = 自然数

メモリーが有限個しか使えないコンピュータが計算できる言語は正規表現だけ であることが知られています。 このように正規表現はプログラムと非常に密接な関係にありますので、さまざ まなところで使用されています。 例えば、 MS-DOS プロンプトやコマンドプロンプトなどでも、ファイル名を指示 する時、不完全ながらも正規表現が使えます。

  1. 文字は正規表現
  2. 正規表現の連結は正規表現
  3. ? は任意の一文字、* は任意の文字列を表します。

但し、これは ab, abab, ababab, ... などの文字列の繰り返しの表現を指定できな いので、完全な意味での正規表現ではありません。 しかし、一応複数のファイル名などを一つの文法で表すことが できます。

演習2-1

コマンドプロンプトまたは MS-DOS プロンプトで c:\windows または c:\winnt ディレクトリの中のファイルのうち、次の条件に当てはまるファイ ル名を dir コマンドを使って表示しなさい。 但し、 Windows や MS-DOS はファイル名を二つの文字列を.(ピリオド)でつな がった形で表しています。 後ろの文字列を拡張子と言います。 例えば「拡張子が txt のファイル名」は dir *.txt で表示します。

  1. a で始まるファイル名
  2. 拡張子が exe のファイル名
  3. ファイル名の前の部分が d で終るファイル名

Java での正規表現

Java ではバージョン 1.4 から java.util.regex パッケージが追加され、次 のような構文で正規表現が使えるようになりました。


 java.util.regex.Pattern p = java.util.regex.Pattern.compile("a*b");
 java.util.regex.Matcher m = p.matcher("aaaaab");
 boolean b = m.matches();

また簡易版として、 java.lang.String クラスに matches メソッドが追加さ れ、次の表記も可能になりました。


 boolean b = "aaaaab".matches("a*b");

以下は Java での正規表現を表し方の例です(詳しくは言語のリファレンス を御覧下さい)。

  1. . は任意の一文字を表す正規表現
  2. ^ は文字列の最初、 $ は文字列の最後を表す正規表現
  3. 英字、数字など特殊文字以外は正規表現
  4. [ と ] に囲まれる文字の列は、それらの文字のうちのどれか一文字を表 す正規表現で、特に文字クラス と呼ばれます。 A-Zという表現で A から Z までのすべての文字を表します。
  5. [^ と ] に囲まれる文字の列は、それらの文字のどれにも含まれない文字 一文字を表す正規表現。 これも同様に A-Z の表現が使えます。
  6. \(バックスラッシュ、または円記号)に一文字を加えるとさまざまなもの を表します。 \n や \t などは C 言語と同じですが、\d で数 [0-9]、 \D で数以外[^0-9]、 \s で空白[ \t\n\x0B\f\r]を表す記号(タブや改行も含む)、\S で空白以外の 文字などを表します。 また、 \\ でバックスラッシュまたは円記号を表します。
  7. \p{名前} は POSIX character classes を指定するのに使います。 \p{Lower} は小文字[a-z]、\p{Upper}は大文字 [A-Z]、\p{Alpha} はアルファベット[\p{Lower}\p{Upper}]、 \p{Digit}は数字[0-9]、\p{Alnum}は英数字 [\p{Alpha}\p{Digit}]、\p{Space}は空白[ \t\n\x0B\f\r] を表 します。
  8. R,S が正規表現の時、 RS は R の次に S が来ることを示す正規表現
  9. R,S,T が正規表現の時、(R|S|T) で R または S または T のどれかを示 す正規表現
  10. R が一文字を表す正規表現の時、R? で R が 0 回または 1 回を表す正規 表現、R* で R が 0 回以上の繰返しを表す正規表現、 R+ で R が 1 回以上 の繰返しを表す正規表現。 なお、 R が複数の文字を表す正規表現の場合も、 (R)?, (R)*, (R)+ で同様 の指定ができます

演習2-2

以下のアプレットを使って、ランダムな 1000 個の文字列のうち、次の文字列 がいくつ出て来るか調べなさい。但し、文字列の長さは 5 とします。

Java アプレットを読み込めませんでした。
  1. a で始まる文字列
  2. a を含む文字列
  3. 英字で始まり、残りは英字と数字だけの文字列

演習2-3

次の Java の正規表現が受理する言語(文字列の集合)を求めなさい。

  1. a*
  2. [abc]
  3. <[^>]*>
  4. [1-9][0-9]*

演習2-4

次の文字列を表す正規表現を示しなさい。

  1. 先頭が a で始まる文字列
  2. 最後が b で終る文字列
  3. c を含む文字列
  4. abc を含む文字列
  5. abc または def を含む文字列
  6. 先頭が英字または_(アンダースコア)、二文字目以降がある場合英字、数 字、または_(アンダースコア)である文字列(C 言語では名前はこ のルールに従う必要がある)

例えば、単語は、英字で始まって、英字または数字が連続するものですが、そ れは [A-Za-z][A-Za-z0-9]* と書くことができます。あるいは Java ではもっと簡単に \p{Alpha}\p{Alnum}*と書くこともで きます。 これを使うと次のようなプログラムが書けます。


import java.util.regex.*;
class Sample {
  public static void main(String[] args){
    final String sample="This is a pen.";
    final Pattern p = Pattern.compile("\\p{Alpha}\\p{Alnum}*");
    final Matcher m = p.matcher(sample);
    while(m.find()){
      System.out.println(m.group());
    }
  }
}

演習2-5

  1. Java の正規表現で、整数を表しなさい
  2. 次の文字列中の整数を列挙するプログラムを作りなさい
    
    String input="-10+30-20";
    
  3. input 文字列に含まれている整数の数を表示するプログラムを作りなさい

2-2. 非決定性オートマトン

ここまでは正規表現を解釈するツールを利用してきました。 これからは、原理を学ぶために正規表現をプログラムに変換する手法を学びます。 単純な正規表現ならわざわざツールに頼らなくとも簡単なプログラムで解釈で きます。

始めにオートマトンについて学びます。 オートマトンとは次のような有向グラフです。

  1. 開始ノードというノードが一つ指定される
  2. 終了ノードが一つ以上指定される
  3. 各辺には文字が一つ以上割り振られている。
オートマトン例

文字列に対して、開始ノードから一文字ずつたどって終了ノードにたどり着け た時、オートマトンはその文字列を受理すると言います。 一方、辺がなかったり、文字列が終った時に終了ノードではなかった時 拒否すると言います。 オートマトンにより、受理する文字列の集合が決定できます。受理する文字列 の集合をそのオートマトンが受理する言語と言います。

各ノードから出る辺に割り当てられている文字がすべて異なるオートマトンを 決定性オートマトンと言います。 このオートマトンは状態遷移図とも呼びます。 決定性オートマトンは入力により流れが分岐するだけですので、簡単にプログ ラムに変換できます。

一方、各ノードから出る辺に割り当てられている文字にダブりがあったり、 ε(空文字)が割り当てられているオートマトンを非決定性オート マトン と言います。 これから、この講義で非決定性オートマトンの受理する言語と正規文法で定められる言語が 一致することを示します。 具体的には正規文法から非決定 性オートマトンへの変換の仕方、次回の講義では非決定性オートマトンから決 定性オートマトンへの変換法を示します。 (非決定性オートマトンから正規文法への変換方法は省略します)

非決定性オートマトン例

正規文法から非決定性オートマトンへ

帰納法により証明します。

ε を受理する非決定性オートマトンは次の通りである。
Case 1
文字 A を受理するオートマトンは次の通りである。
Case 2
R と S が正規表現であり、それを受理する非決定性オートマトンが存在 する時、R|S を受理する非決定性オートマトンは次の通りである。
Case 3
R と S が正規表現であり、それを受理する非決定性オートマトンが存在 する時、RS を受理する非決定性オートマトンは次の通りである。
Case 4
R が正規表現であり、それを受理する非決定性オートマトンが存在 する時、{R} を受理する非決定性オートマトンは次の通りである。
Case 5

(証明終)

例2-3

  1. 正規表現 a は次のような非決定性オートマトンになります。

    a
  2. 正規表現 ab は次のような非決定性オートマトンになります。

    ab

    但し、このオートマトンでεを考える必要は無いので、省略できます。 このように状態数の少ない等価なオートマトンを導くことを 簡単化と言います。

    ab
  3. 正規表現 (ab|ac) は次のような非決定性オートマトンになります。

    (ab|ac)

    但し、これも最初の入力はどちらも a なので εを考える必要はありません。 また、次の入力 b, c でともに受理状態に入ればいいので、こちらも省略できます。 そのため次のように簡単化できます。

    (ab|ac)
  4. 正規表現 (ab|ac)* は次のような非決定性オートマトンになります。

    (ab|ac)*

    これもこのように簡単化できます。

    ab

演習2-6

次の正規表現を受理する非決定性オートマトンを作りなさい。

  1. a*
  2. [abc]
  3. (abc|asd)
  4. .*(a|b)?
  5. 1[01]*

2-3. 付録 Regtest.java


package regtest;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.Hashtable;
public class Regtest extends JApplet {
    private Container contentPane;
    private ResultPanel resultPanel;
    private ControlPanel controlPanel;
    private StringGenerator stringGenerator;
    public void init(){
	contentPane = getContentPane();
	resultPanel = new ResultPanel();
	contentPane.add(resultPanel.getPanel(),BorderLayout.NORTH);
	controlPanel = new ControlPanel();
	contentPane.add(controlPanel.getMiddlePanel(),BorderLayout.CENTER);
	contentPane.add(controlPanel.getSliderPanel(),BorderLayout.SOUTH);

	stringGenerator = new StringGenerator(controlPanel, resultPanel);
	controlPanel.setStartButtonAction(stringGenerator.getStartAction());
	controlPanel.setClearButtonAction(resultPanel.getClearAction());
    }
}
class ResultPanel {
    final private JPanel resultPanel;
    final private ResultArea okArea, ngArea;
    final private ClearAction clearAction;
    public ResultPanel(){
	resultPanel=new JPanel();
	okArea = new ResultArea("適合");
	ngArea = new ResultArea("不適合");
	resultPanel.add(okArea.getPanel());
	resultPanel.add(ngArea.getPanel());
	clearAction = new ClearAction();
    }
    public void add(String str, boolean b){
	if(b){
	    okArea.append(str);
	}else{
	    ngArea.append(str);
	}
    }
    public JPanel getPanel(){
	return resultPanel;
    }
    public ActionListener getClearAction(){
	return clearAction;
    }
    class ClearAction implements ActionListener {
	public ClearAction(){}
	public void actionPerformed(ActionEvent event){
	    okArea.clear();
	    ngArea.clear();
	}
    }
}
class ResultArea {
    final private JPanel panel;
    final private JTextArea textArea;
    final private JScrollPane scrollPane;
    final private JLabel label,counter;
    private int count;
    public ResultArea(String aLabel){
	count=0;
	panel = new JPanel();
	panel.setLayout(new BorderLayout());
	textArea = new JTextArea(10,20);
	scrollPane = new JScrollPane(textArea);
	label = new JLabel(aLabel,SwingConstants.CENTER);
	counter = new JLabel("0");
	counter.setHorizontalAlignment(SwingConstants.CENTER);
	panel.add(scrollPane,BorderLayout.CENTER);
	panel.add(label,BorderLayout.NORTH);
	panel.add(counter,BorderLayout.SOUTH);
    }
    public JPanel getPanel(){
	return panel;
    }
    public void append(String str){
	textArea.append(str+"\n");
	this.addCounter();
    }
    private void addCounter(){
	counter.setText(Integer.toString(++count));
    }
    public void clear(){
	textArea.setText("");
	counter.setText("0");
	count=0;
    }
}
class ControlPanel {
    final private JPanel middlePanel,sliderPanel;
    final private RegField regField;
    final private Executer executer;
    public ControlPanel(){
	regField = new RegField();
	executer = new Executer();
	middlePanel=new JPanel();
	sliderPanel = new JPanel();

	middlePanel.add(regField.getTextField());
	middlePanel.add(regField.getLabel());
	middlePanel.add(executer.getStartButton());
	middlePanel.add(executer.getClearButton());

	sliderPanel.add(new JLabel("長さ:"));
	sliderPanel.add(regField.getSlider());
	sliderPanel.add(new JLabel("回数:"));
        sliderPanel.add(executer.getNumberSlider());
    }
    public int getTimes(){
	return executer.getTimes();
    }
    public void setStartButtonAction(ActionListener al){
	executer.setStartButtonAction(al);
    }
    public void setClearButtonAction(ActionListener al){
	executer.setClearButtonAction(al);
    }
    public int getLength(){
	return regField.getLength();
    }
    public String getText(){
	return regField.getText();
    }
    public JPanel getMiddlePanel(){
	return middlePanel;
    }
    public JPanel getSliderPanel(){
	return sliderPanel;
    }
    public void setErrorMessage(String str){
	regField.setErrorMessage(str);
    }
}
class RegField {
    final private JTextField inputArea;
    final private JSlider lengthSlider;
    final private JLabel  errorMessage;
    public RegField(){
	inputArea = new JTextField("正規表現を入れてください",20);
	errorMessage = new JLabel();
	lengthSlider = new JSlider(1,20,5);
	lengthSlider.setMajorTickSpacing(5);
	lengthSlider.setMinorTickSpacing(1);
	lengthSlider.setPaintTicks(true);
	lengthSlider.setSnapToTicks(true);
	lengthSlider.setPaintLabels(true);
    }
    public int getLength(){
	return lengthSlider.getValue();
    }
    public JLabel getLabel(){
	return errorMessage;
    }
    public void setErrorMessage(String str){
	errorMessage.setText(str);
    }
    public JSlider getSlider(){
	return lengthSlider;
    }
    public JTextField getTextField(){
	return inputArea;
    }
    public String getText(){
	return inputArea.getText();
    }
}
class Executer {
    final private JButton startButton,clearButton;
    final private JSlider numberSlider;
    public Executer(){
	startButton = new JButton("Start");
	clearButton = new JButton("Clear");
	numberSlider = new JSlider(1,5,2);
	final Hashtable<Integer,JLabel> table = new Hashtable<Integer,JLabel>();
	table.put(1,new JLabel(Integer.toString(pow10(1))));
	table.put(5,new JLabel(Integer.toString(pow10(5))));
	numberSlider.setLabelTable(table);
	numberSlider.setMinorTickSpacing(1);
	numberSlider.setPaintTicks(true);
	numberSlider.setPaintLabels(true);
	numberSlider.setSnapToTicks(true);
    }
    public JSlider getNumberSlider(){
	return numberSlider;
    }
    public int getTimes(){
	return pow10(numberSlider.getValue());
    }
    public JButton getStartButton(){
	return startButton;
    }
    public JButton getClearButton(){
	return clearButton;
    }
    public void setStartButtonAction(ActionListener al){
	startButton.addActionListener(al);
    }
    public void setClearButtonAction(ActionListener al){
	clearButton.addActionListener(al);
    }
    private int pow10(int j){
	int ret=1;
	for(int i=0; i<j ; i++){
	    ret *=10;
	}
	return ret;
    }
}
class StringGenerator {
    final private ControlPanel controlPanel;
    final private ResultPanel resultPanel;
    final private ActionListener startButtonAction;
    final private char[]
	letter = {'a','b','c','d','e','f','g','h','i','j','k','l',
		  'm','n','o','p','q','r','s','t','u','v','w','x',
		  'y','z','A','B','C','D','E','F','G','H','I','J',
		  'K','L','M','N','O','P','Q','R','S','T','U','V',
		  'W','X','Y','Z','0','1','2','3','4','5','6','7',
		  '8','9',' ','\t','\"','#','$','%','&','\'',
		  '(',')','-','=','^','~','\\','|','`','@','{','[',
		  ']','}',';','+',':','*',',','<','.','>','/','?',
		  '_'};
    public StringGenerator(ControlPanel cp, ResultPanel rp){
	controlPanel = cp;
	resultPanel = rp;
	startButtonAction = this.new StartButtonAction();
    }
    public ActionListener getStartAction(){
	return startButtonAction;
    }
    private char getChar(){
	return letter[(int)(Math.random()*letter.length)];
    }
    public String getString(int len){
	final StringBuffer retStr=new StringBuffer(len);
	for(int i=0; i<len; i++){
	    retStr.append(getChar());
	}
	return retStr.toString();
    }
    class StartButtonAction implements ActionListener {
	public StartButtonAction(){}
	public void actionPerformed(ActionEvent event){
	    final int times = controlPanel.getTimes();
	    final String regex = controlPanel.getText();
	    controlPanel.setErrorMessage("");
	    try{
		for(int i=0; i<times; i++){
		    final String randomString=getString(controlPanel.getLength());
		    resultPanel.add(randomString,randomString.matches(regex));
		}
	    }catch(Exception e){
		controlPanel.setErrorMessage("正規表現に誤りがあります");
	    }
	}
    }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科