Processing で正規表現を使った文字列抽出を行ってみる。

Processing プログラミング

正規表現を初めて使ってみようと思った人が感覚を掴みやすそうな技術ブログを見つけられなかったので、Processing 言語を使って正規表現を使ってみる方法を要所だけにざっくりと絞って、順を追って紹介してみます。

プログラミングのライブコーディングを眺めていたときに「正規表現ってどうやって使うんだろう」みたいな場面を目にする機会がありました。 限られた時間の中で調べてみるも良い感じの情報が出てこない様子で、そのときに自分も少し調べてみたのですけれど 今ひとつ手軽な情報が見つからなかったので、正規表現を使う上でのごくごく初歩的なところを整理してみることにします。

そのライブコーディングで使われていたプログラミング言語が何かははっきりとはわからないのですけれど、 配信の中で Java という言葉が出てきたのと、もしかすると Processing でコーディングしていそうな様子だったので、 今回は Processing を使って正規表現を使った文字列処理を順を追ってざっくり行ってみます。

Processing での正規表現

正規表現を使うことで、文字列が特定のパターンにマッチするかを判定したり、特定のパターンに該当する文字列を自由に切り出したりできます。

Java 言語の場合は Pattern クラスで正規表現を使っていくことになるようなのですけれど、 Processing では match 関数や matchAll 関数を使って正規表現を行えるようになっているようです。

扱う文字列データの準備

今回は次のような文字列を、正規表現で取り扱ってみようと思います。

この文字列の構造としては、行ごとにパラメーターが記載されています。 そして各行は、アルファベットで構成されたキー名と、空白を含まない任意の文字で構成された値とが : で区切られています。: と値の間には、1文字以上の空白文字が挟まれます。 また、空行を挟むことで複数のデータブロックを表現します。

id: 1000
name: star
option: none
count: 500

id: 1001
name: moon
option: full
count: 1

このテキストデータを、今回は次のようにして変数 text に格納しておくことにします。 本題とは関係ないですけれど、今回はコードを見たときにデータブロックが多少は区別しやすいように += 演算子を使ってブロック単位で追加する形で記述しておくことにします。

String text = "";

text += "id: 1000\nname: star\noption: none\ncount: 500\n";
text += "\n";
text += "id: 1001\nname: moon\noption: full\ncount: 1\n";
text += "\n";

match 関数を使う

Processing で正規表現を行うときには match 関数を使います。

この関数は、第1引数で指定した文字列の中に、第2引数で指定したパターンが含まれているかを判定します。 そして、含まれていればそのパターンにマッチした文字列を String[] 型で返し、 含まれていなかった場合は null を返します。 なお、パターンにマッチした場合は String 型の配列で結果が返りますけれど、 配列の先頭要素に「パターンにマッチした文字列全体」が、それ以降には「サブパターンにマッチした文字列」が順番に格納されるようになっています。

まずはこれを使って "name" という文字列が変数 text に含まれているかを判定してみます。指定した文字列が含まれていることは、戻り値が null ではないことを 調べればわかります。

if (match(text, "name") != null) {

  println("含まれています。");
}
else {
  
  println("含まれていません。");
}

パターンにマッチした文字列を取り出す

パターンにマッチした文字列は、戻り値の最初の要素を参照することで取得できます。

String[] matchedTexts = match(text, "name");

if (matchedTexts != null) {

  println(matchedTexts[0]);
}

今回の例であれば、変数 text の中に "name" という文字列が含まれているので、 match 関数のパターンとして指定した "name" はマッチします。そしてそのパターンにマッチした部分が、戻り値で受け取った配列の最初の要素、すなわち matchedTexts[0] で得られます。

今回の場合はパターンが純粋な文字列なので、得られる文字列は次の通り、指定したパターンそのものです。

name

name キーが現れる行を切り出す

正規表現が威力を発揮するのは、特殊文字を使ってパターンを記述する場面です。

たとえば、キーに name が現れる行全体にマッチさせたかったとします。 Processing では、文字列を改行で区切られた行単位でパターンを判定するので、パターン "^name:.*?$" で判定できます。ここで使われている特殊文字は次の通りです。

特殊文字 該当するもの
^ 行の先頭
. 任意の文字
*? 直前で指定した文字が 0 個以上で、パターンを満たす中で最も短いもの
$ 行の末尾

つまり、今回のパターンは「行の先頭から "name:" で始まり、任意の文字が 0 個以上続いて、行末まで」という正規表現になります。 このパターンで match 関数を使うと、該当する行全体が matchedTexts[0] で得られます。

String[] matchedTexts = match(text, "^name:.*?$");

if (matchedTexts != null) {

  println(matchedTexts[0]);
}

つまり、今回の例では次の結果が得られます。

name: star

name キーに対応する値を切り出す

サブパターンを使うと、パターンにマッチした文字列のうちの任意の部分を取得できます。 たとえば "name:" の先から行末までを取得したい場合は、該当する部分を特殊文字 "("")" で括ったサブパターンを使います。 ただ、今回の文字列では : の直後に空白文字が入るので、特殊文字 \s を使ってそれを表現し、 それを除いた部分をサブパターンで指定することにします。

そうして作った正規表現のパターンは "^name:\\s*(.*? $") です。ここで "\s" のところが "\\s" と表現されているのは、Processing では文字列内の "\" が特別な意味として解釈されてしまうので、"\\" として単なる文字の "\" として扱われるようにしています。

今回の正規表現で新しく出てきた特殊文字は次の通りです。

特殊文字 該当するもの
\s 空白文字(空白やタブ、改行)
* 直前で指定した文字が 0 個以上で、パターンを満たす中で最も長いもの
( サブパターンの開始位置
) サブパターンの終了位置

つまり、今回のパターンは「行の先頭から "name:" で始まり、空白文字が 0 個以上続いた後からサブパターンが始まって行末の直前で終わり、サブパターン内は、任意の文字が 0 個以上続く」という正規表現になります。 このパターンで match 関数を使うと、該当する行全体が matchedTexts[0] で得られると同時に、サブパターンに該当する部分が matchedTexts[1] で得られます。

String[] matchedTexts = match(text, "^name:\\s*(.*?)$");

if (matchedTexts != null) {

  println(matchedTexts[0]);
  println(matchedTexts[1]);
}

つまり、今回の例では次の結果が得られます。

name: star
star

複数箇所のマッチする部分を取得する

これまでの例では、最初にマッチした部分だけが取得できていました。ただ、今回の例ではもう1箇所、パターンにマッチする部分があります。 これらも含めて取得するには matchAll 関数を使います。 この関数では、戻り値が二次元配列の String[][] になっていて、 複数のマッチした部分の文字列を取得できるようになっています。

ひとつもマッチしなかった場合は null を返します。 マッチした場合は、マッチした部分の文字列が match 関数と同じ String[] 型で得られ、それがさらに見つかった順番で配列に格納された二次元配列 String[][] として得られます。

つまり、matchAll 関数で得られた結果を for 構文で繰り返し処理してあげることで、マッチした全ての部分を処理できます。

String[][] matches = matchAll(text, "^name:\\s*(.*?)$");

if (matches != null) {

  for (String[] matchedTexts : matches) {
    
    println(matchedTexts[1]);
  }
}

このようなコードを書くと、今回の例では次の結果が得られます。

star
moon

さまざまな正規表現が可能

今回の例でも特殊文字がいくつか登場しましたけれど、正規表現にはこれ以外にもさまざまな特殊文字が用意されています。 用意されている特殊文字はプログラミング言語によって僅かに違っていたりしますけれど、Processing で使われる Java 言語であれば標準的な特殊文字が使えると思うので「正規表現 特殊文字」などで検索すると見つかると思います。

たとえば、次のような特殊文字がよく使われて、これらをうまく組み合わせていくとけっこう複雑なパターンを表現できたりします。

特殊文字 該当するもの
^ 行の先頭
$ 行の末尾
. 任意の文字
\. ピリオドそのもの(特殊文字の手前に \ を付けるとその文字そのものを表現)
\s 空白文字(空白やタブ、改行)
\d 数字
\w 英数字
[abc] 文字 a, b, c のどれか
[a-z] 文字 a から z までのどれか
[a-zA-Z] 大文字と小文字を含むアルファベットのどれか
[^abc] 文字 a, b, c 以外のどれか
* 直前で指定した文字が 0 個以上で、パターンを満たす中で最も長いもの
*? 直前で指定した文字が 0 個以上で、パターンを満たす中で最も短いもの
+ 直前で指定した文字が 1 個以上で、パターンを満たす中で最も長いもの
+? 直前で指定した文字が 1 個以上で、パターンを満たす中で最も短いもの
? 直前で指定した文字が 0 個または 1 個
( サブパターンの開始位置
) サブパターンの終了位置
| この記号の左側で指定したのパターンまたは右側で指定したパターンのどちらか

他にも「指定した文字列を含まないもの」みたいなパターンを表現する「先読み」という正規表現を表現する特殊文字を使ったりすることもできます。 正規表現は高度な反面難しいところもあるので、特に慣れないうちは正規表現だけに頼ろうとしないで、正規表現で抽出しやすいようにプログラムで文字列を予め整形してから使ったり、正規表現でざっくり取得してから直接的な文字列処理で目的の文字列を抽出していくみたいにしても良いかもしれません。