Find multiline log blocks with DOTALL
Python’s re.DOTALL flag changes the meaning of . so it also matches newline characters. That is useful when each record has a clear opening and closing marker but contains an unknown number of intervening lines. In this example, the expression starts at BEGIN request=<digits> and uses a non-greedy .*? to stop at the nearest following END. re.finditer() returns both complete blocks in their original order.
The input deliberately includes multiple lines inside each block. The assertions check the number of matches, the captured request identifiers, and the exact extracted text. Printing normalized summaries instead of the raw blocks makes stdout compact and deterministic while still demonstrating that embedded newlines were retained.
DOTALL does not make a pattern understand nested structure. If an END line can appear as ordinary payload, this pattern will stop there. For logs with escaping, nesting, or malformed records, a line-oriented parser or a format-aware parser may be more appropriate. The non-greedy quantifier is also important here: a greedy .* would span from the first opening marker to the last closing marker.
re.DOTALL and finditer() are available in supported Python 3 releases; this example needs no newer API.
AI assistance disclosure: This article was drafted with AI assistance and checked against the cited Python documentation.
Sources: Python re flags and Python re.finditer.
import re
log = """BEGIN request=17
step=validate
status=ok
END
noise
BEGIN request=18
step=save
status=failed
END"""
pattern = re.compile(r"^BEGIN request=(\d+)$(.*?)^END$", re.DOTALL | re.MULTILINE)
matches = list(pattern.finditer(log))
assert len(matches) == 2
assert [match.group(1) for match in matches] == ["17", "18"]
assert matches[0].group(0) == "BEGIN request=17\nstep=validate\nstatus=ok\nEND"
for match in matches:
body_lines = len(match.group(2).strip().splitlines())
print(f"request={match.group(1)} body_lines={body_lines}")
request=17 body_lines=2
request=18 body_lines=2