Regex in QGIS — A Practical Reference
Where regex works in QGIS, the four expression functions, and worked examples for cleaning, extracting, selecting, and labeling.
Regex (regular expressions) is a pattern language for matching and manipulating text. In QGIS it is the fastest way to clean attributes, validate IDs, extract values from messy fields, and build label/filter rules. This post covers where regex works in QGIS, the syntax, and worked examples per task.
No prior regex experience is assumed. Familiarity with the Field Calculator and Select by Expression is enough.
Where Regex Works in QGIS
| Location | Regex support |
|---|---|
| Field Calculator | regexp_match, regexp_replace, regexp_substr, ~ |
| Select by Expression | Same functions |
| Rule-based symbology / labeling | Same functions |
| Layer filter (Query Builder) | ~ works for most local formats (provider-dependent) |
| Attribute table quick search | No regex — use Select by Expression |
| Python console / PyQGIS | Python re module |
In QGIS expressions, every backslash must be doubled: write
\\d, not\d. Single backslashes fail silently or raise an error. Python raw strings (r'\d') keep single backslashes.
Syntax Reference
Character classes
| Pattern | Matches | Example match |
|---|---|---|
. |
Any one character | a, 7, - |
\\d |
One digit | 5 |
\\D |
One non-digit | x |
\\w |
Letter, digit, underscore | b, 9, _ |
\\s |
Whitespace | space, tab |
[abc] |
a, b, or c | b |
[^0-9] |
Anything except a digit | k |
[A-Za-z] |
Any letter | Q |
Quantifiers
| Pattern | Meaning | \\d + quantifier matches |
|---|---|---|
* |
0 or more | "", 4, 4567 |
+ |
1 or more | 4, 4567 |
? |
0 or 1 | "", 4 |
{3} |
Exactly 3 | 123 |
{2,4} |
2 to 4 | 12, 1234 |
*? +? |
Lazy — shortest possible match | See Pitfalls |
Anchors, groups, alternation
| Pattern | Meaning |
|---|---|
^ |
Start of string |
$ |
End of string |
\\b |
Word boundary |
(...) |
Capture group — referenced as \\1, \\2 in replacements |
(?:...) |
Group without capturing |
a\|b |
a OR b |
(?i) |
Case-insensitive from this point |
The Four QGIS Tools
~ — quick match test
Returns true/false.
1
"ref" ~ '^A\\d+'
True for A12, A4. False for N12, 12A.
regexp_match(string, pattern) — position of match
Returns an integer: position of the first match, or 0 if none.
1
regexp_match("name", 'berg') > 0
regexp_matchreturns a position, not a boolean. Always compare with> 0.
regexp_substr(string, pattern) — extract text
Returns the matched text; if the pattern contains a capture group, returns the group instead.
1
2
3
regexp_substr('Kerkstraat 12b', '\\d+') -- → '12'
regexp_substr('Kerkstraat 12b', '\\d+[a-z]?') -- → '12b'
regexp_substr('N210; A16', '[AN]\\d+') -- → 'N210' (first match only)
regexp_replace(string, pattern, replacement) — substitute
Replaces all matches.
1
2
regexp_replace('a b c', '\\s+', ' ') -- → 'a b c'
regexp_replace('12/07/2026', '(\\d+)/(\\d+)/(\\d+)', '\\3-\\2-\\1') -- → '2026-07-12'
Worked Examples by Task
Cleaning attributes (Field Calculator)
Collapse whitespace and trim:
1
regexp_replace(trim("name"), '\\s{2,}', ' ')
' De Meent ' → 'De Meent'
Keep digits only:
1
regexp_replace("phone", '\\D', '')
'+31 (0)10-123 45 67' → '310101234567'
Normalize street suffixes:
1
regexp_replace("street", '(?i)\\s*str(\\.|aat)?$', 'straat')
'Hoofdstr.' → 'Hoofdstraat'
Strip HTML tags from imported data:
1
regexp_replace("descr", '<[^>]+>', '')
Remove parenthetical notes:
1
regexp_replace("name", '\\s*\\([^)]*\\)', '')
'Rotterdam (Zuid)' → 'Rotterdam'
Extracting values
Dutch postcode from a full address:
1
regexp_substr("address", '\\d{4}\\s?[A-Z]{2}')
'Coolsingel 40, 3011 AD Rotterdam' → '3011 AD'
Year from mixed text:
1
regexp_substr("source", '(19|20)\\d{2}')
Numeric value out of a unit string, cast to number:
1
to_real(regexp_substr("depth_txt", '\\d+\\.?\\d*'))
'12.5 m' → 12.5
Selecting features (Select by Expression)
Find malformed parcel IDs:
1
NOT ("parcel_id" ~ '^[A-Z]{2}-\\d{4}-\\d{2}$')
Motorways or national roads:
1
"ref" ~ '^(A|N)\\d+$'
NULL, empty, or whitespace-only fields:
1
regexp_match(coalesce("notes", ''), '^\\s*$') > 0
Exact word, not substring — Park but not Parkweg:
1
"name" ~ '\\bPark\\b'
Labeling and symbology (rule-based)
Rule filter — label only A-roads:
1
"ref" ~ '^A\\d+'
Label expression — drop the prefix:
1
regexp_replace("ref", '^[AN]', '')
Shorten names for small scales:
1
regexp_replace("name", '(?i)^(Gemeente|Municipality of)\\s+', '')
Validation before analysis
Coordinates stored as text — check format before casting:
1
"lat_txt" ~ '^-?\\d{1,2}\\.\\d+$'
Fix comma decimal separators, then cast:
1
to_real(regexp_replace("lat_txt", ',', '.'))
Flag non-ISO dates:
1
NOT ("date_txt" ~ '^\\d{4}-\\d{2}-\\d{2}$')
Python Console / PyQGIS
Standard re module; raw strings keep single backslashes.
1
2
3
4
5
6
7
8
9
import re
layer = iface.activeLayer()
pattern = re.compile(r'^\d{4}\s?[A-Z]{2}$')
for f in layer.getFeatures():
value = f['postcode'] or ''
if not pattern.match(value):
print(f.id(), repr(value))
1
2
3
4
5
6
7
8
import re
layer.startEditing()
idx = layer.fields().indexOf('name')
for f in layer.getFeatures():
cleaned = re.sub(r'\s+', ' ', (f['name'] or '').strip())
layer.changeAttributeValue(f.id(), idx, cleaned)
layer.commitChanges()
For large layers, prefer the Field Calculator or
processing.run("native:fieldcalculator", ...)over per-feature loops.
Quick Pattern Cookbook
| Goal | Pattern |
|---|---|
| Whole string is digits | ^\\d+$ |
| Integer or decimal | ^-?\\d+(\\.\\d+)?$ |
| Dutch postcode | ^\\d{4}\\s?[A-Z]{2}$ |
| ISO date | ^\\d{4}-\\d{2}-\\d{2}$ |
| Email (rough) | ^\\S+@\\S+\\.\\S+$ |
| URL (rough) | ^https?:// |
| EPSG code in text | EPSG:?\\s?\\d{4,5} |
| Contains no letters | ^[^A-Za-z]*$ |
| Trailing whitespace | \\s$ |
Pitfalls
- Single backslashes in expressions fail;
\\din expressions,\donly in Python raw strings. regexp_matchreturns an integer — always write> 0.- NULL breaks matching — wrap fields with
coalesce("field", ''). - Case sensitivity is the default — prefix with
(?i)when needed. - Greedy matching over-captures — on
'<a><b>', pattern'<.*>'matches the entire string;'<.*?>'matches'<a>'. regexp_replacereplaces all occurrences — anchor with^or$to limit it.- Numeric fields need casting first —
to_string("code") ~ '^\\d{3}$'. - Escape special characters to match them literally:
\\.\\(\\)\\[\\+\\?\\*\\$\\^.
Test patterns in the Expression Builder preview against a sample feature before running on the full layer. Prototype complex patterns at regex101.com (PCRE flavor), then double the backslashes when pasting into QGIS.