正規表現を学んでみませんか

はじめに

正規表現とは何か?学ぶことで何ができるようになるのか?

例えばサーバ内のログを調べる際、「○月○日から○月○日までのデータを抽出したい」とか 「○○という拡張子の付くファイルへのアクセスだけを抽出したい」といったことができれば良いなと思ったことはありませんか?

○に該当する文字は一定の条件に合致していれば何でも良くて、指定する形式にだけ一致するよう指定したものを「正規表現」と呼びます。

例えば、特定の条件に合致する文字列をテキストファイルから抽出したい時や、自分で書いたプログラムにおいて、入力された形式が正しいかどうかを確認する際、正規表現を使う価値があります。

では、どういうパターンであれば正規表現として表せそうか考えてみましょう。

項目 条件 補足
会員ID 3桁の英文字と5桁の数字 xyz00000 さくらインターネットのユーザならおなじみ
拡張子 ファイル名の末尾にドット記号 . があり、それ以降を識別子として用いた文字列 秘密の写真.jpg
IPv4アドレス 1桁から3桁の数値を、ドット記号 . で4つのブロック(オクテット)として構成したもの 192.168.0.1 厳密に言えば違う
郵便番号 3桁の数字と4桁の数字をハイフン記号 - で連結したもの 541-0054 参考

正規表現をサポートしていないツール(テキストエディタやビューア)だと、検索機能だけで文字を探さなければなりませんが、 正規表現を理解し、また、正規表現をサポートするツールを用いれば比較的簡単に抽出することが可能です。

この記事は、POSIXに基づいた正規表現(伝統的なUNIXの正規表現)の理解を深めることを目的に作成した社内向けの資料を手直しして作成しています。予備知識無しにこの記事を見られた方は「POSIXて何?」、「伝統的なUNIX?Linuxとは違うの?」とか「正規表現?なんですかそれ」といった疑問があるかと思いますが、細かいことは気にせず気軽に読み進めてみてください。わからなくても大丈夫です。書いた本人も最初はわかっていませんでしたし、なんなら今もわから 知らない世界を知ることは楽しいと思います。

対象読者

  • サーバ内の調査等でgrep、awk、sed、vi等のコマンドを使う必要のある方
  • 正規表現という言葉は知っているものの、使い所がいまいち思い浮かばない方
  • 「さくらのレンタルサーバ」や「さくらのVPS」、「さくらのクラウド」等、サーバへシェルログインできる環境があり、スキルアップするためのきっかけが欲しい方←露骨

ある程度UNIX系OSを学習された経験がある方であればスラスラ読めるよう意識して書いてはいますが、シェル上でよく使うコマンド(ls, cat, less等)が使えないと読むのはつらいかもしれません。わからない単語が出てきたら適宜インターネット上の記事や書籍等で補ってください。

とはいえ、サーバ管理者への道を歩みはじめた方に向けた記事もさくらのナレッジ内にはいっぱいあるので安心ですね。

ご注文はログ調査ですか?~さくらの社内勉強会より~

使用するツールについて

grepの正規表現オプション(つまり grep -E、egrep)を使って説明しています。Z圧縮されたファイル(拡張子が.gz)であればzgrepコマンドを使ってみてください。

grepの使い方についてはこちらをご覧ください。 → Linux版FreeBSD版

接続環境について

SSHを用いたtty接続でシェルログインができればOSやソフトウェアは問いません。

(参考)SSH接続できるソフトウェア

ログファイルのパスについて

途中、HTTPアクセスログを活用したサンプルが出てきます。 /PATH/TO/access.log と書いてある箇所は皆さんがご利用の環境へ適宜読み替えてください。また、/PATH/TO/maillog はSendmailのログとお考えください。

試すサービス 説明
さくらのレンタルサーバ /home/アカウント名/log/ にHTTPアクセスログがあります。存在していなければ、レンタルサーバ コントロールパネルからログの出力を有効化してください。Sendmailのログは確認できないので、興味が湧いたら「さくらのVPS」や「さくらのクラウド」でお試しください。
さくらのVPS、さくらのクラウド インストールされるサーバソフトウェアによって異なるのですが、LAMP環境(Linux, Apache, MySQL, PHP)であればApacheの設定ファイル(httpd.conf)に出力先が書かれています。また、Sendmailについては適宜セットアップを行ってください。

何はともあれ使ってみる

ということでお待たせしました本題に移りましょう。

と言っても、正規表現として使える文字をいきなり列挙するとドン引きするかもしれないので、まずは一番簡単な正規表現を使ってみます。サーバへシェルログインし、以下の文字列を打ち込んでください。

% grep -E '*' /etc/motd

実行結果(WordPressのDockerコンテナにて試しています。実行環境によって結果は異なります)

% grep -E '*' /etc/motd
 The programs included with the Debian GNU/Linux system are free software;
 the exact distribution terms for each program are described in the
 individual files in /usr/share/doc/*/copyright.

 Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
 permitted by applicable law.

「*」は「アスタリスク」と読み、0回以上の連続する繰り返しを意味します。この場合、/etc/motdというファイルの中から、0回以上の連続する繰り返しを探し出して表示しています。つまり、全部表示します。

では次に、アスタリスクをドット . に変えてみましょう。

% grep -E '.' /etc/motd

実行結果(一例です。実行環境によって結果は異なります)

% grep -E '.' /etc/motd
 The programs included with the Debian GNU/Linux system are free software;
 the exact distribution terms for each program are described in the
 individual files in /usr/share/doc/*/copyright.
 Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
 permitted by applicable law.

ドット . は「任意の1文字」を意味し、文字だけが当てはまります。このため空行が無くなってしまいました。

こんどは少しだけ難しくなります。

% grep -E '[0-9]' /etc/motd

実行結果(一例です。実行環境によって結果は異なります)

% grep -E '[0-9]' /etc/motd
 ... なにも表示されない(さくらのレンタルサーバならOSの情報が表示されます) ...

角カッコ[]の中に文字を入れると、その文字に該当するものだけが抽出されます。「0-9」とすれば、「0」、「1」、「2」、「3」、「4」、「5」、「6」、「7」 、「8」、「9」が含まれる行だけを抽出します。/etc/motdの中身を見ると、数字が1つも含まれていません。よって、何も抽出されないという結果になります。

motdは「Message Of The Day」の略で、サーバへログインした際に表示させたい内容を書いておくためのファイルです。OSやサーバによっては「便利な使い方」や「お知らせ」などが書いてあります。

正規表現として使える代表的な文字

では次に、一覧を確認してみましょう。他にもありますが、ここではよく使うものだけを表示します。ちなみに、これらの文字は「メタキャラクタ」と呼ばれます。

文字 読み方 意味 補足
* アスタリスク、スター 0回以上の繰り返し 「ファイルグロブ」や「ワイルドカード」とも呼ばれています
. ドット 任意の1文字
[] 角カッコ 文字の指定 大カッコ
? クエスチョン(クエスチョンマーク)、はてな 直前の文字が0個か1個である場合に一致
+ プラス 直前の文字が1個以上連続する場合に一致
- ハイフン、マイナス 範囲指定 a-dはabcd、1-4なら1234という意味
\ バックスラッシュ 直後のメタキャラクタ1文字を打ち消す UNIX系OSやmacOSだとこっち
¥ 円マーク 直後のメタキャラクタ1文字を打ち消す Windowsだとこっち
^ ハット、キャレット 行頭
$ ドル、ダラー 行末
[^] 角カッコの中にハット記号 文字の否定。つまり含まれないものすべてが一致
{x,y} 波カッコ 直前の文字(文字列)のうち、x回以上y回以下の繰り返しに一致 「中カッコ」や「カーリーブレイス」、「マスタッシュ(ひげ)」と呼ばれることも
x|y パイプ(パイプライン)、バーティカル、縦棒、OR記号 xもしくはy
() 丸カッコ グルーピングさせたい時に使用 小カッコ

それぞれの意味については使っているうちに自然と身につくかと思います。無理に急いで丸暗記しなくても良いです。
ほかにもたくさんあります。また、使用するツールやプログラム言語によって異なる場合があります。

実践

ディレクトリだけを一覧表示

例えば /etc 以下のファイルを一覧で表示させ、その中からディレクトリ(フォルダ)であるものを表示させたい場合、以下のようにします。

% ls -l /etc | grep '^d'

実行結果(例)

% ls -l /etc | grep '^d'
 drwxr-xr-x 2 root root 4096 Apr 11 06:01 ImageMagick-6
 drwxr-xr-x 1 root root 4096 Apr 11 06:02 alternatives
 drwxr-xr-x 1 root root 4096 Apr 11 06:02 apache2
 drwxr-xr-x 1 root root 4096 Apr 8 00:00 apt
 ... 略 ...

lsの結果にて、行頭の文字が「d」であれば表示します。

あいまいな文字列を抽出

jpgという拡張子のファイルはjpegと書いてある場合もあります。ログファイルの中から探してみましょう。

% grep -E '\.jpe?g ' /PATH/TO/access.log | less

\.」は「.」という文字を表しています。「.jpe?g」と書いてしまうと、「aaa.jpg」だけではなく、「aaa1jpg」や「aaazjpg」という文字列にもマッチしてしまうので、「\」記号を直前に付けることで、その意味を打ち消しています。

「?」は、「直前の一文字があってもなくても良い」ということを表します。このように書くことで「jpg」も「jpeg」も拾うことができます。

クイズ

htmという拡張子のファイルはhtmlと書いてある場合もあります。どうすれば漏れなく見つけることができるでしょうか。

・・・・・ 考えてみよう! ・・・・・

答えはこちらです。

'\.html?'

先のjpeg文字列を抽出した条件と入れ替え、動作を確認してみましょう。

もう一つの書き方

「jpgもしくはjpeg」という文字を抽出したい場合、以下のように書くこともできます。

% grep -E '\.(jpg|jpeg) ' /PATH/TO/access.log | less

これを応用すれば、例えば「sendmailのログからfromもしくはtoを含む文字を抽出したい」という時にも同じような書き方ができます。

% grep -E '(from|to)=' /PATH/TO/maillog | less

ちょっと応用

「RBLってブラックリスト?ブロックリスト?どっちだったっけ?」というときは、以下のようにすればいいかもしれません。

% grep -iE 'bl[ao]ck ?list'

grepの-iオプションを使うと、大文字小文字問わず抽出できます。
この書き方でマッチするパターンはいっぱいあります。

  • blacklist
  • block list
  • BlackList
  • ...他いっぱい

以下のようにすれば確かめることができます。

% echo 'blacklist' | grep -iE 'bl[ao]ck ?list'
 % echo 'block list' | grep -iE 'bl[ao]ck ?list'
 % echo 'BlackList' | grep -iE 'bl[ao]ck ?list'

echoの引数を色々と変えて確かめてみてください。

ちなみに、正規表現だけで書くと以下のようになります。少し読みにくいですね。

% echo 'blacklist' | grep -E '[Bb][Ll][AaOo][Cc][Kk] ?[Ll][Ii][Ss][Tt]'

正規表現だけを使うと読みにくくなるので、大文字小文字問わずに検索できるオプションは重宝します。sedやawk、perl、php等もこのオプションはあります。

オプション文字の i は「ignore(無視)」と覚えちゃいましょう。

IPv4アドレス形式を抽出

sendmailのログから、IPアドレス(IPv4)形式で書かれていない行だけを出してみましょう。

% grep -vE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' /PATH/TO/maillog | less

すべてを正規表現で表すと複雑になるので、ここではgrepの-vオプションを用いています。-vオプションは「パターンに一致したもの以外」を出力します。

抽出条件に指定している「[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}」という表現は、パッと見は文字数が多いので戸惑うかもしれません。ですが、小分けにして考えればさほど難しくありません。

IPアドレス(IPv4)はどういう形式で表示されるか思いだしてみましょう。

・・・・・ 考えてみよう! ・・・・・

0から255までの数値が4つあり、それぞれの間にドット . が入っています。例えば、「192.168.0.1」や「255.255.255.255」等はIPアドレス(IPv4)形式です。

考え方を少し変えると、「数字は1桁から3桁で、それらをドット . を挟んで4つあればIPアドレスとして判断しても良いんじゃないか」と考えることができます。

256から999までの数値を省くことも考慮しなければならないのですが、それを正規表現だけで表現するのはとても複雑になる、というか無理です。正確に判定したい場合、一般的にはPerlやPHP、Pythonといったプログラム言語(他にも多数あります)のほうで行います。

「数字」は[0-9]、「1桁から3桁」は{1,3}で表すことができます。それにドット . そのものを \. として4つ組み合わせれば、「[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}」となります。

※ただしawkだと波カッコ{}は使えないので、「match($0, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]/)」みたいな書き方になります。

もっと簡単に書けないの?

実は「[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}」という表現は、もう少し短くすることができます。 もう少し突き詰めてみると、「1~3桁の数字とドット . が3つ」と、「1~3桁の数字」と考えても良いと思いませんか?

こういった場合、丸カッコで「1~3桁の数字とドット . 」を括り、それを「3回繰り返している」を示す{3}を足すことで、以下のようにも書けます。

([0-9]{1,3}\.){3}[0-9]{1,3}

正しく抽出できているか確かめる

「書いた条件は正しくマッチしているのか?」と思うこともあるかと思います。このような場合、grepの-oオプションを使ってみましょう。

% grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' /PATH/TO/maillog | head

-oオプションを付けると、条件にマッチした行全体ではなく、マッチした文字列だけを出力します。

特定の時間や期間を抽出したい

ログ上で表示される時刻は、どのような形式で表現されているでしょうか?「0時12分3秒」を例にして考えてみましょう。

ログだけに限った話ではありませんが、時刻を表す場合、空いている桁は0で埋められるので、「00:12:03」となります。きちんとした呼び名はないのですが、「HH:MM:SS 形式」等と呼ばれています。

では本題です。2時から8時台のログだけを抽出したい場合、どのように書けば良いか考えてみましょう。

・・・・・ 考えてみよう! ・・・・・

この表現にマッチする正規表現は以下のようになります。今までの説明を踏まえていれば理解できると思います。

% grep -E ' 0[2-8]:[0-9]{2}:[0-9]{2} ' /PATH/TO/maillog | less

「何時でも良いから、HH:MM:SS 形式のものだけ取りたい!」という場合は以下のようにも書けます。

% grep -E ' ([0-9]{2}:?){3} ' /PATH/TO/maillog | less

どのようにマッチしたかは、-oオプションを付けて確認してみてください。

・・・と、ここまで書いてはみたものの、この表現はコロン : のない数字の連続、例えば000000等もヒットする可能性があるため、あまりおすすめしません。

正規表現は簡潔でも冗舌でも読みにくくなります。一読して理解しづらい文字の羅列は、他の誰か(数年後の自分)が見たときに「?」が頭の上に出ちゃうと思われるため、理解の助けになりそうなコメントをソースコードや手順書のコメントに書いておくと良いかもしれません。

正確にマッチさせるコツ

時刻を正規表現で抽出する場合、前後にスペースを入れておくようにしてください。
確率は限りなく低いですが、MACアドレスやIPv6アドレスもコロン : で区切った形式であるため、マッチする可能性があります。

% ifconfig | grep -E '[0-9]{2}:[0-9]{2}:[0-9]{2}'

実行するサーバによっては以下のようにマッチするかもしれません。

% ifconfig | grep -E '[0-9]{2}:[0-9]{2}:[0-9]{2}'
 inet6 2403:3a00:201:17:112:78:125:142 prefixlen 64
 ether 00:1d:92:04:68:72

なぜマッチしたのか考えてみましょう。

クイズ2

さくらのレンタルサーバやマネージドサーバのホスト名をマッチさせるにはどうすれば良いでしょうか?
☆☆☆に正規表現を書いてみてください。

% echo 'www202.sakura.ne.jp' | grep -E '☆☆☆'
 % echo 'www1202.sakura.ne.jp' | grep -E '☆☆☆'
 % echo 'www202b.sakura.ne.jp' | grep -E '☆☆☆'
 % echo 'www1202m.sakura.ne.jp' | grep -E '☆☆☆'

・・・・・ 考えてみよう! ・・・・・

答えはこちらです。

grep -E 'www[0-9]{3,4}[bm]?\.sakura\.ne\.jp'

字を変えたり桁を増やしたりして試してみよう!

クイズ3

以下のサーバリストから、空行と、#から始まる行を取り除くにはどのように正規表現を書けば良いでしょうか。

#ServerList(std)
 www222.sakura.ne.jp
 www223.sakura.ne.jp
 www224.sakura.ne.jp

 #ServerList(business)
 www322b.sakura.ne.jp
 www323b.sakura.ne.jp

・・・・・ 考えてみよう! ・・・・・

答えはこちらです。

grep -Ev '^(#|$)'

実際に試してみよう!/etc/motdや/etc/servicesでも確認できるよ!

※ Windowsから転送したファイルで同様のことを行う場合、「tr -d '\r' | grep -Ev '^(#|$)'」として、改行コードを先に取り除かないといけないかもしれません。

心得的なところ

正規表現ではできないこと、不向きなこと

正規表現は強力な機能ですが、弱点もあります。

数値の範囲指定

IPアドレスのマッチングでも触れましたが、「0から100のうち、30から40までの間」といった指定はややこしいのでおすすめしません。そう言ったお仕事はプログラム側で判断させましょう。

メールアドレスの抽出

メールアドレスとして使用できる文字やルールはとても緩いので、正確にマッチさせることはできません。ログから抽出する場合は、fromやto、ホワイトスペース等、メールアドレスの前後にある文字列を利用します。

不等号 <> をmaillogの中から抽出するためのマッチング文字に使用しないでください。狙った通りに拾えません。

覚えておくと良いかもしれないこと

  • 厳密にマッチさせると処理が重くなる
  • 万能で最適な正規表現は難しい
  • 言語によって使い方は変わっても、できることはだいたい同じであることが多い
  • マッチさせたい内容はどこかに書いておくと、後で役に立つかもしれない

補足

文字の指定方法

[0-9]や[A-Z]は:digit:や:alpha:と書くこともできます。実装している言語によって微妙に異なります。方言みたいなもんだと捉えてOKかと思います。

同じ正規表現でも言語やツールによって微妙に解釈が違うかも

例:HTMLタグを取り除く

% echo '<p>abc</p>' | egrep '<[^>]*>'		→ <p>abc</p>
 % echo '<p>abc</p>' | sed -e 's/<[^>]*>//'		→ abc</p>
 % echo '<p>abc</p>' | perl -ne 'print $& if /<[^>]*>/'		→ <p>

※ 上記の例はいずれも同じ文字列(<p>)がマッチングしますが、sedは「マッチした文字列を消す」という解釈のため、<p>が取り除かれて出力されます。perlは「マッチした文字列だけを出力する」ため、<p>が出力されます。

grepやawkは最短一致が使えない

最短一致は量指定子記号(*や+)の直後に?を書きますが、grepやawkだと思った通りの挙動とはならないようです。

例1:perl
% echo '[abc],[123]' | perl -ne 'print "$& \n" if /\[.*?\]/'
 [abc]
 % echo '[abc],[123]' | perl -ne 'print "$& \n" if /\[.*\]/'
 [abc],[123]
例2:grep
% echo '[abc],[123]' | grep -oE '\[.*?\]'
 [abc],[123]
 % echo '[abc],[123]' | grep -oE '\[.*\]'
 [abc],[123]
例3:sed
% echo '[abc],[123]' | sed -e '/\[.*?\]/p'
 [abc],[123]
 % echo '[abc],[123]' | sed -e '/\[.*\]/p'
 [abc],[123]
 [abc],[123]

おわりに

正規表現の習得はつらいかもしれませんが、ひとたび使えるようになれば日々の生活が彩られること間違いなしです(知らんけど)。この記事が手始めのきっかけになれば幸いです。

応用次第でこんなこともできちゃいます

リンク