2017/12/13

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 です。

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() の戻り値としては、対象のパターンが見つかった場合は正規表現の「マッチオブジェクト」というものが、見つからなかった場合は 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 です。

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() に直接渡す必要があります。

このコードを実行すると、次のような出力が返されるはずです。

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

マークダウンテキストの中に含まれる 2 つの順序無しリストのグループが無事抽出できることが確認できます。

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

  • re.MULTILINE: 指定されると、 ^ が「文字列全体の先頭」と「各行の行頭」を、 $ が「文字列全体の末尾」と「各行の末尾」を意味するようになる。指定されなかった場合のデフォルトのふるまいは、 ^ は「文字列全体の先頭」、 $ は「文字列全体の末尾」を意味する。
  • re.DOTALL: 指定されると、 . が改行を含むあらゆる文字を意味するようになる。指定されなかった場合のデフォルトのふるまいでは、 . は「改行以外のあらゆる文字」を意味する。

いずれも短く書けるバージョンが用意されており、 re.MULTILINEre.M と、 re.DOTALLre.S と書くこともできます(個人的には、 S は馴れていないとパッと読んだときに意味がわからないので、省略しない re.DOTALL の方で書く方が好みです)。

ちなみに、 re.MULTILINEre.DOTALL の他にも、 re.compile() で指定できるオプションフラグには次のようなものがあります。

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

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

Regular expression operations — Python 3 documentation

Python の正規表現で複数行マッチする方法についてでした。必要なときにパッと使えるようにしておくと便利です。

0 件のコメント: