Compiling and Iterating
Regular Expressions in Practice
Chapter 6 ยท Compiling and Iterating
So far each call has handed re a pattern string and let it do the work. This chapter covers two tools that make larger jobs tidier: compiled patterns, which let you name a pattern once and reuse it, and iteration, which lets you walk through matches one at a time and see exactly where each one sits. Together they make it possible to write a small scanner: a loop that reads text from left to right and labels each piece.
Compiling a pattern
re.compile(pattern, flags) returns a pattern object. It has the same operations as the module (search, match, fullmatch, findall, finditer, sub, split), but the pattern and flags are already fixed, so you do not pass them again.
Why bother?
- A name for the idea.
date_reororder_resays what the pattern is for, and one definition near the top of a file is easier to maintain than the same string pasted in three places. - Flags travel with it.
re.compile(r"...", re.I | re.X)is written once; later calls need noflags=. - Speed, a little. The module functions compile the pattern behind the scenes and keep a small internal cache, so calling
re.searchrepeatedly is not wildly slow. Compiling yourself skips that lookup and avoids any chance of recompiling when the cache is busy. Treat speed as a minor benefit and clarity as the main one.
A pattern object also reports facts about itself, and compiling an already-compiled pattern simply returns the same object.
Where to put the compile
Compile at module level (or once before a loop), not inside a function that is called many times. The compiled object is plain data and can be shared freely.
Match objects and positions
findall returns strings, which is convenient but throws away where each match was. A match object keeps that. m.start(), m.end() and m.span() give positions in the original string, and m.group(n) or m["name"] gives the text.
finditer
finditer does what findall does, but yields a match object for each non-overlapping match, lazily, one at a time.
Use finditer when you need positions (to highlight, to slice the original text, to report line and column), when you want several groups by name, or when the text is large and you would rather not build one big list. Use findall when the strings are all you need.
The iterator is single-use: once it has been walked, create a new one to go again. Empty matches are reported too, and the engine steps past them so the loop always ends:
Starting part-way: pos and endpos
A compiled pattern's search, match, fullmatch, findall and finditer accept optional pos and endpos arguments that restrict where the engine looks, without slicing the string. Positions in the results still refer to the whole string.
Two details are worth remembering. First, p.search(s, 3) starts looking at index 3, which is in the middle of 12, so it finds only '2'. Second, match(s, pos) insists that the match begin exactly at pos, which is the key to the scanner loop below. A caution: ^ does not match at pos unless pos is really the start of the string (or of a line, with re.M):
So when scanning with pos, leave ^ out and let match do the anchoring.
A scanner with named groups
A tokenizer splits text into labelled pieces: numbers, names, operators. With regex the recipe is one alternation of named groups, one group per kind of token. The match object's lastgroup attribute gives the name of the group that matched, so you never need to test which group is not None.
Several design points are packed into that example:
- Order matters. Alternatives are tried left to right, so put more specific kinds first. The final
BADgroup is a catch-all, so anything unrecognised surfaces as an error with a position instead of being silently skipped. - Full coverage. Because
SKIPandBADtogether cover every character, the matches tile the whole input with no gaps. Without a catch-all,finditerwould skip unrecognised characters without a word. - It is a generator.
yieldhands tokens out lazily, so a caller can stop early.
A manual loop with match(string, pos)
finditer is allowed to skip over text that nothing matches. When you need the opposite, a hard guarantee that every character is accounted for, move a position forward yourself and use match at that position. If it fails, you know exactly where the input went wrong.
Here WS can match an empty string, so WS.match(s, pos) always succeeds and simply returns the position after any spaces. The loop is the scanner: each pass consumes one piece, and pos only ever moves forward.
| Need | Reach for |
|---|---|
| Just the matched strings | findall |
| Positions, named groups, or lazy processing | finditer |
| Labelled pieces covering all the text | Named groups with a catch-all, plus lastgroup |
| A guarantee about every character, with exact error positions | A while loop using match(s, pos) |
| Same pattern used in several places | re.compile once, near the top |
m.lastgroup is the name of the last matched capturing group, and m.lastindex is its number. With a top-level alternation of groups, that is the group that matched. If the matching group has no name, lastgroup is None but lastindex still works. Nested groups can make "last" surprising, so keep scanner groups flat and use (?:...) for any grouping inside them, as NUM does above.
Challenges
Given "GET /a 200 12ms; POST /b 404 8ms; GET /c 200 130ms", compile one pattern with named groups method, path, status and ms. Use finditer to print each method, path and the position where its record starts, then print the total of the ms values.
Write a scanner for "go 12 steps, then 7 more!" with named groups INT, WORD, SP (spaces) and a catch-all X for anything else. Print the list of (kind, text) pairs without the spaces, and use collections.Counter on lastgroup to count how many tokens of each kind there were.
Split say 'hello world' to "big wide" world into words, keeping a quoted phrase together (single or double quotes) and removing the quote marks. Use one compiled pattern with an alternation and finditer; expected result: ['say', 'hello world', 'to', 'big wide', 'world']. Hint: a match has several groups, only one of which is not None.
Quick Reference
- re.compile(p, flags): returns a pattern object with the same methods as the module; compile once, reuse, and give it a meaningful name
- finditer: lazy iterator of match objects; use
start(),end(),span(),group(),groupdict() - findall vs finditer: strings only versus full match objects with positions
- pos / endpos: limit the search on a compiled pattern; results still index the whole string;
^ignorespos - match(s, pos): must match exactly at
pos; the basis of a manual scanner loop - Tokenizer recipe: flat named groups in one alternation, a catch-all last, and
m.lastgroupto label each piece