第IV部〜テキスト編集を極める!!
|
●正規表現について
秀丸だけでなく、高度で自由度の高い検索をするには必須の「正規表現」。 ここでは実用上重要な例をとりあげながら、わかりやすく解説します。 今日からあなたの「検索」が変わります。
正規表現に関する話題は、 次の「【0】いんとろだくしょん」に続く 以下の5章から構成されています。【1】 基本編 〜表記のルールと意味〜
【2】 実践編 〜正規表現による置換の実際〜
【3】 まとめ
【4】 参考文献
【5】 (補足) 秀丸上での複数行にまたがる検索について※以下の内容は、秀丸ヘルプの「検索系コマンド→検索→正規表現」の 内容についての補足です。 よってヘルプを参照しながら読まれる事を想定して書かれています。
【0】 いんとろだくしょん
いきなりですがここでクイズです。
Q. 文書から「明日は…でした。」という日本語として 変な文を全て抜き出すにはどうしたらよいでしょうか?
但し、簡単のために現在のファイルは「1行につき1文」というルールで 作成されているとします。文書に検索を行って、ヒットしたものを全て抜き出す作業に grep 機能 を使うのは前提として、 ここで問題なのは どういう検索を行えばよいか という事です。 単に "明日は" や "でした。" を検索すると、 正しい文までヒットしてしまうのは言うまでもないでしょう。 とりあえずそれらで検索した後、 該当部分だけ抜き出せばいいというのも、
せっかく「こんぴゅーた」を使っているのに
何とも口惜しい…。 まぁ
「こんぴゅーた」なんてのは与えた命令しか実行しない無粋なモンだ
と言ってしまえばそれまでですが、逆に言えば、 こんぴゅーたに「何を探したいか」を適切に教えてやれば、 ちゃんと仕事をしてくれるはずです。
この例の場合、探したいものは
「明日は」で始まり、途中に何か文があって、「でした。」で終わる という 文字の並び方のルール として表現できますが、 このようにルールとして与えてやれば、
ルール通りに物事を運ぶことにかけては右に出るものはない
こんぴゅーたのことですから、きっとうまいこと(笑)やってくれるはず。
…前置きが長くなりましたが、こういう
文字の並びのルールを コンピュータが理解できるように表現したものが 『正規表現』
です。
ちょっと先走ってしまいますが、 言葉で表現した文字の並びのルールを正規表現を使って表現すると、
^明日は.+でした。$
となります。 上の正規表現には、"明日は" や "でした。" などのような 探したい文字そのもの の他、 "
^
" や ".+
" や "$
" という 記号 が含まれています。 すぐ後の【1】基本編 で説明しますが、 これらの記号は文字の並び方のルールを表現するための特別な意味 を持っていて、 上の例でいうと 「〜で始まり」「何か文があって」「〜で終わる」といった ルールに対応しています。 このように、正規表現は文字の並びのルールを言葉ではなく
(普通の)文字 + (文字の並びのルールを表現するための)記号
で表します。
なお、上の例のように正規表現を使って表した文字の並びのルール自体を正規表現、 または パターンと呼ぶことがあります。 用語の混乱を避けるために、 以下の説明では「パターン」と呼ぶことにします。
途中で脱線してしまいましたが、 探したいものをコンピュータにもわかるようにパターンとして表わすことができたら、 あとは検索文字列に上のパターンを書き込んで、
それいけー!!
というかけ声と共に検索を実行すれば、 望み通り変な文だけを検索することができます。では、以下の章でこれらの記号の意味と使い方について説明していきます。
【1】 基本編 〜表記のルールと意味〜
[1-1] メタキャラクタとエスケープシーケンス
まず前章の例の真ん中辺りにあった「.+
」ですが、 これは言葉で表現した文字の並びのルールの 「何か文があって」という部分に対応しています。 この記号は「.
(ピリオド)」と 「+
(プラス)」との組み合わせで、 それぞれ
.
(ピリオド) … 任意の1文字
+
(プラス) … 直前の文字の1回以上の繰り返しという意味を持っています。
ピリオドは、 MS-DOS のワイルドカード(懐かしい(笑))では 「
?
」と表現していたもので、 どんな1文字にもマッチ(*1)します。 例えば "あ.た
" というパターンは、 "あんた" にも "あいた" にも "あした" にも、 果ては
"あ#た" という 謎のダイイングメッセージ
にもマッチします(笑)。
(*1) 正規表現で書かれた検索文字列で対象の文を探していって、 それに一致する文字列を見出したとき、 「(パターンに)マッチした」と表現します。 プラス記号の方はちょっと注意が必要で、 これ単独では意味を成しません。 必ず直前に文字 があって、 その1回以上の繰り返しにマッチします。 例えば "
痛て+
" というパターンは "痛て" という軽い痛みから
"痛てててててて"
という悶絶するような痛み全てにマッチします。 正規表現を使わないと "痛て" と "痛てて" と "痛ててて" と…と全ての可能性をいちいち検索する必要がある所を、 このように
一発!
で指定できるわけです。今説明したピリオドやプラス記号のような、 正規表現の中で特殊な意味を持つ記号のことを メタキャラクタと呼びます。 メタキャラクタはこれらの他にも下記の表にあるようにいっぱい(笑)あります。
メタキャラクタ 意味 .
(改行文字を除く)任意の1文字にマッチ *
直前の文字(※)の0個以上の繰り返しにマッチ +
直前の文字(※)の1個以上の繰り返しにマッチ ?
直前の文字(※)の0個(=1個もない)または1個にマッチ ^
(パターンの先頭にある時のみ)行頭にマッチ $
(パターンの末尾にある時のみ)行末にマッチ [ ]
キャラクタクラス指定(後述) ( )
グルーピング指定(後述) |
パターンの論理和(後述) \
エスケープキャラクタ これら以外にも、特定の場所に現れた場合に限って特殊な意味を持つ記号 (キャラクタクラス指定の中の "
※ またはグルーピングされたパターンでもよい(後述)。 -
" 等)もあります。 ちなみに、これら以外の普通に記号と呼ばれている "@
" 等は、 正規表現では「普通の文字」として扱われます。・繰り返しを表わすメタキャラクタと「最長一致」のルール
先程説明した "
+
" とよく似た使い方をする メタキャラクタがあと2つあります。まずアスタリスク "
*
" は 「0個以上の繰り返し」となっていますが、 これは "痛て*" というパターンを例に取ると、 "痛て+" と同じ物を全て含む他に、
"痛" という余りの痛さにその後の声が続かない究極の痛み
も含んでいます(笑)。 ここで、"*
" に関して
必ずと言っていいほど注意される事
をここでも述べますが、 MS-DOS のワイルドカードの場合と違って、 正規表現の "*
" は 必ず直前の文字とペアで意味を持ちます。 逆に、MS-DOS の "*
"(空を含む任意の文字列にマッチ)は 正規表現では ".*
" となります。
一方、クエスチョンマーク "
?
" は 「0個(=1個もない)、または1個ある場合」を表しているので、 "エディター?
" というパターンは "エディタ" にも "エディター" にもマッチします。ここで、「痛てててててて!」という文(?)に対して "
痛て+
" というパターンで検索すると、 1回の検索でどこまでがマッチの対象になるか? ということが問題になります。 "痛て" もマッチするし "痛てて" もマッチするし…というわけで、 「検索を繰り返す毎に一つ一つ順番にマッチしていくの?」と考えた人は……
残念、ハズレです。
実際には 1回目の検索でいきなり "痛てててててて"(!の直前) までがマッチの対象、 つまり検索結果になります。この様に、正規表現による検索は
与えられたパターンにマッチする 最も長い文字列 が検索結果になる
という規則になっています (最長一致のルール)。
このルールがあるおかげで、 正規表現は我々に色々楽しい経験をさせてくれますが、 それらの例については【2】を参照して下さい。・行の区切りを表わすメタキャラクタ
"
^
" と "$
" は 【0】の例にも出てきていますが、 それぞれ「行頭」及び「行末」にマッチする、となっています。 では、以下のような場合について、 "^.+$
" という(任意の空でない1行にマッチする) パターンで検索してみたら、どういう結果になるでしょうか?
↓
この文はこの行の右端で折り返されています。 ここのことです→
この行の末尾で [Enter] キーで改行されています。#↓
[EOF]
この場合、見た目の行、 つまり2行目だけ("この文は" から "→" まで)がマッチしそうな所ですが、 実際にやってみると2行目の先頭から3行目の行末("#")まで がマッチします。 この例からもわかるように、正規表現で言う所の「行」とは 秀丸の画面上での「見た目の行」のこと ではなく、
ファイルの先頭 または 改行文字 (↓) から、 次の改行文字 または ファイルの末尾 (
を指しています(いわゆる「論理行」のこと)。 上の例では見た目が4行のファイルですが、論理行で数えると[EOF]
) で区切られた部分
の3行からなるファイル、と言う事が出来ます。 「テキストファイル」は(文字コード体系の違いを除けば) 普通の文字(*2)と改行文字の並びで構成されているので、 どんな環境でも論理行は同じものになります。 元々正規表現は sed 等のテキスト整形ツールで使われていたものですが、 それらはエディタの様にテキストファイルを「表示」することなく ツール内部だけで処理をするため、 見た目にはよらない形でテキストを扱うような 形式になっていなければならないわけです。
見た目の1行目(空ですが(^^;) = ファイルの先頭 〜 1行目の改行文字(の手前)まで 見た目の2行目から3行目の末まで = 1行目の改行文字(の次) 〜 3行目の改行文字(の手前)まで 見た目の4行目(1行目と同じく空ですが) = 3行目の改行文字(の次) 〜 ファイルの末尾([EOF])まで
(*2) タブ、改ページ文字等のいわゆるコントロール文字も含みます。 なお、"
^
" ("$
") は、 上記のように パターンの先頭(末尾)にある時にのみ 上のような特殊な意味を持ちます。 つまり "ab^cd$efg
" というパターンの中の "^
" や "$
" は、 単にその文字(ハットやドル)そのもの を表現しています。ここでちょっと脱線ですが、"
$
" と 似て非なる物 として "\n
" (改行文字) があります (後述の「エスケープシーケンス」を参照)。 直前の話からすると、 正規表現では「行末 = 改行文字」なわけで、 何でこんな余計なものがあるのか疑問に思う人がいると思いますが、 その理由は2つあります。
1つめは、"
$
" は ファイルの末尾 "[EOF]
" も含む ということです ("\n
" はあくまでも「改行文字そのもの」を表す)。 というわけで、最後の行が("↓" ではなく) "[EOF]
" で 終わっている場合も "$
" を使えば正しく処理できます。
2つめの理由は、置換を行う時に 改行文字も含めて置換したい場合 があるからです。 次の2つのパターン
//.*$
と
//.*\n
はどちらも
C++ 言語では // から行末までをコメントとみまします。↓ という行の "
//
" からそれ以降の文字にマッチしますが、 "$
" で行末を指定した場合はマッチの範囲に 改行文字("↓")を含みません。 一方 "\n
" の方は改行文字もマッチの範囲に含まれます。 このマッチの仕方の違いをうまく利用した置換の例については、 このQ&A集の 「●空白行を削除したい」 「●空白行を改行だけの行にしたい」 の2つの例を見てください。 逆に、これら2つを組み合わせて使う場合、 意図した結果が得られないことがありますが、 その例と解説については第5章をご覧ください。・エスケープキャラクタとエスケープシーケンス
メタキャラクタの表の最後にある "\" (エスケープキャラクタ) は 今までのメタキャラクタよりもっと特殊で、 それ自体では意味を持たず、 その直後の文字とペアになって一つの特殊文字 を表現します。 例えば前述の "\n"(改行文字) はその一つです。 このように "\" で作られる特殊文字の事を エスケープシーケンスと呼びますが、 それらを下の表にまとめてみました。
エスケープシーケンス 意味 \<oct> <oct> を8進数とみなして、それを文字コードに持つ文字 \x<hex> <hex> を16進数とみなして、それを文字コードに持つ文字 \a ベル (\x07) \b バックスペース (\x08) \t タブ (\x09) \n 改行 (\x0A) \v 垂直タブ (\x0B) \f フォームフィード(改ページ) (\x0C)(※) \r キャリッジリターン (\x0D) \z EOF(End Of File) 記号 (\x1A) \< 英単語の始まりにマッチ(文字ではない) \> 英単語の終わりにマッチ(文字ではない) \w 英単語にマッチ(アンダースコア "_" を含む) \<char> 上記以外の文字の場合は <char> そのもの
(メタキャラクタのエスケープに使用)
※ Ver.3.0 以降の秀丸では、" \f
" が一般的な意味の「改ページ(\x0C)」ではなく、「タグ付き正規表現」における区切り子として解釈されるようになりました。Ver.3.0 以降の秀丸で改ページコードを指定したい場合は、\x0C と記述する必要があります。例えば行の先頭がタブ文字で始まる行を検索するには "
^\t
" というパターンを指定します。 なお、"\<
" と "\>
" はその他の物とは異なり、 それぞれ英単語の先頭、末尾に一致します (行における "^
"、"$
" と同じ意味)。 例えば "\<a
" は "a" で始まる英単語 を検索するので、
This is a pen. That is the present of my father. の1文目の "a" にはマッチしますが、 2文目の "That" や "father" の中の "a" にはマッチしません。
最後の "\<char>" は、最初から特殊な意味を持っているメタキャラクタの 特殊な意味を消すために使います。 例えば "*
"(アスタリスク) を検索したい時には "\*
" と指定します。 あとよく出てくる話ですが、 "\
" 自身もメタキャラクタなので、 "\
" そのものを検索したい時は "\\
" と指定します。
[1-2] キャラクタクラスとグルーピングその他
・キャラクタクラス
前節で説明したメタキャラクタのうち、".
" (任意の1文字にマッチ) は非常に使える表現で、 これがなければ正規表現は
全く使い物にならないと言っても過言ではない
でしょう。 ですが、逆に
強力すぎて使えない
場面にもしばしば出会います。 例として以下のクイズを考えてみましょう。
Q. 日本語の文書に混じった英単語(半角のアルファベットのみ)だけを 探すには、どのような正規表現をかけばいいでしょうか? ここで、「英単語」とは 「半角の a 〜 z, A 〜 Z のみからなる文字の並び」なわけですから、 もし「任意の半角アルファベット1文字にマッチするメタキャラクタ」 があれば、 前節で説明したメタキャラクタ "
+
" と
合わせて一発!!
です。 が、幸か不幸か正規表現にはそんなメタキャラクタは用意されていません。
※ "\w
" はアンダースコア "_" を含むため、今の目的には使えません。が、落胆するのはまだ早いです。 何と正規表現には 任意のアルファベット どころか 「自分で勝手に定義した任意の文字の集まりに マッチするメタキャラクタ」 を作るルールが用意されているのです(わーいわーい)! それがいわゆるキャラクタクラス指定 です。
例えば "a", "b", "c", "d", "×" の5文字のうちの どれか1文字にマッチするメタキャラクタが欲しい時は、
[abcd×]
という風に、 含めたい文字を "
[
" と "]
" で 囲むだけでよいのです。 メタキャラクタ ".
" が (改行文字を除く)任意の1文字にマッチするのに対し、 キャラクタクラス指定はそれより 狭い範囲の文字の集まりの中の1文字にマッチするわけです。では、先のクイズに戻って 「任意の半角アルファベット」にマッチするキャラクタクラスを作りましょう。 もちろん
[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]
で正解なのですが、 これは以下のようにもっと短縮した形で書く事ができます。
[A-Za-z]
ここで、"
-
" は (文字コードで見て) その前後の間にある文字を全て含める、という意味になっています (というわけで "[Z-Az-a]
" は 間違い)。
※ この " -
" を使った表現は 厳密には機種(というか採用している文字コード)に依存しますが、 今回はこれ以上深い説明は割愛しますm(_ _)m。さて、キャラクタクラス指定によって 「任意のアルファベットを表す」表現が出来たので、 あとはそれを1個以上繰り返す、という指定をすればよいわけです。 というわけで結局クイズの答は
[A-Za-z]+
ということになります。
ここで、いくつかの典型的なキャラクタクラス指定を紹介します。
キャラクタクラス 意味 [0-9]
半角の数字にマッチ [ぁ-ん]
全角ひらがなにマッチ [ァ-ヶ]
全角カタカナにマッチ [ -~]
任意の1バイト文字(半角空白" "から"~"まで)にマッチ [亜-K]
漢字にマッチ (詳細は●漢字だけを探したい を参照) キャラクタクラス指定は 「任意の」という範囲が広すぎる "
.
" に比べて、 より狭い範囲の文字の集まりに限定してマッチさせるものでしたが、 この逆、つまり「指定したいくつかの文字以外の1文字にマッチする」 ような指定はできないのでしょうか? …と白々しく(笑)ネタを振るぐらいなので ちゃんとそういうことができるようになっていて、 "[
" と "]
" の代わりに "[^
" と "]
" で囲むと、 「その間にある文字以外の1文字(改行文字も含む(*3))」 という意味の指定になります (「[^0-9]
(半角数字でない1文字)」etc.)。ここでよく勘違いされるので注意が必要ですが、 例えば "
[^abc]
" という指定は 「"abc" でない文字列」という意味 ではなく、 「"a" でも "b" でも "c" でもない1文字」という意味になります。
※ 「"ABCDE" でない、"A" で始まって "E" で終わる5文字の文字列」を 指定したつもりで " A[^BCD]E
" と指定する etc.…ここまでくると、 「じゃあ "^" や "-" をキャラクタクラスに入れたい時はどうするの?」 という声が聞こえてきそうですが、 これもしっかり逃げ道 があって、 "
^
" は "[
" の直後以外、 "-
" は "[
" (または "[^
") の直後か "]
" の直前に書けば、 キャラクタクラス指定に入れることができます。
あ、そうそう。メタキャラクタは キャラクタクラス指定の中では (エスケープキャラクタを前に置かなくても自動的に)その特殊な意味を失って、 単にその文字自身を表わすようになります。 ただし "
]
" は例外で、 ちゃんと "\
" を前に付けないと駄目です…って当たり前ですが(^^;。
(*3) 古い正規表現では否定のキャラクタクラス指定に 改行文字は含まれませんでしたが、 新しい正規表現(秀丸が内部で使用している正規表現エンジン JRE32.DLL も)では 改行文字も含まれるようです。 ・グルーピング
これまで説明したルールでかなり柔軟な検索指定ができるわけですが、 例えば「うひうひうひうひ…」という
オヤジのイヤラシイ笑い
にすべてマッチするようなパターンはまだ書けません。 例えば "うひ+
" だと、 "うひ" にマッチするのはまだいいとして、 "うひひ" とか "うひひひ" にマッチしてしまいます…ってこれも
意味的にはOK
ですが(笑)。 …まぁ冗談はさて置き、この場合 「"うひ" が何回か繰り返されるパターン」 が欲しいわけですが、 これを実現するために グルーピングを使います。 「グルーピング」というと何だか難しそうですが、 単に "*
" や "+
" のような繰り返し指定の対象を 「1文字」から「いくつかの文字の並び」に広げるために、 それらの文字を「グループにまとめ」て、 あたかも1つの文字であるかのように扱う、それだけのことです。グルーピングを使うと、 今の例は以下のようなパターンを指定すれば望みの検索ができます。
(うひ)+
つまり、"
(
" と ")
" で 括られた物をひとまとめにして(=グループにまとめて) それを繰り返す、という指定をするわけです。上の説明では「いくつかの文字の並び」と表現しましたが、 一般に括弧の中には文字の並びだけでなく 任意のパターンを書くことができます。 そして、括弧で括ったパターンに対して繰り返しの指定を行うと、 そのパターンの繰り返しにマッチすることになります。 例えば
(うひ+)+
というパターンを指定すると、
うひうひひうひうひひひひひ
という
もはや正気の沙汰ではない笑い
にもマッチするようになります(笑)…って笑ってる場合じゃないですね。 ちゃんと説明すると、"うひ+
" というパターンに対して、 前から順に「うひ」「うひひ」「うひ」「うひひひひひ」がマッチしています。 で、括弧の中のパターンにマッチするものが(1個以上)並んでいるわけですから、 この文全体が "(うひ+)+
" というパターンにマッチするわけです。さて、今度は「"今日" または "明日"」のように
パターンもヘッタクレもない、単なるワガママのような検索条件
に使えるルールですが、 この場合は
今日|明日
と指定すればOKです。 ここで、"
|
" (縦棒) は 「〜または〜」という意味を持ち、 (ソフトウェアの制限の許す限り) 何個でもつなげて指定できます。 つまり、
一昨日|昨日|今日|明日|明後日
のような指定ができるわけです。 なお、この場合は先程説明したグルーピングを用いて、
(一昨|昨|今|明)日
という指定や、さらに
(一昨|[昨今明])日
という指定もできます。
【2】 実践編 〜正規表現による置換の実際〜
前章では「検索」のための正規表現の書き方を紹介しましたが、 この章では「置換」の場合の正規表現の書き方と、 その時に陥りやすい「落とし穴」について紹介します。
[2-1] 「置換・削除」と「最長一致のルール」 …うわぁ!みんな消えちまった!!
ここでは実用性の高そうな例として
いんたーねっとのほーむぺえじ
の HTML ファイルの、「タグ」と呼ばれる "<"と">"で囲まれた記号を削除することを考えてみましょう。 以下は、HTML ファイルの中身を一部だけ抜き出した物です。
<PRE>これ以後は見た目通りの改行位置で改行されます。↓
<FONT SIZE=5>フォントサイズ指定子</FONT>の例です。↓
<HR><!-- 横線を引きます(これは注釈です) -->↓
<CENTER>中央揃え指定子</CENTER>↓
</PRE>これ以後は改行が無視されます。↓ここで、"<PRE>" や "</FONT>" が 「タグ」の例です。 通常の置換では、何種類もあるこれらのタグをいちいち指定して 何回も置換を繰り返すことになりますが、 正規表現を使えば これらのタグを「1回の置換」で削除することができます。
・失敗例
要するに「タグ」とは "<" と ">" で囲まれたものなわけで、 正規表現で表すと
<.*>
でよさそうです。 というわけで、取りあえず
検索文字列: <.*>
置換文字列: (何も書かない) で「全置換」を実行してみましょう。 以下がその結果です。
これ以後は見た目通りの改行位置で改行されます。↓
の例です。↓
↓
↓
これ以後は改行が無視されます。↓ありゃりゃ(笑)。 1、3、5行目の結果は問題ないですが、 2行目と4行目の結果は???です。
・考察
ここで何が起こったのかを理解するために、 正規表現の「最長一致のルール」を 思い出しましょう。 これは「正規表現による検索では 与えられたパターンに一致する最長の文字列 がマッチする」というルールでした。
上の例では、 2行目は "<FONT SIZE=5>" の始めの "<" から、 "</FONT>" の終わりの ">" までがマッチしていることに 気がつきます(4行目についても同様)。 前章で説明したように ".
" は 「(改行を除く)任意の文字」に マッチしますから、 確かに処理としては正しいことをしています (間違うのはいつも人間 :-P)。
では、正しく「タグ」だけを指定するにはどうしたらよいのでしょうか? この場合は「タグ」の性質をよく考えれば比較的簡単に答がわかります。 「タグ」とは要するに 「"<" で始まり、最初に現れた ">" で終わる」ものなので、 「任意の」という部分を 「">" を除いた任意の」という 表現に置き換えてやればよいわけです。 これを正規表現で表現したものが以下です。
検索文字列(改良版): <[^>]*> ではこれで置換を実行してみましょう。以下がその結果です。
これ以後は見た目通りの改行位置で改行されます。↓
フォントサイズ指定子の例です。↓
↓
中央揃え指定子↓
これ以後は改行が無視されます。↓…どうやらうまくいったようです。
上の例で見たように、"
.
" という表現は 便利で正規表現には無くてはならないものなんですが、 「置換」の場合は
使い方をよく考えないと思わぬ結果を引き起こす
というわけです。・補足(タグの消去の方法…実践編)
上の例では複数行にまたがるタグや、コメントタグの中に書かれたタグ のことを考慮していませんでした。 それらも考慮した、タグをほぼ完全に消去するパターンを以下に示しておきます。
<!--(.|\n)*-->|<[^>]*>
※ ただし、この正規表現では 約3行以上に渡るタグを正しく置換できません (後述の【5】秀丸上での複数行にまたがる検索について を参照して下さい)。
[2-2] 「文字列の否定」について…あきらめずに頑張ろう!!
今度はC言語のコメントを削除する例を考えてみましょう。
C言語のコメントは "
/*
" と "*/
" で囲まれた部分です。 この場合も前節の HTML タグの削除でやったように "/*
" と "*/
" の間に "*/
" を「否定」する 表現を入れてやればよい事は
明白です
…と言いたい所ですが、 正規表現には否定のキャラクタクラス指定という 「文字」の否定をする表現はありますが、 直接「文字列」を否定する表現はありません。
…もうおわかりでしょうが、 "[^*/]
" は 「"*
" でも "/
" でもない 1文字」という意味であり、 「"*/
" でない文字列」 という意味にはなりません。
となると、
じゃあ正規表現ではC言語のコメントもちゃんと消せないのぉー。 だっせー+!
という声が聞こえてきそうですが、 どうぞご安心を(笑)。
知恵さえしぼれば
ちゃんと消すことができます。 では、以下の2つの例で「知恵のしぼり方」をご覧ください。・C言語のコメントを消す
"/*
" と "*/
" の間にある文字列のパターンは、 以下のように表わすことができそうです。
「"/" 以外の文字」と「直前が "*" 以外の "/"」を組み合わせた文字列 ここで、"
/
" の直前に "*
" がないことを指定して実質的に 「文字列の否定」をしていることに注意して下さい。ちなみに、同じような表現と思われる
「"*" 以外の文字」と「直後が "/" 以外の "*"」を組み合わせた文字列 ではうまく行きません。 理由は、"
**/
" の場合に "**
" (直後が "/
" 以外の "*
") と "/
" ("*
" 以外の文字) という風にマッチしてしまうため、 例えば
/***/ /***/ の "/***/ /***/" 全てにマッチしてしまうからです。
以上のことを考慮して、 ちゃんとコメントにマッチする(と思われる)パターンを書いたものが以下です。
検索文字列: /\*([^/]|[^*]/)*\*/
("
*
" は メタキャラクタなので、 キャラクタクラス指定の中にある "*
" 以外は "\
" でエスケープしてあります。)これで一見うまくいくように思えます。 が、実はこのパターンは以下のようなコメント(?)にマッチしません。
/*/ */
一般に、"
/*
" の直後に "/
" がある場合はアウトです。 なぜかというと、 上のパターン(の "[^*]/
" という部分)では "/
" の直前に必ず1文字存在することを要求しているからです。 ("/*
" は既に "/\*
" がマッチしているので対象外です。)というわけで、 この特殊な場合も考慮したパターンを作る必要があります。 で、それは以下のようになります。
検索文字列(改良版): /\*/?([^/]|[^*]/)*\*/
("
/?
" という指定で、(もしあれば) "/*
" の直後の "/
" にマッチさせています。)
※ ただし、この表現のままでは 複数行に渡るコメントは 正しく置換できません。 これはこのパターンが不完全なのではなく、 秀丸の正規表現検索の仕様によるものです (後述の【5】秀丸上での複数行にまたがる検索について を参照して下さい)。 任意の行数にまたがるコメントを削除することは その仕様のためにできないのですが、 例えば2行以下のものであれば以下のパターンを使えば可能です。
検索文字列(2行までのコメントにマッチ): /\*/?(\n|[^/]|[^*]/)*\*/
"
[^/]
" は「"/
" 以外の1文字」を表しているので、 その中には "\n
" も含まれています。 したがって、真ん中の括弧の中に入れた "\n
" は正規表現としては不必要なものです。 しかし、この "\n
" を加えることで秀丸の動作が変わり、 複数行にまたがるコメントも検索できるようになります。・間違ったヘッダを持つ行を探す
行の先頭から "P:XXXX"("XXXX" には半角の数字が入る) となっているはずが、 "P" が "p" になっていたり忘れているような場合や、 ":" が ";" になっていたり忘れているような行を 探すことを考えます。
この場合、「行の先頭が "P:" ではない」という 「文字列の否定」ができれば話は簡単なのですが、
そうは問屋が卸さない
のは前の例で述べた通りです。 というわけで、ここでも知恵をしぼって考えることにしましょう。問題の条件から、具体的にどのような間違いが有り得るかをまず考えると、 行の先頭から2文字についてそれぞれ3通り (正しい文字、間違っている文字、文字がない)の可能性あり、 3×3−1の計8通りの間違いがあるはずです。 もちろんそれらを全部 書き並べてもいいのですが、 ここではもう少し効率のよい方法を紹介します。
ます、8つの間違いが以下の4つのグループにわけられることを使います。
0123 (何もない) P0123, :0123, etc. (文字が1つだけしかない) p:0123, p;0123 (文字が2つあっても先頭が "P" でない) P;0123 (2つあって先頭が "P" でも次が ":" でない) で、これを素直に 正規表現で表わすと
検索文字列: ^(|.|[^P].|P[^:])[0-9]+$
となります。 2番目と3番目、 特に2番目は "
.
" を使うことで4つの間違いにマッチしています。・一般の「文字列の否定」はどうするか?
上の2例については運良く(笑) 解答が見つかりましたが、 では一般的に「文字列の否定」をするにはどうしたらよいのでしょうか? もう少し正確な言い方をすれば、 「部分的に〜という文字列を含まない文字列にマッチするパターン」 を作る一般的な方法はあるのでしょうか? …と、ここで「一般論」を説明できれば
何て素晴らしい解説なんだ
とは思いますが(笑)、 筆者の知る限りでは
全ての場合に通用する「一般論」は存在しない
と思います (もしご存知の方がいらっしゃいましたら、筆者までご一報くださいm(_ _)m)。 が、しかし、上で見たように
知恵をしぼって考えれば
何とかなってしまいます(断言)。 別の言い方をすれば
ケース・バイ・ケース
であれこれ試していけば、 そのうちにきっと答が見つかります。
※ この節の最初の例も 第2版 Rev.D までは「答は無い」と考えていましたが、 色々試したら結局解答(に近いもの?)が見つかっちゃいました(^^;。
【3】 まとめ
というわけで多少駆け足で説明してきましたが、 正規表現を使う事に対する大体の感覚は掴めてもらえたと思います。 あとは、実際に自分で色々な場合について パターンを書いてみることをお勧めします。
その時に以下のことを念頭においてパターンを考えてみて下さい。
最後に、貴方の
・ 探したい物を理路整然とした 「言葉」で表現できるか? ・ その「言葉」の各パートに対応したパターンは何か? ・ (置換の場合は特に)そのパターンは 必要十分(最小限)なものか?
#試しに「検索」のみでやってみるのもいいかも。
正規表現ライフ(謎)
が実りある物になることを祈りつつ…。
【4】 参考文献
正規表現は sed や awk といった UNIX 関連のツールに対する書籍を買えば 大抵説明されていますが、 今回参考にした本だけを参考文献として紹介します。
#他にも「これがいい!」という本がありましたら教えて下さいm(_ _)m。
AWKを256倍使うための本
(アスキー書籍編集部編 志村 拓・鷲北 賢・西村克信 共著)
正規表現に関してはホンの一部だけ説明されていますが、 例(?)が豊富でしかも笑えます。 今回の執筆&改訂に多大なる影響を与えてくれました。
『sed&awkプログラミング』
(アスキー出版局編 Dale Dougherty 著 福崎俊博 訳)
いわゆる「教科書的」な構成で、読むのに結構根気がいるんですが、 一冊ぐらいはこういう本を読まないと…ね(←意味不明)。
【5】 (補足) 秀丸上での複数行にまたがる検索について
第1章で "$
" と "\n
" の違いを説明しましたが、 この章では この2つを組み合わせたパターンを使う場合の注意について説明します。例えば "
@
" を含まない、 空でない任意の1行を検索する場合、 今までの説明をちゃんと読んできた貴方なら 以下のようなパターンが思い浮かぶはずです。
[^@\n]+$
さて、このパターンを以下の3つのファイルで検索してみましょう。
ファイル1
↓
この行はマッチするはず↓
[EOF]
ファイル2
↓
この行もマッチするはず…??↓
↓
[EOF]
ファイル3
↓
↓
この行もマッチするはず↓
↓
[EOF]
カーソルを1行目に置いてから検索を行うと、 ファイル1と3は期待通りに2行目及び3行目にマッチしますが、 ファイル2の2行目は期待に反してマッチしません。
なんじゃそりゃ〜!?
と思わず叫びたくなるような結果ですが、 ここはひとまず冷静になって何がどうなってるのかを考えてみましょう…
てなことやってるといつまでたってもおわんないし〜
というわけであっさり降参して秀丸ヘルプを見ると、 「検索系コマンド」の中にある 「\n
を使った複数行検索の際の制限について」に こんなことが書いてあります。
秀丸エディタでは 検索元文字列の中に「\n」がいくつ含まれているかを計算し、 その数+1の行まで検索でヒット可能にします。 そういう特殊な仕組みのため、\n 自体を任意の回数繰り返すような正規表現は、 期待通りに動作しないことがあります。
…だそうです(笑)。 「期待通りに動作しないことがある」と言われて納得できればそれでよし! ですが、
なぜにそんなヘンテコなことになるのか気になって夜も寝られない(涙)
とご心配の方のために、 以下でそのカラクリについて説明します。込み入っているので順序立てて要点だけ挙げていきますが、
- 秀丸の正規表現検索は外部ツールである
JRE32.DLL
に 編集中のテキストの一部を渡す事で実行している。- 秀丸自体は正規表現を解釈していないので、
JRE32.DLL
の関数に渡すテキストの範囲を 現在のカーソル位置の次の文字から 検索文字に含まれる "\n" の個数+1行分、と決め打ちしている。JRE32.DLL
は、 渡されたテキストの末尾に "\n" があればそれを "$" にマッチさせる。 なければテキストの末尾の次に仮想的に "\n" を追加し、 それを "$" にマッチさせる。 逆に、テキストの途中に現れる "\n" には "$" はマッチしない(*5)。- 2. の仕様により、例えば先の例ではそれぞれ以下のテキストが
JRE32.DLL
の関数に渡される。
ファイル1: この行はマッチするはず\n
ファイル2: この行もマッチするはず…??\n\n
ファイル3: \nこの行もマッチするはず\n
ファイル1と3のテキストの末尾には "\n" があり、 それが "$" に置きかえられ、 ファイル2のテキストの末尾には二つ "\n" があるが、 その内の2番目の "\n" のみが "$" に置き換えられる。
ファイル1: この行はマッチするはず$
ファイル2: この行もマッチするはず…??\n$
ファイル3: \nこの行もマッチするはず$
この中で "[^@\n]+$
" にマッチするのは "$" の直前に "\n" を持たない1と3だけ、となる。というわけで、 複数行にまたがる検索の結果は期待通りには行かない場合があるので、 きっちりとした処理がどうしても必要であれば、 正規表現検索(置換)だけに頼らずに マクロか sed 等のフィルタ系ツールで処理しましょう(*6)。 ここではマクロと正規表現の組み合わせの例として、 2行以上に渡る改行のみの行を1行にする、 という置換を行うマクロを紹介します(Thanks to TAKA さん!!)。
// 改行のみの行が連続していたら1行にするマクロ // ※ファイルの終端から検索&置換していくのがミソです。 // 以下のように先頭から置換するとうまく行かない場合があります。 // replaceall "^\\n\\n+", "\n", regular; // 駄目な例 // // なお、マクロについての解説はこちらを参照して下さい。 // gofileend; replaceup "^\\n\\n+", "\n", regular; while (result) findup;
(*5) テキストの途中の "\n" にも "$" がマッチするようになれば 少なくとも今回の例のような事はなくなるわけですが、 これは JRE32.DLL
の代わりにBRegIf.DLL
(Arimac さん作) とBREGEXP.DLL
(Babaさん作) を組み合わせて使えば まさにそのような動作になります。 これらの DLL を使えばその他の拡張機能も利用できるようになるので、 デフォルトの正規表現検索では満足できないという方は(そうでない方も(^^;) ぜひ試してみて下さい。 なお、お二方の Web ページの URL は(2001 年5月中旬の時点で) 以下の通りです。
Arimac さんの Web ページ: https://www.arimac.com/
Babaさんの Web ページ: http://www.hi-ho.ne.jp/babaq/index.html
(*6) sed を使う場合も複数行にまたがる置換をする時は ちょっと面倒な処理をする必要があります。 もっとも、元々 UNIX 由来のツールは行指向で作られているものが多いので、 こういう特殊ケースは どうしても扱いが面倒になってしまうのはしょうがないです。