行頭の意味と、ドットの意味を分ける
BEGINとENDで囲まれた複数行の実験記録を抜き出したいとき、re.MULTILINEとre.DOTALLは別の問題を解決する。MULTILINEは^と$を各行の先頭・末尾にも一致させ、DOTALLはドットを改行にも一致させる。片方を付ければすべて複数行対応になるわけではない。両方の役割を分けると、パターンの意図が読みやすくなる。
以下では、開始行がBEGINと半角大文字ID、終了行がENDだけで構成され、記録が入れ子にならない形式を前提にする。マーカーには前後の空白がなく、本文には独立したEND行が現れない。改行は事前にLFへ統一する。任意の文書やHTMLをこの式一つで解析することは目的にしない。
最短一致で次の記録まで飲み込まない
example.py
import re
text = "header\r\nBEGIN A\r\nvalue=1\r\nnote=first\r\nEND\r\nBEGIN B\r\nvalue=2\r\nEND\r\n"
text = text.replace("\r\n", "\n").replace("\r", "\n")
pattern = re.compile(
r"^BEGIN (?P<id>[A-Z]+)\n(?P<body>.*?)^END$",
flags=re.MULTILINE | re.DOTALL,
)
records = [(m.group("id"), m.group("body")) for m in pattern.finditer(text)]
assert records == [("A", "value=1\nnote=first\n"), ("B", "value=2\n")]
assert re.search(r"^BEGIN", text) is None
assert re.search(r"^BEGIN", text, flags=re.MULTILINE) is not None
assert re.fullmatch(r"a.b", "a\nb") is None
assert re.fullmatch(r"a.b", "a\nb", flags=re.DOTALL) is not None
greedy = re.compile(r"^BEGIN [A-Z]+\n(.*)^END$", re.MULTILINE | re.DOTALL)
assert len(list(greedy.finditer(text))) == 1
assert "BEGIN B" in greedy.search(text).group(1)
for record_id, body in records:
print(record_id, body.splitlines())
print("greedy block count:", len(list(greedy.finditer(text))))
実行結果
A ['value=1', 'note=first']
B ['value=2']
greedy block count: 1
なぜ二つのフラグが必要なのか
開始マーカーは文字列全体の先頭ではなく、headerの次の行にある。MULTILINEがない^BEGINでは見つからない。一方、本文の.*?は複数行にまたがるので、DOTALLがなければ改行で止まり、その先のENDへ届かない。コード中の短いassertは、この二つの違いを個別に確認するためのものである。
.*は可能な限り長く一致しようとするため、最初のBEGINから最後のENDまでを一つの記録として取り込めてしまう。.*?へ変えると、後ろの条件である行頭のENDが成立する最初の位置まで進む。実行結果で記録がAとBに分かれ、貪欲版では1件になることを確認している。最短一致は「何でも正しく区切る」という意味ではなく、後続条件を満たす最短の範囲という意味である。
本文末尾の改行もbodyへ含まれる。splitlines()は表示のために行へ分けているだけで、records内では改行を含む元の本文を保持している。保存時に改行を落としたくない場合は、表示用の加工をそのままデータに適用しない。空行や空白に意味がある形式では、strip()でまとめて削る前に仕様を確認する。
欠けたマーカーと入れ子には別の設計が必要
この例は整った記録からの抽出であり、入力全体の構文検証ではない。ENDが欠けると後の記録のENDまで一致する可能性があり、BEGINのない本文は無視される。壊れたログを確実に検出したい場合は、行ごとに「記録の外」「記録の中」の状態を持ち、開始中に次のBEGINが来たらエラーにする方式が適している。
同じ理由で、入れ子のBEGIN/ENDをこの最短一致だけで扱うことはできない。本文中にENDという単独行を書ける仕様なら、エスケープや長さの情報など、区切りと本文を区別する規則が必要になる。マーカーを緩い部分一致にして回避すると、通常の文章まで終了行と誤認しやすい。
マーカーの大文字小文字や余白を許す変更にも注意する。例えば行末に[ \t]*を認める変更なら、余白付きの正常例と、本文中の似た語を拾わない拒否例を追加する。\s*では改行まで含めるため、意図せず次の行へ進む場合がある。行内の空白と行の境界を区別して書くのが大切である。
処理対象の大きさも確認する
例は短い文字列を丸ごと保持している。何GBもあるログを読み込んで同じ式を使うと、本文と結果の保持が負担になる。記録ごとに処理できる形式なら行を順に読み、完了したブロックをすぐ渡す方が扱いやすい。正規表現で表せるかだけでなく、入力が壊れた場合の挙動と必要なメモリを基準に方式を選ぶ。
実行環境と関連情報
掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。
関連:測定ログを列に分ける:名前付きグループで正規表現を読む / HTMLからリンク一覧を取り出す:HTMLParserの小さな使い方
- Python公式ドキュメント(2026年10月2日参照)
- Python公式ドキュメント(2026年10月2日参照)
