メインコンテンツへスキップ
  1. ノート/
  2. 正規表現/

正規表現の補足:先読み、後読み、後方参照、再帰

·1377 文字·3 分· loading · loading · · ·
ICE345
著者
ICE345
CS Student | System | Linux | OCaml
Python標準ライブラリのreは先読み、後読み、後方参照をサポートします。一方、バランスグループや再帰的な括弧マッチングには、サードパーティーのregexモジュールが必要です。

1. 先読み(Lookahead)
#

先読みは、現在位置の後ろに条件を満たす文字列があるかを確認します。確認した文字列そのものはマッチ結果に含まれません。

1.1 肯定先読み
#

(?=pattern)

後ろに数字が続く単語だけを取り出す例です。

import re

text = "apple 123, banana456, cherry 789."
pattern = r'\b\w+(?=\d)'
matches = re.findall(pattern, text)
print(matches)  # ['banana']

1.2 否定先読み
#

後ろに特定のパターンが続かない位置を確認します。

(?!pattern)
import re

text = "apple 123, banana456, cherry 789."
pattern = r'\b\w+(?!\d)'
matches = re.findall(pattern, text)
print(matches)

否定先読みは、特定の拡張子を除外する、予約語の後ろを除外する、といった条件に使えます。ただし、(?!\d)は「単語全体の後ろに数字がない」という意味ではなく、その位置の直後に数字がないという意味です。

2. 後読み(Lookbehind)
#

後読みは、現在位置の前に条件を満たす文字列があるかを確認します。

2.1 肯定後読み
#

(?<=pattern)

@の直後にあるドメイン名を取り出す例です。

import re

text = "Contact us at support@example.com or sales@company.com."
pattern = r'(?<=@)\w+'
matches = re.findall(pattern, text)
print(matches)  # ['example', 'company']

2.2 否定後読み
#

(?<!pattern)

直前が@ではない単語を取り出します。

import re

text = "Contact us at support@example.com or sales@company.com."
pattern = r'(?<!@)\b\w+'
matches = re.findall(pattern, text)
print(matches)

Pythonの標準reでは、後読みの長さを固定する必要があります。可変長の後読みが必要な場合は、regexモジュールの仕様を確認してください。

3. 後方参照(Backreference)
#

後方参照は、前にあるキャプチャグループの内容を、後のパターンで再利用します。\1\2などで参照します。

import re

text = "I saw a dog and a cat, but I did not see the dog dog."
pattern = r'\b(\w+)\b\s+\1\b'
matches = re.findall(pattern, text)
print(matches)  # ['dog']

この例では、同じ単語が2回連続する箇所を検出しています。

4. 再帰的な括弧マッチング
#

括弧のような入れ子構造は、単純なreだけでは扱いにくい問題です。Python標準のreは、正規表現自身を再帰的に呼び出す(?R)をサポートしていません。

regexモジュールを使うと、次のようなパターンを記述できます。

python -m pip install regex
import regex

text = "(a(b(c)d)e)f(g(h)i)"
pattern = r'\((?>[^()]+|(?R))*\)'
matches = regex.findall(pattern, text)
print(matches)
  • (?R):正規表現全体を再帰的に呼び出す。
  • (?>...):原子的グループ。いったん確定した部分のバックトラッキングを抑える。
  • [^()]:括弧以外の文字。

5. 複数の機能を組み合わせる
#

括弧の中にskipが含まれない部分を探す、という単純な例です。

import re

text = "Match (this), (skip this), and (another one)."
pattern = r'\((?![^)]*skip)[^)]*\)'
matches = re.findall(pattern, text)
print(matches)  # ['(this)', '(another one)']

このような否定先読みは、特定のキーワードを含む候補を除外する場合に便利です。ただし、入れ子になった括弧を扱う場合は、このパターンでは不十分であり、再帰的なパターンやパーサーを使う必要があります。

6. 括弧の対応を検証する
#

regexモジュールで、文字列全体が正しく入れ子になった括弧で構成されているかを検査できます。

import regex

def is_valid_parentheses(value: str) -> bool:
    pattern = r'^\((?>[^()]+|(?R))*\)$'
    return bool(regex.fullmatch(pattern, value))

print(is_valid_parentheses("(a(b)c)"))  # True
print(is_valid_parentheses("(a(b)c"))   # False

まとめ
#

  • 先読み:後ろの条件を確認するが、確認部分を消費しない。
  • 後読み:前の条件を確認するが、確認部分を消費しない。
  • 後方参照:前に捕捉した文字列を後のパターンで再利用する。
  • 再帰的マッチング:入れ子構造を扱う。Pythonではregexモジュールが必要になることがある。

正規表現を複雑にしすぎると、可読性や性能が低下します。単純な文字列処理やパーサーで解決できる場合は、無理に1本の正規表現へまとめない方が保守しやすくなります。

関連記事


评论