Perl Hackers Hub

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

この記事を読むのに必要な時間:およそ 3.5 分

本連載では第一線の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文字として解釈されます。

注1)
詳しくは,perldoc perlunicodeを参照してください。
注2)
UTF-8での日本語の1文字は,通常は3バイトとなります。

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内部文字列として正しく扱うことをお勧めします。事情によりそれを行うことができないプログラムで日本語文字列を正規表現処理する場合は,上記を理解しておくとよいでしょう。

著者プロフィール

尾形鉄次(おがたてつじ)

大学院卒業後の2003年,Webメール開発会社に入社。以後10年以上,Perlを使ったWeb開発に携わる。

2015年,株式会社ガイアックスに入社。オンプレやクラウドの構築や保守運用を担当するインフラチームに所属しつつ,2018年頃から社外イベント運営やプログラミング教育などにも携わっている。

教育に関しては大学でのTAの経験などから課題感を持っていたが,2013年以降にPerl入学式などのコミュニティで教える側に立つことにつながり,そこで得られた知見を社内外で活かしている。

2019年よりJapan Perl Association理事,およびPerl入学式2代目代表を務める。