Regular Expressions in R
Back to our workshop. Five of the people who signed up in Lesson 1 also pasted a one-line confirmation when they registered, and each line is a jumble: a name, an email, the amount they paid, the date, and a ticket code, all crammed into a single string like "Aarti Nair [email protected] paid 1500 on 2026-03-08 ticket A12".
In Lesson 1 you handed the stringr verbs a plain word like "Mumbai" and it matched that exact text. That only finds things you can spell out in advance. To pull the email, the date, or the ticket out of a line like the one above, you need to describe their shape, not their exact text. That description is a regular expression.
By the end of this lesson you will be able to:
- Build a pattern from the ground up with character classes, quantifiers and anchors
- See exactly which characters a pattern matches, live
- Find, extract and replace text with that pattern, in base R and with stringr
Prerequisites: you can run R and store a result with <- (Atomic Vectors and Data Types), and you have met the stringr verbs str_detect, str_extract and str_replace (Strings with stringr). This lesson turns their "pattern" argument into a real tool.
The widget below is the payoff. Tap each pattern and watch it light up the exact characters it catches. By the end you will write every one of these yourself.
A pattern matches a shape, not exact text
A plain word like "Mumbai" is the simplest possible pattern: it matches itself, those six letters in that order. A regular expression (regex) goes further. It is a tiny language for describing a shape of text, so one pattern can match many different strings that share a form: any digit, any email, any date.
The first building block is the character class, written in square brackets: [...] means "match any one character from this set." The dash inside makes a range.
A few characters are not taken literally inside a regex; they are metacharacters with a special job. The square brackets are the first you have met. Here is our sign-up data, and a class that finds a digit:
It found a single digit in each line: the first 1 of 1500, and the 2 of 2000. Ranges and negation give you the rest:
\d is any digit (the same as [0-9]), \w is a "word" character (letter, digit or underscore), and \s is any whitespace. One catch: in R you must double the backslash, writing "\\d", not "\d", because R strings treat a lone backslash specially. We will return to that in the gotchas at the end; for now, [0-9] and \\d mean the same thing.What does [0-9] match?
You write the pattern [0-9] (no quantifier after it). Run against the text "room 1500", what does a single match find?
[...] matches exactly ONE character from the set, and 0-9 is the range of all ten digits. It stops after the first one.[0-9]+, which is the next step.Quantifiers: how many to match
A class matches one character. A quantifier sits right after an item and says how many of it to match:
+one or more*zero or more?optional (zero or one){n}exactly n{n,m}between n and m
[0-9]+ is greedy: it takes as many digits as it can, so it returns the whole amount 1500, not just the first 1. The ? quantifier is handy for optional letters, so one pattern can absorb two spellings:
"colou?r" matched color (no u) and colour (with u), and returned NA for colossal, which has no match at all. Tap the patterns below and watch the same string yield fewer, longer matches as you tighten the quantifier.
Grab the whole amount
Each line has an amount like 1500 right after the word paid. The blank below should pull the whole run of digits, not just the first one, so you get "1500", "1800", and so on. Reach for a digit class plus the right quantifier.
[0-9]+ (or \d+) means one or more digits, so it grabs the whole run like 1500, not just the first digit.Add a quantifier after the digit class: [0-9]+ matches one or more digits in a row, the whole amount.Show answer
str_extract(signup, "[0-9]+")
#> [1] "1500" "1800" "1500" "2000" "1500"Anchors, and building a real pattern
Classes and quantifiers say what and how many. Anchors say where, and they match a position rather than a character:
^the start of the string$the end of the string\ba word boundary (the edge between a word character and a non-word character)
Now combine all three ideas into the patterns from the cover. Each is just classes, quantifiers and (where useful) anchors stacked together:
Find, extract and replace in R
A pattern is only useful once you DO something with it. The three jobs are the same ones from Lesson 1, except the second argument is now a real pattern. Each has a base R verb and a stringr verb that do the same thing:
The base R verbs need no package at all and read almost the same:
First match, or every match?
You run str_extract(x, "[0-9]+") on the single line "room 12, seat 9". What comes back?
str_extract (like base regexpr) returns only the FIRST match in each string. For every match you need str_extract_all (or gregexpr).str_detect, which answers "is it there?". str_extract returns the matched text itself, here the first match.Redact every email
Privacy pass: replace every email in signup with the word [hidden], leaving the rest of each line untouched. Fill in a pattern that matches an email: some letters, dots or an underscore, then an @, then a domain.
@, then a class for the domain, for example [a-z._]+@[a-z.]+.Your pattern needs an @, the one character every email has. Try [a-z._]+@[a-z.]+.Show answer
str_replace_all(signup, "[a-z._]+@[a-z.]+", "[hidden]")
#> [1] "Aarti Nair [hidden] paid 1500 on 2026-03-08 ticket A12" ...Three traps, and where regex stops
Regex is powerful, but a few things bite everyone at least once. Here they are, with fixes:
xml2, jsonlite, rvest). Regex is for shapes inside flat text; that is where it shines.Keep these in mind:
.means "any character"; to match a literal dot, escape it as\\..+and*are greedy; add?after them (+?,*?) to make them lazy.- Always double the backslash in R strings:
\\d,\\.,\\b.
References
Four trustworthy places to take this further, all free:
- R for Data Science (2e): Regular expressions - the canonical, example-led chapter that builds regex from scratch.
- stringr: Regular expressions (vignette) - the exact syntax the stringr verbs understand, with a clear cheat-table.
- Base R regular expression reference (?regex) - the authoritative help page behind
grepl,gsub,regmatchesand friends. - Posit strings cheatsheet - a one-page visual map of regex and stringr, worth keeping open while you work.
Lesson 2 complete
You can now describe a shape of text instead of spelling it out. You built patterns from character classes ([0-9], [A-Z], [^...]), quantifiers (+, ?, {n}), and anchors (^, $, \b), assembled them into real email, date and ticket patterns, and used them to detect (grepl, str_detect), extract (regmatches, str_extract_all), and replace (gsub, str_replace_all). You also met the traps: greedy matching, the doubled backslash, and case.
One of those patterns you wrote was a date: [0-9]{4}-[0-9]{2}-[0-9]{2}. That matches the text of a date, but it does not understand it: it cannot tell you the weekday, add seven days, or know that one line is earlier than another. Next, Lesson 3: Dates and Times in R. You will turn date text into real dates you can do arithmetic on, and handle the time zones that trip everyone up.