正規表現ドリル 第1回 特殊文字や文字クラスの利用

問題と解答というドリル形式で、正規表現を学びます。今回は、特定の文字列のみへのマッチさせる正規表現と、見出し開始タグへマッチさせる正規表現です。

発行

著者 山田 順久 フロントエンド・エンジニア
正規表現ドリル シリーズの記事一覧

はじめに

正規表現は文字列のパターンを表現したもので、ユーザーの入力がルールに一致しているかどうかをチェックしたり、テキストの中から特定のパターンの文字列を検索したりといった用途に使える強力なツールです。

テキストエディタで開いた文書の中からcatという文字列を検索するには単にcatと検索すればよいでしょう。しかしcで始まる3文字を検索したいケースになると正規表現の出番となります。たとえば正規表現を使ってc..というパターンを検索すれば、catでもcarでも、「cから始まる3文字すべて」にマッチさせられます。

プログラマやエンジニアといった職種でなくても、正規表現が役立つ人もいるのではないでしょうか。実際、正規表現はコンピューターでテキストを少しでも扱う人すべてに便利なものです。正規表現はプログラム言語のソースコードの中で使うことができますが、世の中の大抵のテキストエディタの検索機能も正規表現をサポートしており、それを利用した文字列の検索や置換が可能です。

今回はこのシリーズの記事を書くにあたり、正規表現を使って解ける問題をいくつか作成して、社内のスタッフに解いてもらいました。そうして集まった解答とそれらへの解説を行いながら、正規表現の機能を解説していきたいと思います。

正規表現とは

改めて正規表現とは何かについて簡単に触れておきます。正規表現とはテキストの中からルールにマッチする箇所を検索できるパターンのことです。単体のプログラミング言語というよりは、プログラミング言語やアプリケーションが持つ機能のひとつとして取り入れられています。

どこかで標準規格が決められているわけでもなく、正規表現をサポートする言語によって方言が存在しています。といっても新たな言語を学習するたびに正規表現を一から学習する必要はなく、大体の言語はPerl言語における正規表現の方言をベースにしています。細かな違いはあれども一度覚えれば他の言語やツールでも同じ知識を活用できます。

テキストと特殊文字

正規表現の構文には、大きく分けて2種類の文字があります。まずひとつはただのテキストです。テキストの中からcatの文字列を検索したい場合、catというただのテキストだけで書かれたパターンも正規表現です。ただ、これなら普通のテキスト検索で十分です。

もうひとつの種類が特殊文字です。これは単純なテキストよりも強力な表現力を持つ文字で、たとえば冒頭の例で利用した.は、任意のすべての1文字にマッチする特殊文字です。これを使ったc..という正規表現はcで始まる3文字にマッチします。さらに繰り返しを意味する{n}の特殊文字を使えばi.{18}nのように、inの間に任意の18文字がある文字列というパターンも作れます。

これらテキストと特殊文字を組み合わせることで、目的の文字列にマッチさせるパターンを作ることが正規表現を使いこなすひとつのポイントです。

今回の問題

今回解説する問題は、次の2つになります。社内の開発職ではないスタッフにも解答に参加してもらったため、簡単な問題からスタートしました。解答例を見る前に、ぜひご自身でも考えてみてください。

第1問

「cat」「category」「muscat」の3つのテキストのうち、1つ目の「cat」にだけマッチするJavaScriptの正規表現を書いてください。

第2問

次のHTMLに存在する<h1>から<h6>までの見出し開始タグにマッチする正規表現を書いてください。たとえば<h1>h1見出し</h1>に対して<h1>の部分がマッチすることを期待します。

<h1>h1見出し</h1>
<h2>h2見出し</h2>
<h3>h3見出し</h3>
<hr>
<h4>h4見出し</h4>
<h5>h5見出し</h5>
<h6>h6見出し</h6>

第1問:特定の文字列のみへのマッチ

それでは最初の問題です。正規表現の方言はJavaScriptにおける実装としています。

問題

「cat」「category」「muscat」の3つのテキストのうち、1つ目の「cat」にだけマッチするJavaScriptの正規表現を書いてください。

解答例

^cat$

解説

社内の多くの解答が上記の正規表現で、筆者が想定していたのもこれでした。単純にcatのパターンを検索してしまうと、その3文字を含む「category」と「muscat」にもマッチしてしまいます。そこで、先頭のcと末尾のtを区別するために^$の特殊文字を使います。

^ は行の先頭、あるいは改行がない場合にテキスト全体の先頭という位置にマッチする特殊文字です。正規表現を^caから始めることで、問題の3つのテキストのうち、先頭がc、そしてaが続く「cat」と「category」に絞りこむことができます。

$は行の末尾、あるいは改行がない場合にテキスト全体の末尾にマッチします。t$で正規表現を終えることで、末尾がtである「cat」のみに絞り込めます。

その他の解答と解説

\bcat\b

こちらの解答もありました。これは行頭と行末の特殊文字を使う代わりに、単語境界の特殊文字(\b)を利用しています。これは単語を構成する文字と、そうではない文字の間にマッチします。これも^$と同じく位置にマッチする特殊文字です。

単語を構成する文字がなにかというと、JavaScriptにおいては英数大小文字と_です。\bはそれらの文字と、そうではない文字の間にマッチするということなので、「category」や「muscat」のように「cat」の前後に普通のアルファベットが存在するテキストにはマッチせず、ただ「cat」という単語からなるテキストのみにマッチします。

^cat$にマッチせず\bcat\bにマッチするテキストを考えると、「My name is cat.」や「(cat)」が挙げられます。これらは行頭から行末まで「cat」のみで構成されるテキストではありませんが、「cat」という単語の前後は、半角スペースやピリオド、丸括弧のような非単語構成文字です。\bはこうした、単語を成り立たせる文字とそうでない文字の間にマッチします。

第2問:見出し開始タグへのマッチ

問題

次のHTMLに存在する<h1>から<h6>までの見出し開始タグにマッチする正規表現を書いてください。たとえば<h1>h1見出し</h1>に対して<h1>の部分がマッチすることを期待します。

<h1>h1見出し</h1>
<h2>h2見出し</h2>
<h3>h3見出し</h3>
<hr>
<h4>h4見出し</h4>
<h5>h5見出し</h5>
<h6>h6見出し</h6>

解答例

<h[1-6]>

純粋な正規表現パターンとしては上記のとおりですが、すべての見出し開始タグへ一度にマッチさせるには、グローバルサーチオプションを有効にします。JavaScriptの場合には次のようにgというフラグを指定します。これを指定しないと、正規表現のマッチングは最初の<h1>を見つけたところで終了します。

JavaScriptでグローバルサーチを有効にした正規表現

/<h[1-6]>/g

JavaScriptのコード中における正規表現の構文については後ほど詳しく説明します。

解説

ここで利用している角括弧を使った[1-6]のような記法は文字クラスと呼ばれます。これは角括弧の中に列挙された文字のどれか1つとマッチします。たとえば次の正規表現はabcdefのどれか1文字にマッチします。

[abcdef]

こうしたアルファベット順に連続した文字の範囲を扱う場合には、文字クラスの中でハイフンを利用できます。次の正規表現でも、aからfまでのどれか1文字にマッチします。

[a-f]

文字の範囲は英数字に限りません。たとえばひらがなすべてにマッチする文字クラスは次のように表現します。

[ぁ-ゟ]

上記はUnicode*がひらがなと定義している文字の範囲です。

* 注:Unicode(ユニコード)

世界中の文字に対して16進数の番号を割り当てて管理している規格です。その中で3041番目の「ぁ」から309F番目の「ゟ」がひらがなとして定義されています。

わかりやすさのために、解答例の「<h[1-6]>」がどのように文字を探索するかを分解して説明すると、次のような順番になります。

// 「<」で始まる
<

// 「<」の次に「h」がある
<h

// その次は1から6のどれかの数字がある
<h[1-6]

// その次に「>」がある
<h[1-6]>

また、文字クラスには否定形もあります。[の直後にキャレット^を置くことで作れます。次のように記述することで、aからf以外の1文字という意味になります。

[^a-f]

その他の解答と解説

<h\d>

こうした解答もありました。\dは数字にマッチする特殊文字であり、文字クラスで表すところの[0-9]です。これでも1から6までの見出しタグにマッチさせられます。しかし<h0><h7>のような文字列にもマッチしてしまいます。そもそもとして、HTMLとして間違った見出しタグは別途、事前に正されているべきであるので、実際の仕事ではこれも正しく機能するでしょう。

しかし、よい正規表現とはなにかを考えたときのひとつの指針として、「必要なものにマッチして必要でないものにはマッチしない」というものがあります。その考えに基づくと、正しい見出しタグにマッチさせる意図の明確な<h[1-6]>がよりよいだろうと考えました。

JavaScriptと正規表現

この記事では純粋な正規表現のパターンのみを記載していますが、JavaScriptのコード中で正規表現を生成するには、組み込みオブジェクトのRegExpを用いるか、次のように/で正規表現パターンを囲みます。

「cat」の3文字のみで構成されたテキストにマッチする正規表現

const catRegExp = /^cat$/;

RegExpクラスを初期化して生成することも可能ですが、numberRegExpの例のように、特殊文字に使う\のエスケープのために、さらに\を重ねなくてはならないため、少し表記がややこしくなります。基本的には先に説明した/で囲む方法でよいと思います。/で囲んで正規表現を生成する書き方を「正規表現リテラル」と呼びます。

RegExpクラスの初期化の場合はバックスラッシュのエスケープがややこしくなる

const numberRegExp = new RegExp("\\d");

こちらの方法には文字列の変数から正規表現を作れる特徴があります。別途コード中で宣言している変数を利用したい場合や、ユーザーの入力した文字列を正規表現パターンとして利用する動的な状況などではRegExpを選択できます。

RegExpクラスの初期化の場合は変数をパターンとして渡せる

const userDefinedRegExp = new RegExp("<ユーザーの入力した文字列>");

正規表現オプション

正規表現はマッチング時の動作モードを指定することができます。たとえば英字の大文字小文字を区別しないことを指定するオプションや、先ほど少し登場した、テキスト全体からすべてのマッチを返すグローバルサーチオプションなどがあります。

こうしたオプションはそれぞれ一文字の英字で表されたフラグを指定することで有効になります。フラグは、正規表現リテラルの場合は/で囲んだ直後に指定するか、 RegExpクラスの初期化をする場合であれば第二引数に指定します。

パターンとフラグを持つ正規表現の構文

// フラグを持つ正規表現リテラルの構文
/パターン/フラグ;

// フラグを持つRegExpクラスの初期化構文
new RegExp("パターン", "フラグ");

大文字小文字を区別しないフラグを用いて、aAの両方にマッチする正規表現は次のようになります。

大文字小文字を区別しないフラグを含めた正規表現の実例

// aとAにマッチする正規表現をリテラルで生成
/a/i;

// aとAにマッチする正規表現をRegExpクラスの初期化で生成
new RegExp("a", "i");

リテラルの場合と、RegExpクラスの初期化の場合を併記していますが、正規表現オブジェクトとしての働きはどちらも同じです。

おわりに

今回は行頭と行末を意味する^$の特殊文字、文字の範囲を意味する文字クラスを使って解ける正規表現の問題を紹介しました。また、単語境界を表す\bや、数字を表す\dを使った解答もありました。

次回もまた別の正規表現の知識を使って解く問題とその解答を紹介していきます。