Regular expressions you can see
·
Course overview
1
The missing payment: finding text as it is
2
The hit-and-run car: character classes
3
The torn phone book: quantifiers
4
The fake sign-ups: anchors and validation
5
Dates all over the place: groups and alternation
6
The 3 a.m. login: named groups and Match objects
7
The night before the story: substitution and splitting
8
The Backtracker's challenge: greedy, lazy and possessive
9
The safe's rules: lookaround
10
Same modus operandi: backreferences and conditionals
11
The unreadable regex: flags and compiling
12
Unmasking the Backtracker: review and a tokenizer
CASE-002
Grep & Co. Data Detective Agency
Case file
Client
Detective Kang, local precinct
Evidence
One witness statement and the entry/exit log of an underground car park. The witness saw only part of the number plate.
To close the case
Pull every number plate out of the car park log, and narrow it down to the one car that fits the witness statement.
Toward evening, Detective Kang pushes through the agency door, shaking off a wet umbrella. "Last night, around 22:45, hit-and-run at the exit of an underground car park. Witness didn't catch the whole plate." The director doesn't lift her hands from the keyboard. "Then write down what the witness did catch, as a pattern. Evidence leaves a pattern."
Korean number plates look like 12가3457: two digits, one Hangul syllable, four digits. Here is the witness statement, exactly as it was taken down at the precinct.
- 앞 두 자리는 12 아니면 17이었어요.
- 가운데 한글은 가, 거, 고 중 하나였어요.
- 마지막 숫자는 7이었어요.
- 차는 출구(OUT) 쪽으로 빠져나갔어요.In English:
And here is the car park's entry/exit log. The camera sometimes misreads a plate.
2026-10-19 22:41 IN B1 12가3457
2026-10-19 22:43 IN B1 34나1207
2026-10-19 22:47 OUT B2 17거3407
2026-10-19 22:52 IN B1 12-3457
2026-10-19 22:58 OUT B2 56다9081
2026-10-19 23:05 IN B1 12a3457Every line of the statement is a condition of the form "this position holds one of these". The character classes you learn today are exactly the syntax for writing that down.
Square brackets [...] mean any one of the characters written inside. The witness said "one of 가, 거 or 고", so that position is written [가거고].
import re
re.findall("12[가거고]3457", "12가3457 12거3457 12나3457") # ['12가3457', '12거3457']
re.findall(r"[ab]c", "ac bc cc") # ['ac', 'bc']
re.search(r"[abc]", "xyz") # None[가거고] is not three characters but one.[가거고] and [고거가] are the same class.. was "any one character", the brackets are "one of the characters I picked".Evidence lab
re.search(r"", text)
Return value
Writing out all ten digits as [0123456789] is tedious. Inside brackets, a - between two characters makes a range. [0-9] is 0 through 9, and [a-z] is every lowercase English letter.
re.findall(r"[0-9]", "B1") # ['1']
re.findall(r"[a-z0-9]", "B1 b2") # ['1', 'b', '2']
re.findall(r"[a-zA-Z]", "a1Z") # ['a', 'Z'][a-z0-9] is one lowercase letter or digit.[a-z] doesn't include capital letters.In Unicode, the 11,172 precomposed Hangul syllables sit in one unbroken run, from 가 (U+AC00) to 힣 (U+D7A3). So a single [가-힣] catches any complete Hangul syllable.
print(hex(ord("가")), hex(ord("힣"))) # 0xac00 0xd7a3
re.findall(r"[가-힣]", "ㄱ ㅏ 가") # ['가']Standalone consonants and vowels (jamo) such as ㄱ and ㅏ live in a different block, so [가-힣] doesn't include them. The middle character of a number plate is always a complete syllable, so this range is all you need.
Write a range backwards and you get an error the moment the pattern is built.
re.compile("[a-Z]") # re.PatternError: bad character range a-Z at position 1Evidence lab
re.search(r"", text)
Return value
Dot's tip
A range only goes from the smaller code point to the bigger one. If you try to catch both cases at once with [a-Z], you get an error, because a is number 97 and Z is number 90, so it's backwards. Want upper and lower case? Use two ranges, like [a-zA-Z].
Put a ^ right at the front, just inside the bracket, and the meaning flips. [^0-9] is one character that is not a digit.
re.findall(r"[^0-9]", "12가34") # ['가']
re.findall(r"[0-9^]", "2^3") # ['2', '^', '3']
re.findall(r"[^0-9]", "1\n2") # ['\n']^ means negation only when it comes first. Further back, as in [0-9^], it's just the ^ character.[^0-9] still takes up one character. It's not "a spot with no digit" but "one character that isn't a digit".[^0-9] catches it.The car park camera sometimes misreads digits as letters: 1 as I, 5 as S, 2 as Z, and so on. Look for characters that are neither digits, Hangul nor spaces, and the misread spots give themselves away.
Dot's tip
I can't get past the window (a newline), but [^0-9] can. A newline counts as 'a character that isn't a digit'. Whenever you use [^...] on a multi-line log, check whether it's grabbing newlines too.
Evidence lab
re.search(r"", text)
Return value
Common classes have a short form: a backslash and one letter. The uppercase version means the opposite.
| Shorthand | Meaning | Roughly the same class |
|---|---|---|
\d | one digit | [0-9] |
\D | one character that isn't a digit | [^0-9] |
\w | one letter, digit or underscore (Hangul included) | wider than [a-zA-Z0-9_] |
\W | one character that isn't \w | |
\s | one whitespace character (space, tab, newline, etc.) | wider than [ \t\n\r\f\v] |
\S | one character that isn't whitespace |
re.findall(r"\d", "B1 12가") # ['1', '1', '2']
re.findall(r"\D", "12가-3") # ['가', '-']
re.findall(r"\w", "a_1 가!") # ['a', '_', '1', '가']
re.findall(r"\W", "a_1 가!") # [' ', '!']
re.findall(r"\s", "a b\tc\nd") # [' ', '\t', '\n']
re.findall(r"\S", "a b\tc") # ['a', 'b', 'c']You can mix shorthands with other characters inside brackets. [\d,.] is one character that is a digit, a comma or a period.
re.findall(r"\d[\d,.]\d", "1,2 3.4 5-6 789") # ['1,2', '3.4', '789']Now let's write the number plate with shorthands. Writing the middle as \D (not a digit) looks convenient, but then the camera's misreads 12-3457 and 12a3457 pass too. The middle shouldn't be "any non-digit"; it has to be "one Hangul syllable".
Evidence lab
re.search(r"", text)
Return value
Inside brackets, most metacharacters lose their power. [.*+?] is just one character out of period, asterisk, plus and question mark. No backslashes needed.
Only four characters stay special inside brackets.
\ : escapes, and shorthands like \d] : the end of the class. But at the very front, it's a literal character^ : negation, when it's at the very front- : a range. But at the very front or very end, it's a literal characterre.findall(r"[.*+?]", "a.b*c+d?") # ['.', '*', '+', '?']
re.findall(r"[]a]", "a]b") # ['a', ']']
re.findall(r"[-+]", "3-2+1") # ['-', '+']
re.findall(r"[a\-z]", "a-mz") # ['a', '-', 'z']
re.findall(r"[\]\\]", r"a]b\c") # [']', '\\'][a\-z] isn't a range; it's the three characters a, - and z. That's why m isn't caught. If the position rules get confusing, just adding a backslash is easier to read.
Python str patterns are Unicode by default, so the shorthands are wider than you might think. You've already seen \w catch Hangul. \d also counts the Arabic-Indic digit ٣ (U+0663, the digit 3) as a digit. \s catches the full-width space (U+3000) that often sneaks into Korean, Japanese and Chinese documents.
re.findall(r"\d", "3٣") # ['3', '٣']
print(int("٣")) # 3
re.findall(r"\s", "a\u3000b") # ['\u3000'][0-9] instead of \d.re.ASCII flag, which narrows the shorthands to the ASCII range, comes in Lesson 11.Finally: doubling a symbol inside brackets, as in [[, --, &&, ~~ or ||, raises a FutureWarning. Python is keeping those spots free for set operations between classes (union, intersection and so on) in the future.
import warnings
warnings.simplefilter("always")
re.compile(r"[[a]") # FutureWarning: Possible nested set at position 1
re.compile(r"[a&&b]") # FutureWarning: Possible set intersection at position 2They work today, but their meaning may change later. If you really need those characters, escape one of them, as in [\[a] or [a&\&b]. We'll go over this again in Lesson 12.
Say the car park log is stored in a string called parking. First, pull out every real plate. The two lines the camera misread drop out.
plates = re.findall(r"\d\d[가-힣]\d\d\d\d", parking)
print(plates) # ['12가3457', '34나1207', '17거3407', '56다9081']Now turn the witness statement into classes, one position at a time. The first two digits are 1[27], the middle is [가거고], and the last is 7.
print(re.findall(r"1[27][가거고]\d\d\d7", parking)) # ['12가3457', '17거3407']
print(re.findall(r"OUT B\d 1[27][가거고]\d\d\d7", parking)) # ['OUT B2 17거3407']There are two candidates, but 12가3457 came in before the accident and has no exit record. The only car that left through the exit, two minutes after the accident, is 17거3407.
Case closed report — 17거3407, which left through exit B2 at 22:47, matches every point of the witness statement. Asked Detective Kang to run a vehicle check.
Once Detective Kang has gone, the director taps the pattern on the screen. "\d\d[가-힣]\d\d\d\d. You wrote the same thing four times. And these days some plates have three digits up front." In the next lesson you'll learn how to shorten this long pattern.
B1 and B2, in the car park log. How do B[12] and B\d differ?[^\d\s] find? Make a guess on the camera log above, 12가3457 I2가3457 12가34S7 1Z거3407, then run it.22:41.The director's notebook
[abc] is one character: a, b or c. However many you put in, it takes one position.
Ranges like [a-z0-9] follow code point order. Complete Hangul syllables are [가-힣]. [a-Z] is an error.
[^...] is negation only when ^ comes first. It catches newlines too.
\d \w \s and their opposites \D \W \S. Mix them inside a class, as in [\d,.].
Inside a class, . * + ? are literal characters. Only \ ] ^ - are special.
In str patterns, \d \w \s cover all of Unicode. For just 0-9, use [0-9].
← Previous
Lesson 1 · The missing payment: finding text as it is
Next →
Lesson 3 · The torn phone book: quantifiers
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.