Perl Hackers Hub

第58回正規表現の勘所―わかりづらい記法の覚え方、先読みや後読みの実践(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはPerl入学式のサポーターで知られる尾形鉄次さんで、テーマは「正規表現の勘所」です。

本稿のサンプルコードは、執筆時点(2019年9月)の最新であるPerl 5.30.0で動作確認を行っています。紙幅の都合で、誌面ではuse strict;use warnings;は省略しています。全文は、WEB+DB PRESS Vol.113のサポートサイトから入手できます。

正規表現は文字列処理の強い味方

Perlは文字列処理に強いと言われていますが、その強さの一因は正規表現でしょう。少しの記述で効果的な文字列処理が期待できるため、ぜひとも活用したい機能です。

一方で、正規表現は読みづらいなどの理由で敬遠されがちです。そこで本稿では、初学者向けの丁寧な解説が少ないと筆者が感じたPerlの正規表現について解説していきます。

Perl正規表現の基本

正規表現の話題は広範です。紙幅の都合もあり、すべてを紹介することはできません。そのため本稿では、表1にある基本的な正規表現のメタ文字については、簡単な使い方を知っていると仮定して進めます。

表1 基本的な正規表現のメタ文字
メタ文字説明
. 任意の1文字にマッチする
[■]文字クラス。リスト■の中にある任意の1文字にマッチする
[^■]否定の文字クラス。リスト■の中にない任意の1文字にマッチする
●?正規表現●が0個もしくは1個存在する
●*正規表現●が0個もしくは1個以上存在する
●+正規表現●が1個以上存在する
●*?●*と同じ意味だが、マッチパターンが複数ある場合、●*はなるべく長くマッチするのに対し、●*?はなるべく短くマッチする
(●)グループを作る。正規表現●にマッチした場合、キャプチャを行う
(?:●)グループを作るが、キャプチャは行わない
●|◎左右の正規表現●◎のどちらかにマッチする。上記のグループの中で使われることが多い
^●正規表現●の冒頭にある場合、文字列の冒頭でのみマッチする
●$正規表現●の末尾にある場合、文字列の末尾でのみマッチする
\s空白、改行、タブのうち1文字を表す
\d数値1文字を表す
\b単語境界を表す

表1に登場する用語を説明します。対象文字列が正規表現で表される場合、マッチすると言います。また、文字そのものを表さず特殊な意味を持つ正規表現内の文字のことを、メタ文字と言います。そして、特定のメタ文字を使って正規表現にマッチした一部分を抜き出すことを、キャプチャすると言います。キャプチャ結果は、キャプチャの順番に応じて番号付き変数$1、$2……に格納されます。

日本語文字列と正規表現

標準のPerlでは、正規表現のメタ文字.にマッチするのは半角の英数や記号など1バイト文字のみです。日本語などマルチバイトの1文字に正しくマッチさせるには、少し準備が必要です。

Perlで日本語の1文字を正しく認識する

日本語文字列を含むマルチバイト文字列をPerlで正しく扱うには、外部との入出力はバイト列で行い、ソースコード内ではバイト列からPerlの内部文字列に変換する必要があります。つまり、入力時にバイト列をPerlの内部文字列に変換し、出力時にPerlの内部文字列をバイト列に変換する必要があります。

ソースコード内に直接書かれた文字列リテラルを一括でPerlの内部文字列に変換するには、ソースコードを文字コードUTF-8で書いたうえで、冒頭でutf8プラグマを宣言します。

外部との入出力においてバイト列とPerlの内部文字列を相互変換するには、EncodeモジュールやPerlIOを使います[1]⁠。print関数などでの画面出力時にPerlIOを使用して自動的にバイト列への変換を行うbinmode STDOUT, 'utf8' 命令は、utf8プラグマ宣言時に便利です。

上記を怠った場合も、たとえば入力したバイト列をそのまま出力する場合は表示上の問題はありません。しかし、文字を処理する際に日本語の1文字を正しく認識できないという問題が発生します。

# このスクリプトはUTF-8で書かれている
use utf8;  ――(1)
binmode STDOUT, ':utf8';  ――(2)

my $greeting = "こんにちは";  ――(3)
my $len = length $greeting;
print "あいさつは「${greeting}」、文字数は${len}文字\n";

(1)utf8プラグマ宣言によって、(3)の文字列リテラル"こんにちは"が5文字のPerlの内部文字列となり、$len5になります。(2)binmode STDOUT, 'utf8'命令によって、Perlの内部文字列が外部出力の際にUTF-8のバイト列に変換されます。

(1)(2)をコメントアウトすると文字列リテラルはバイト列となり、バイト列はそのまま正常に外部へ送出されます。ただし、length関数は1バイトを1文字と解釈して、$len15となります[2]⁠。

まとめると、文字列における1文字は、Perlの内部文字列の場合は本来の意味での1文字として解釈されますが、バイト列の場合は1バイトが1文字として解釈されます。

Perlの正規表現で日本語の1文字に正しくマッチする

バイト列とPerlの内部文字列の違いは、lengthだけでなく1文字を認識しようとするPerlでの指示全般に言えます。正規表現のメタ文字も例外ではなく、任意の1文字にマッチする.や、リストの中にある任意の1文字にマッチする[■]が影響を受けます。

# このスクリプトはUTF-8で書かれている
# utf8プラグマ宣言なし。つまり文字列リテラルはバイト列

my $str = "ミーティングの日は木曜日です";
my ($week1) = $str =~ /(.)曜日/;  ――(1)
my ($week2) = $str =~ /([月火水木金土日])曜日/;  ――(2)
my ($week3) = $str =~ /(月|火|水|木|金|土|日)曜日/;  ――(3)

(1)(2)$week1$week2は文字化けするのに対し、(3)$week3は意図どおり"木"となります。

(1)は、"木"が文字どおりの1文字ではなく3バイト分のUTF-8のバイト列"\xE6\x9C\xA8"として評価され、正規表現の/(.)曜日/でキャプチャされるものは印字不可能な1バイト文字"\xA8"です。(2)は、正規表現[月火水木金土日]の中には21バイトの文字があると評価されますが、結果として(1)と同様に"\xA8"がキャプチャされます。

(3)が意図した結果になるのは、キャプチャをするグループ化の選択(●|◎)には1文字を評価する意図がないためです。

上記の例においてutf8プラグマ宣言がある場合、(1)(2)で意図どおり"木"がキャプチャされます。

新規で書くPerlプログラムではutf8プラグマを宣言して、日本語文字列はPerl内部文字列として正しく扱うことをお勧めします。事情によりそれを行うことができないプログラムで日本語文字列を正規表現処理する場合は、上記を理解しておくとよいでしょう。

正規表現にまつわる演算子

Perlには正規表現にまつわるさまざまな演算子があります。お馴染みかもしれませんが、軽く見ていきましょう。

m//演算子─⁠─検索を行う

まずは、検索を行うm//です。m//mはマッチmatchの頭文字で、m//はマッチ演算子とも呼ばれます。

文字列が代入された変数$strに対し、与えられた正規表現にマッチするパターンがあるかを検索したい場合、マッチ演算子m//と二項演算子=~を使って$str=~ m/●/と書きます。マッチするパターンがある場合に$str =~ m/●/は真、ない場合に偽となるので、if文の条件部分で使うことが多いです。

my $str = "ミーティングの日は木曜日です";
if ( $str =~ m/木曜日/ ) { # マッチする
    print "Thursday\n"; # 表示される
}

m//ではなくm||などのスラッシュ以外の区切り記号の選択については後述しますが、区切り記号がスラッシュの場合に限ってm記号を省略できるというルールがあります。上記の$str =~ m/木曜日/の場合、$str=~ /木曜日/mを省略できます。

本稿でも、省略できる場合はmを省略します(前項のコードでも省略していました⁠⁠。区切り記号が変更されたり、mが省略されている場合でも、本稿の解説ではマッチ演算子自体を指してm//と書いています。

s///演算子─⁠─置換を行う

マッチした箇所を別の文字列に置換する場合、s///を使います。s///sは代用を意味するsubstitutionの頭文字です。

文字列変数$strと正規表現があり、マッチ箇所を文字列で置換する場合、次の(1)のようにs///=~を使って$str =~ s/●/■/と書きます。

my $str = "ミーティングの日は木曜日です";
$str =~ s/木曜日/火曜日/;  ――(1)
print "$str\n"; # => "ミーティングの日は火曜日です"

通常、置換は最初にマッチした箇所のみ行われますが、マッチするすべての箇所を置換する場合、次の(1)のようにs///gと右側にgを付けます。gは大域を意味するglobalの頭文字です。

my $str1 = "アリスさんとボブくん";
$str1 =~ s/君|くん|さん/さま/;
print "$str1\n"; # => アリスさまとボブくん
my $str2 = "太郎君と花子さん";
$str2 =~ s/君|くん|さん/さま/g;  ――(1)
print "$str2\n"; # => "太郎さまと花子さま";

このgに代表される、正規表現のマッチや置換の方法を変える指示のことを修飾子modifierと呼びます。正規表現の解説では修飾子であることを明示するため、スラッシュ記号を伴って/gのように書きます。

よく使われる修飾子については後述します。

特殊変数$_─⁠─正規表現演算子のデフォルトの処理対象

m//s///が二項演算子=~を伴わない場合、特殊変数$_とのマッチを試みます。つまり、m//s///の左側に$_ =~が省略されたとみなされます。

特殊変数$_は、標準入力から1行ずつ受け取るwhile(<>)構文で1行の文字列が格納される変数など、多くの組込み機能がデフォルトの処理対象とします[3]⁠。

この直接書かれない$_を活用して簡潔なプログラムを書く方針には賛否両論ありますが、書き捨てプログラムなどをすばやく書きたい場面で効果的です。

while(<>) {  ――(1)
    next if /^\s*#/ || /^\s*$/;  ――(2)
    print;  ――(3)
}

上記の例は、標準入力で与えられたソースコードから、空行や#開始のコメント行を除いて表示します。

(1)while(<>)によって、$_に標準入力の1行が順番に代入されます。標準入力の末尾に到達すると、<>は偽と評価されてwhile文から抜けます。

(2)/\s*#/はコメント行、/^\s*$/は空行を表していて、これらのm//=~を伴っていないので、(1)で用意された標準入力からの1行である$_とマッチを試みます。

また、(3)は、出力文字列を引数で与えられていないprint$_を出力文字列とする性質を利用しています。

区切り記号の変更

m//の解説時に少し触れましたが、m//s///は区切り記号をスラッシュ/以外から選ぶことができます。

while(<>) {
    if ( m|(https?://\S+)| ) {  ――(1)
        print "$1\n";
    }
}

(1)では、m//の区切り記号として縦線|を選んでいます。

(1)でスラッシュが区切り記号の場合、正規表現を構成する文字にスラッシュを使うには、それが正規表現の終端文字ではないことを示すため、m/(https?:\/\/\S+)/のようにバックスラッシュを使ってエスケープする必要があります。正規表現で特定の記号群を多用する場合、それら以外を区切り記号に選ぶことで可読性が上がります。

s///の場合も、s|||のように変更できます。

対となる記号がある括弧類を区切り記号にすることも可能で、その場合はm{}m[]s{}{}s[][]と直感的に書くことができます。波括弧{}は正規表現のメタ文字[4]としての使用頻度がそれほど高くないため、括弧類の区切り記号として選ばれやすいです。

<続きの(2)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧