Post

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 in QGIS — A Practical Reference

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_match returns 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; \\d in expressions, \d only in Python raw strings.
  • regexp_match returns 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_replace replaces all occurrences — anchor with ^ or $ to limit it.
  • Numeric fields need casting firstto_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.

This post is licensed under CC BY 4.0 by the author.