やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「正規表現」を読む1

 Unicodeのプロパティ指定は使えそう。

成果物

情報源

正規表現

  • メタ文字列とリテラル、メタ文字とエスケープ
  • 式展開
  • 文字
  • 任意の1文字
  • 文字クラス
  • 特別な文字列に対するマッチ
  • 繰り返し
  • キャプチャ
  • グループ
  • 部分式呼び出し(subexpression call)
  • 選択
  • アンカー
  • 条件分岐
  • オプション
  • エンコーディング
  • コメント
  • フリーフォーマットモード
  • 非包含オペレータ (absence operator) (実験的)
  • 一覧
  • 特殊変数
  • 参考文献

正規表現(regular expression)は文字列のパターンを記述するための言語です。また、この言語で記述されたパターンも正規表現と呼びます。

 sedコマンドで置換するときなどでお世話になる。

正規表現を用いると、文字列が指定したパターンを含んでいるかどうかを判定し、また含んでいるならばそれが文字列中のどの場所であるかを知ることができます。

/pat/
%r{pat}

などの正規表現リテラルRegexp.new などで正規表現オブジェクトを得ることができます。

 なるほど。で、どうやって使うの? とりあえず適当にやってみる。

> /[rR]uby/.match('ruby')
=> #<MatchData "ruby">
> p 'OK' if /[rR]uby/.match('ruby')
=> "OK"
> p 'OK' if /[rR]uby/.match('Nuby')
=> nil

メタ文字列とリテラル、メタ文字とエスケープ

正規表現の文法には、正規表現内で特別な働きをする文字列と、それ以外のその文字列そのものにマッチするような文字列があります。前者をメタ文字列(meta string)、後者をリテラル(文字列)(literal string)と呼びます。

/京都|大阪|神戸/

という正規表現においては、「京都」「大阪」「神戸」がリテラルで、 2つの「|」がメタ文字列です。

 あい。

以下の文字は「メタ文字」(meta character) と呼ばれる、正規表現内で特殊な働きをする文字です。

( ) [ ] { } . ? + * | \

これらの文字をリテラルのようにその文字としてマッチさせるためには、バックスラッシュ「\」を前に付けます。「\」はバックスラッシュ1文字にマッチします。

 メタ文字のエスケープね。

メタ文字以外の文字も、メタ文字に続けて置くことで特別な働きをするようになる場合があります。つまりメタ文字列を構成します。例えば

/[a-z]/
/\Axyz\Z/

という正規表現において "[a-z]", "\A", "\Z"はメタ文字列です。

 [a-z]はわかるけど、\A,\Zって何? もしや行頭・行末か? ^, $ではないの?

式展開

正規表現内では、#{式} という形式で式を評価した文字列を埋め込むことができます。

place = "東京都"
/#{place}/.match("Go to 東京都") # => #<MatchData "東京都">

 これはRubyならでは。

埋め込んだ文字列にメタ文字が含まれているならば、それはメタ文字として認識されます。

number = "(\\d+)"
operator = "(\\+|-|\\*|/)"
/#{number}#{operator}#{number}/.match("43+291")
# => #<MatchData "43+291" 1:"43" 2:"+" 3:"291">

 \\というのがダサい。//で囲ってみた。

number = /\d+/
operator = /(\+|-|\*|\/)/
p /#{number}#{operator}#{number}/.match("43+291") => #<MatchData "43+291" 1:"+">

 なんか出力結果がちょっと違うように見えるけど、たぶん同じっしょ。

埋め込む文字列をリテラルとして認識させたい場合は Regexp.quote を使います。

 はぁ。動くコード書いてくんない?

文字

正規表現内では、「\」の後に文字列を置くことで、ある特定の文字を表現することができます。これは、改行のように Ruby の文法で特別な意味を持つ文字を埋め込むためなどに用いられます。文字列リテラルの記法とほぼ同様(リテラル/バックスラッシュ記法)で、以下の記法が利用可能です。

\t           水平タブ            horizontal tab (0x09)
\v           垂直タブ            vertical tab   (0x0B)
\n           改行                newline        (0x0A)
\r           復帰                return         (0x0D)
\b           バックスペース      back space     (0x08)
\f           改ページ            form feed      (0x0C)
\a           ベル                bell           (0x07)
\e           エスケープ文字      escape         (0x1B)
\nnn         符号化バイト値の8進数表現 (nnn の8進数3文字で表現)
\xHH         符号化バイト値の16進数表現 (HH16進数2文字で表現)
\cx, \C-x    制御文字 (x は a から z までのいずれかの文字)
\M-x         メタ (x|0x80)
\M-\C-x      メタ制御文字
\uHHHH       ユニコード文字 (HHHH16進数4桁)
\u{HHHHHH HHHHHH ....} ユニコード文字列 (HHHHHH16進数1桁から6桁まで指定可能)

\b は文字クラス内でのみ有効な表現です。文字クラスの外では単語の区切りを表すメタ文字列と解釈されます。

 文字クラスってなに? 説明なしに言われてもわからん。ググったら[a-z]のように[]で囲ったやつのことらしい。

「\s」は文字列では空白(0x20)を意味しますが、正規表現ではタブなどを含む空白文字全般にマッチするメタ文字列です。

任意の1文字

メタ文字 . は改行を除く任意の1文字にマッチします。

 よく?+と組み合わせて使う。

ただし、オプション m によって改行にもマッチするようになります。

 複数行モードってやつか。

文字クラス

文字クラス(character class) とは角括弧 [ と ] で囲まれ、1個以上の文字を列挙したもので、いずれかの1文字にマッチします。

 ここで説明きたよ。

/W[aeiou]rd/

は Ward, Werd, Wird, Word, Wurd のいずれかにマッチします。

 はい。

文字クラス内のハイフン(-)は文字の範囲を表すメタ文字です。例えば [abcd] という文字クラスは [a-d] と表すことができます。複数の範囲指定をすることもできます。例えば [abcdpqrs] は [a-dp-s]と表すこともできます。

 まあ大抵は[a-z]とかで使うと思う。

文字クラスの [ の直後の文字がキャレット(^)である場合、列挙「されていない」文字にマッチするようになります(これは否定文字クラスと呼ばれます)。

[^a-d]

はabcd以外の1文字にマッチします。

 [^]は否定形。

文字クラス内に別の文字クラスを含めることができます。 [a-z[0-9]] は [a-z0-9]と同じ意味を持ちます。これだけではあまり意味がありませんが、文字クラスは && という、共通部分を取る演算をサポートしているため、これと組合せることで意味を持ちます。

 [a-zA-Z0-9]みたいな使い方ね。&&なんて知らんかった。

/[a-z[0-9]]/.match("y") # => #<MatchData "y">
/[a-z[0-9]]/.match("[") # => nil
r = /[a-w&&[^c-g]e]/ # ([a-w] かつ ([^c-g] もしくは e)) つまり [abeh-w] と同じ
r.match("b") # => #<MatchData "b">
r.match("c") # => nil
r.match("e") # => #<MatchData "e">
r.match("g") # => nil
r.match("h") # => #<MatchData "h">
r.match("w") # => #<MatchData "w">
r.match("z") # => nil

文字クラスでは、否定(^)範囲(-)共通部分(&&)列挙(並べる)という演算が可能ですが、これらは - > (列挙) > && > ^ という順の結合強度を持ちます。

 複雑になってきた。

文字クラス内の3つのメタ文字を通常の文字の意味で使用したい場合には、 \ によってエスケープ する必要があります。

 \^, \-, \&ってことかな。

文字クラスの略記法

良く使われる文字クラスには省略記法が存在します。

\w 単語構成文字 [a-zA-Z0-9_]
\W 非単語構成文字 [^a-zA-Z0-9_]
\s 空白文字 [ \t\r\n\f\v]
\S 非空白文字 [^ \t\r\n\f\v]
\d 10進数字 [0-9]
\D10進数字 [^0-9]
\h 16進数字 [0-9a-fA-F]
\H16進数字 [^0-9a-fA-F]

これらの「空白」「数字」などは ASCII の範囲の文字のみを対象としています。いわゆる「全角アルファベット」「全角空白」「全角数字」などはここの空白、数字、には含まれません。

 えー。日本語ダメじゃん。

/\w+/.match("ABCdef") # => nil
/\W+/.match("ABCdef") # => #<MatchData "ABCdef">
/\s+/.match(" ") # => nil
/\S+/.match(" ") # => #<MatchData " ">

 \W\Sは単なる否定形。決して全角に絞ってヒットしてくれるわけではない。残念。

これらは文字クラス内で演算することもできます。

r = /[\d&&[^47]]/ # 4, 7 以外の数字
r.match("3") # => #<MatchData "3">
r.match("7") # => nil

 これはちょっと便利かも?

Unicode プロパティによる文字クラス指定

また、Unicodeのプロパティ(属性情報)による文字クラス指定も可能です。以下の記法が使えます。

\p{property-name}
\p{^property-name} (否定)
\P{property-name} (否定)

サポートされているプロパティのリストは https://github.com/k-takata/Onigmo/blob/master/doc/UnicodeProps.txt を参考にしてください。また、プロパティの意味は Unicode の仕様を参照してください。

 URLをみるに、おそらくプロパティとは文字種のことだろう。

/\p{Letter}+/.match(".|あaABc123") # => #<MatchData "あaABc">

 Unicodeについて勉強しないと有意義に使えるのかどうかすら判断できない。

ひらがなの文字クラスは[\p{Hiragana}]とすっきり書くことができます。

 ま・じ・か!

 これは使えそう。

漢字を[\p{Han}]と疑問の余地なく簡潔に書くことができます。

 なんでHanなの? めっちゃ疑問の余地あるんですけど。

[\p{Han}]で表現される漢字には日本語だけではなく中国語や韓国語で使用される漢字もすべて含まれます。

 ハングル文字のHANなのか? 知らんけど。

長音がKatakanaに含まれていません。

これを回避するには、[\p{Katakana}ー]のように文字クラスに長音をじかに追加してください。

 結局そうなるのかって感じ。句読点とか鉤括弧とかも考えたらどうなることやら。

約物(パンクチュエーション:英語の感覚で言うとアルファベットと数字以外のすべて)とマッチさせたければ[\p{P}]と書けばよい

p /\p{Hiragana}+/.match('ぜんぶひらがな')
p /\p{Hiragana}+/.match('一部ひらがなダヨね')
p /\p{Katakana}+/.match('ゼンブカタカナ')
p /\p{Han}+/.match('漢字。') # 日本語中国語韓国語ベトナム語の漢字すべてにマッチする
p /[\p{Katakana}ー]+/.match('ゼンブカタカーナ')
p /[\p{Katakana}\p{P}ー]+/.match('「ゼンブ、カタカーナ。ダ!ヨ?」')
p /[\p{Han}\p{Hiragana}\p{Katakana}\p{P}ー]+/.match('「漢字、ひらがな!カタカーナ?。」')

 でも全角英数字がヒットしないんだよね。半角のそれらと使い分けられるのかな? あと絵文字や顔文字は? まだまだ勉強せにゃならん。

POSIX 文字クラス

Unicodeプロパティと似た機能を持つ記法として、POSIX 文字クラスと呼ばれるものがあります。これらは上の省略記法とは異なり、文字クラスの中でしか用いることができません。これらは [:クラス名:] という記法を持ちます。また、[:^クラス名:]という記法でその否定を意味します。以下の括弧では実際にどの文字にマッチするかが Unicode プロパティや Unicode コードポイントで示されています。

[:alnum:] 英数字 (Letter | Mark | Decimal_Number)
[:alpha:] 英字 (Letter | Mark)
[:ascii:] ASCIIに含まれる文字 (0000 - 007F)
[:blank:] スペースとタブ (Space_Separator | 0009)
[:cntrl:] 制御文字 (Control | Format | Unassigned | Private_Use | Surrogate)
[:digit:] 数字 (Decimal_Number)
[:graph:] 空白以外の表示可能な文字(つまり空白文字、制御文字、以外) ([[:^space:]] && ^Control && ^Unassigned && ^Surrogate)
[:lower:] 小文字 (Lowercase_Letter)
[:print:] 表示可能な文字(空白を含む) ([[:graph:]] | Space_Separator)
[:punct:] 句読点 (Connector_Punctuation | Dash_Punctuation | Close_Punctuation | Final_Punctuation | Initial_Punctuation | Other_Punctuation | Open_Punctuation)
[:space:] 空白、改行、復帰 (Space_Separator | Line_Separator | Paragraph_Separator | 0009 | 000A | 000B | 000C | 000D | 0085)
[:upper:] 大文字 (Uppercase_Letter)
[:xdigit:] 16進表記で使える文字 (0030 - 0039 | 0041 - 0046 | 0061 - 0066)
[:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)

これらの POSIX 文字クラスは \s といった省略記法と異なり、 ASCIIコード範囲外の空白などを考慮に入れます。

/[[:alnum:]]+/.match("abAB121") # => #<MatchData "abAB121">
# \u3000 は全角空白
/[[:graph:]]/.match("\u3000") # => nil
/[[:blank:]]/.match("\u3000") # => #<MatchData " ">
/[[:alnum:]&&[:^lower:]]/.match("aA") # => #<MatchData "A">
/[[:print:]&&[:^lower:]]/.match(" ") # => #<MatchData " ">

注: POSIX ではここで言う文字クラスのことを「ブラケット表現」と呼び、[:xxx:] というのを文字クラス、と呼んでいます。よって、 POSIX文字クラス、というのは厳密には「POSIXブラケット表現における文字クラス」と呼ぶべきものですが、ここでは「POSIX文字クラス」と呼ぶことにします。

 ブラケットって[,]文字の名前だっけ? ややこしいなぁ。

注: [:word:] と [:ascii:] は POSIX では定義されていません。 Ruby/Oniguruma/Onigmo独自のものです。

 じゃあ厳密にはPOSIX文字クラスじゃないじゃん。

注: エンコーディングによってこれらの POSIX 文字クラスの挙動が異なります。上に書いている「マッチする文字」は Unicode 系統のエンコーディングで使われるものです。 Unicode 系統以外のものは Onigmo のドキュメントを参照してください。

 しゃーないよね。

オプション

文字クラスの挙動は オプション で変更することができます。 d, a, u の3つのオプションがあります。

2.0.0以降では、文字クラスの挙動を変更するための d,a,u というオプションを (?on:pat)もしくは(?on)の形で指定することができます。(?on-off:pat), (?on-off) という形式を用いる場合はonの部分にのみ用いることができます(offはできません)。

 って書いてあったけどよくわからん。(?on:d)とか(?off:a)みたいに書けるってことでいいの? 動くコードで示してくれ。

op 説明
d デフォルト(1.9.3互換) (\w, \d, \s は、非ASCII文字にマッチせず、 \b, \B, POSIXブラケットは、各エンコーディングのルールに従う)
a ASCII (\w, \d, \s, POSIXブラケットは、非ASCII文字に マッチしない)
u Unicode (\w (\W), \d (\D), \s (\S), \b (\B), POSIXブラケット は、各エンコーディングのルールに従う)

 まあべつに知らなくても大丈夫そう、かな?

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux