Python Tips: 正規表現で複数行を扱いたい

Python で正規表現を利用するときに複数行マッチを行う方法についてご紹介します。

Python で正規表現といえば、標準ライブラリの re です。

import re

上の記事では re の下にある関数を使った正規表現の利用方法をご紹介しましたが、今回は re.compile() で取得できる正規表現オブジェクトを使った方法を用いてご説明していきます。

まずはおさらいとして通常の(デフォルトの)単一行にマッチさせる方法から見ていきます。

単一行マッチ

単一行のマッチの場合は、 re.compile() の第 1 引数に正規表現パターンを渡してそのまま search() メソッドなどを実行すれば OK です。

次のサンプルではマークダウンテキストの中から見出しを抽出してリストを生成しています。

# coding: utf-8

import re
from io import StringIO

MARKDOWN_TEXT = StringIO('''
# Python Tips: 正規表現で複数行を扱いたい

Python で正規表現を利用するときに複数行マッチを行う方法についてご紹介します。

Python で正規表現といえば、標準ライブラリの `` re `` です。

```python
import re
```

- [ライブラリ: re](/2014/10/re-regular-expression.html)

上の記事では `` re `` 直下にある関数を使った方法をご紹介しましたが、今回は `` re.compile() `` で利用できる「正規表現オブジェクト」を使った方法でご説明していきます。  まず、通常の(デフォルトの)単一行にマッチさせる方法から見ていきましょう。

まずはおさらいとして通常の(デフォルトの)単一行にマッチさせる方法から見ていきます。
''')


def main():
    # 見出しを抽出する
    headings = extract_markdown_headings(MARKDOWN_TEXT)
    for level, text in headings:
        _print_list_item(level, text)


def extract_markdown_headings(file):
    '''Markdown の見出しを抽出する'''
    headings = []
    pattern = re.compile(r'^(#+) (.+)$')
    for line in file:
        match = pattern.search(line)
        # (見出しレベル, 見出しのテキスト) の形で返す
        if match:
            headings.append((len(match.group(1)), match.group(2)))

    return headings


def _print_list_item(level, text):
    '''リストアイテムを出力する'''
    print('{}- {}'.format(' ' * (level - 1) * 2, text))


if __name__ == '__main__':
    main()

少し長めですが、ここでのポイントは関数 extract_markdown_headings() だけです。

def extract_markdown_headings(file):
    '''Markdown の見出しを抽出する'''
    headings = []
    pattern = re.compile(r'^(#+) (.+)$')
    for line in file:
        match = pattern.search(line)
        # (見出しレベル, 見出しのテキスト) の形で返す
        if match:
            headings.append((len(match.group(1)), match.group(2)))

    return headings

re.compile() で「正規表現オブジェクト」を生成して、 search() メソッドで各行に対して対象のパターンの有無をチェックしています。

出力は次のとおりとなります。

- Python Tips:正規表現で複数行を扱いたい
  - 単一行マッチ
  - 複数行マッチ

search() の戻り値は、対象のパターンが見つかった場合は re のマッチオブジェクト、見つからなかった場合は None です。

続いて、複数行マッチの方を見ていきます。

複数行マッチ

複数行マッチの場合は、 re.compile() の第 1 引数に対象パターンを指定する点は単一行マッチと同じですが、第 2 引数にオプションフラグを指定する形になります。

例えば、マークダウンテキストの中から順序無しリストのグループを全件抽出したい場合は次のようにオプション re.MULTILINEre.DOTALL を指定します。

import re

pattern = re.compile(r'- .+?\n$', re.MULTILINE | re.DOTALL)
lists = []
for match in pattern.finditer(text):
    lists.append(match.group(0))

finditer() はパターンにマッチした部分を格納するマッチオブジェクトを返すイテレータオブジェクトです。戻り値には None などは含まれないので、ループ内で if 文で場合分けしたりする必要はありません。

上のコード片を含む、もう少し長いサンプルは次のとおりです。

# coding: utf-8

import re
from io import StringIO

MARKDOWN_TEXT = StringIO('''
# Python Tips:正規表現で複数行を扱いたい

Python で正規表現を利用するときに複数行マッチを行う方法についてご紹介します。

- これは
    - いわゆる
    - リスト
    - です

Python で正規表現といえば、標準ライブラリの `` re `` です。

```python
import re
```

- これも
    - ある種の
    - リストです
''')


def main():
    # 順序無しリストを抽出する
    lists = extract_unordered_lists(MARKDOWN_TEXT)
    print(lists)


def extract_unordered_lists(file):
    pattern = re.compile(r'- .+?\n$', re.MULTILINE | re.DOTALL)
    lists = []
    for match in pattern.finditer(file.read()):
        lists.append(match.group(0))
    return lists


if __name__ == '__main__':
    main()

当然ですが、単一行マッチの場合のように文字列を事前に行単位で分割してしまうと複数行のパターンが抽出できないので、対象の文字列全体を finditer() に直接渡す必要があります。

このコードを実行すると次のような出力が返されます。 マークダウンテキストの中に含まれる 2 つの順序無しリストが抽出できることが確認できます。

['- これは\n    - いわゆる\n    - リスト\n    - です\n', '- これも\n    - ある種の\n    - リストです\n']

ここで利用したフラグ re.MULTILINEre.DOTALL の意味合いはそれぞれ次のとおりです。

  • re.MULTILINE: 指定された場合、正規表現中の ^ が「文字列全体の先頭」または「各行の行頭」、 $ が「文字列全体の末尾」または「各行の末尾」を意味する。指定されなかった場合、 ^ は「文字列全体の先頭」、 $ は「文字列全体の末尾」を意味する。
  • re.DOTALL: 指定された場合、正規表現中の . が改行を含むあらゆる文字を意味する。指定されなかった場合、 . は「改行以外のあらゆる文字」を意味する。

いずれも短縮版が用意されており、 re.MULTILINEre.M と、 re.DOTALLre.S とも書けます。

ちなみに、 re.MULTILINEre.DOTALL の他にも、 re.compile() で使えるフラグには次のようなものがあります。

  • re.ASCII (re.A)
  • re.DEBUG
  • re.IGNORECASE (re.I)
  • re.LOCALE (re.L)
  • re.VERBOSE (re.X)

名前からイメージが湧くものも多いですが、実際に使うときは公式のドキュメントを参照してください。

Python の正規表現で複数行マッチする方法についてでした。