2.35. パターンの基礎

正規表現は、文字列を厳密な内容ではなく形式によって記述するための小さな言語です。記述はできるが列挙はできない パターン に従う場合に限って文字列が一致する、というときに使います。たとえば「数字の並びに続いて単位」、「ERROR で始まり数値で終わる行」、「これらの拡張子のいずれか、任意の順序で、任意の v 接頭辞付き」といったものです。

通常の文字列メソッドでは対応できないときに のみ re に手を伸ばしてください。

これらはいずれも、同等の正規表現よりも高速で、読みやすく、間違えにくいものです。正規表現は、文字列の 形式 が重要で正確な部分文字列が重要でないときに使ってください。

2.35.1. 使うことになる 4 つのもの

MicroPython の re モジュールは 4 つのものを提供します:

  • re.compile() -- パターン文字列を、再利用できるコンパイル済みパターンオブジェクトに変換します。

  • re.match() -- 文字列の 先頭 でパターンを試します。パターンは位置 0 にアンカーされます。

  • re.search() -- 文字列の 任意の位置 でパターンを試します。最初の一致を返します。

  • re.sub() -- すべての一致を見つけて置換します。

CPython と比べた顕著な省略点: モジュールレベルに re.findallre.finditerre.split はなく(その代わりコンパイル済みパターンには split メソッドがあります)、re.fullmatch もなく、re.IGNORECASE のようなフラグ定数もありません。CPython でこれらのいずれかに手を伸ばす場面では、ループの中で re.search() から同等の処理を組み立ててください。

2.35.2. 最初のパターン

パターン r'\d+' は 1 文字以上の数字に一致します:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

いくつか注目すべき点があります:

  • パターンは 生文字列r'...')として書かれており、\d のバックスラッシュが Python の文字列エスケープとして処理されるのではなく re に届くようになっています。正規表現のパターンには常に生文字列を使ってください。

  • re.search() は成功時に マッチオブジェクト を、失敗時に None を返します。match.group() を呼び出す前に必ず確認してください。

  • m.group(0) はパターンが一致したテキスト全体です。グループ 1、2、... は、パターンにキャプチャ用の括弧が含まれると後から現れます。

同じパターンを re.match() で使うと、文字列が数字で 始まっていない ため None を返します:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. パターンの構成要素

ほとんどの有用なパターンは、わずかな構成要素の集まりから組み立てられます。MicroPython で動作するものは次のとおりです:

リテラル文字 -- 特殊でない文字はそれ自身に一致します。hellohello に一致します。

特殊文字 -- . ^ $ * + ? { } [ ] \ | ( ) はすべて以下のような意味を持ちます。これらのいずれかにリテラルとして一致させるには、バックスラッシュでエスケープします。\. はリテラルのドットに一致します。

文字クラス -- 一般的な文字集合の省略表記:

  • \d -- 任意の数字 0-9

  • \D -- 任意の非数字

  • \s -- 任意の空白文字(スペース、タブ、改行)

  • \S -- 任意の非空白

  • \w -- 任意の「単語」文字: 英字、数字、アンダースコア

  • \W -- 任意の非単語文字

  • . -- 改行を除く任意の文字

量指定子 -- 直前の構成要素が一致しなければならない回数:

  • * -- 0 回以上(貪欲)

  • + -- 1 回以上(貪欲)

  • ? -- 0 回または 1 回

  • {n} -- ちょうど n

  • {m,n} -- m 回から n 回(両端を含む)

組み合わせ: \d{3}-\d{4} は 3 桁の数字、ダッシュ、4 桁の数字に一致します。\s+ は 1 文字以上の空白文字に一致します。hello.*worldhello、任意の内容(何もない場合も含む)、続いて world に一致します。

注釈

貪欲 とは、パターンの残りが依然として一致できる範囲で、量指定子が入力を可能な限り多く消費することを意味します。hello x world y world に対して hello.*world.* は、末尾に world を残したまま最長の連続部分に一致します。つまり短い x ではなく x world y をキャプチャします。+{m,n} の範囲形式についても同様で、エンジンは可能な限り最長の一致を取り、パターンの残りが失敗した場合にのみ後退します。

2.35.4. 置換

re.sub() はすべての一致を見つけて文字列に置換します。置換では \1\2、... を介してキャプチャされたグループを参照できます(残りのグループ構文とともに後ほど扱います)。グループがなければ、re.sub は正規表現に対する単純な検索・置換です:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

3 番目の引数は処理対象の文字列で、結果はすべての一致が置換された新しい文字列です。

2.35.5. 分割 -- コンパイル済みパターンに対してのみ

モジュールレベルに re.split はありません。正規表現で分割するには、まずパターンをコンパイルしてその split メソッドを呼び出します:

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

省略可能な 2 番目の引数は分割回数の上限を設定します:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. 再利用のためのコンパイル

同じパターンを何度も実行する場合(ループの中やホットな関数の中など)は、一度コンパイルしてコンパイル済みオブジェクトを再利用してください:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

コンパイル済みオブジェクトに対して pattern.match()pattern.search() を呼び出すのはモジュールレベルの関数と同じですが、呼び出しのたびに再コンパイルするコストを省きます。

2.35.7. 何にも一致しないパターン

特に次の 3 つのパターンは開発者を引っかけます:

  • .* は空文字列に一致します。re.search(r'.*', s).group(0) はどんな入力に対しても '' を返します。

  • エスケープされていない特殊文字を含むパターンは構文エラーです。re.compile(r'cost: $5') は、$ が「文字列の末尾」を意味するため ValueError を発生させます。r'cost: \$5' を使ってください。

  • ドット . は改行には 一致しません。改行をまたいで一致させるには、[\s\S] で明示的に改行を扱うようにパターンを書くか、1 行ずつ与えてください。

これらの構成要素があれば、パターンはほぼすべての 固定形式 のテキスト断片に一致できます。一致から構造化された データ を取り出すには、キャプチャグループが必要です。