R: R 正则表达式
最后更新:2026-08-26
上一课我们学了字符串基础操作,但 80% 的真实场景需要模式匹配——不是匹配固定字符串,而是匹配"符合某种规则的字符串"。这就是正则表达式 (Regular Expression, regex) 的威力。
读完这一课你能用 R 正则做:手机号校验、邮箱提取、URL 解析、日志分析、文本挖掘。
1. 你将学到
- 正则表达式基础(字符、量词、锚点、分组)
- R 正则语法(POSIX/Perl 风格)
- stringr 正则函数(str_match/str_extract/str_detect)
- 捕获组与反向引用
- 贪婪 vs 非贪婪匹配
- 实战:手机号、邮箱、URL、身份证号验证
2. 一个日志分析的故事
(1) 痛点:百万条日志怎么办
Alice是运维,需要从 1 GB Nginx 日志中提取所有 404 错误的 URL:
TEXT
📖 仅展示
192.168.1.1 - - [15/Jan/2024:10:30:45 +0800] "GET /api/users/123 HTTP/1.1" 404 512
192.168.1.2 - - [15/Jan/2024:10:30:46 +0800] "POST /api/login HTTP/1.1" 200 1024
192.168.1.3 - - [15/Jan/2024:10:30:47 +0800] "GET /api/products/456 HTTP/1.1" 404 256
...
要在 @,000 rows of里找 404 错误的所有 URL,手要废了——
(2) R 的解法
R
library(stringr)
# 1. 读取日志
log <- readLines("nginx.log")
# 2. 用正则匹配 404 错误的 URL(一行)
pattern <- '"GET (\\S+) HTTP.*" 404'
urls_404 <- str_match(log, pattern)[, 2]
# 3. 统计访问量
url_table <- table(urls_404)
sort(url_table, decreasing = TRUE)[1:10]
3 行代码搞定百万条日志。这就是正则的威力。
3. 正则基础语法
(1) 4 大元素
graph TB
A[正则表达式] --> B[字面字符<br/>abc 123]
A --> C[元字符<br/>. \\d \\s]
A --> D[量词<br/>* + ? {n}]
A --> E[锚点<br/>^ $ \\b]
style A fill:#fff3cd
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#e1d4ff
(2) 字面字符
R
# 直接匹配字面字符
"cat" # 匹配 "cat"
"123" # 匹配 "123"
(3) 元字符(核心)
| 元字符 | 含义 | 示例 | 匹配 |
|---|---|---|---|
. |
任意单字符(除换行) | a.c |
"abc", "axc", "a9c" |
\d |
数字 [0-9] |
\d+ |
"123", "9" |
\D |
非数字 | \D+ |
"abc", "—" |
\s |
空白(空格/Tab/换行) | \s+ |
" ", "\t" |
\S |
非空白 | \S+ |
"abc" |
\w |
字母数字下划线 | \w+ |
"abc_123" |
\W |
非 \w |
\W+ |
"—", "@" |
[abc] |
字符类(a/b/c 任一) | [aeiou] |
任意元音 |
[^abc] |
非字符类 | [^0-9] |
非数字 |
[a-z] |
范围 | [a-z] |
任意小写字母 |
⚠️ 注意:R 字符串里的
\ 是转义符,所以正则的 \d 在 R 里要写成 "\\d"。
(4) 量词
| 量词 | 含义 | 示例 | 匹配 |
|---|---|---|---|
* |
0 次或多次 | ab* |
"a", "ab", "abb" |
+ |
1 次或多次 | ab+ |
"ab", "abb"(不匹配 "a") |
? |
0 次或 1 次 | ab? |
"a", "ab" |
{n} |
恰好 n 次 | \d{3} |
"123" |
{n,} |
至少 n 次 | \d{2,} |
"12", "1234" |
{n,m} |
n 到 m 次 | \d{2,4} |
"12", "1234" |
(5) 锚点
| 锚点 | 含义 | 示例 | 匹配 |
|---|---|---|---|
^ |
字符串开头 | ^Hello |
"Hello..." |
$ |
字符串结尾 | World$ |
"...World" |
\b |
单词边界 | \bcat\b |
"cat"(不匹配 "concatenate") |
4. 分组与捕获
(1) 捕获组 (...)
R
# 提取日期的年月日
str_match("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})")
# [,1] [,2] [,3] [,4]
# [1,] "2024-01-15" "2024" "01" "15"
# 第一列是完整匹配,后面是每个捕获组
(2) 反向引用 \1
R
# 匹配重复的单词(如 "the the")
str_detect("the the cat", "\\b(\\w+)\\s+\\1\\b")
# [1] TRUE
# 匹配 XML/HTML 标签
str_detect("<div>content</div>", "<(\\w+)>.*</\\1>")
# [1] TRUE
(3) 非捕获组 (?:...)
R
# 不捕获的分组(用 ?:)
str_match("John, Smith", "(\\w+)(?:,\\s+)(\\w+)")
# [,1] [,2] [,3]
# "John, Smith" "John" "Smith" ← 逗号空格不单独成组
5. 贪婪 vs 非贪婪
(1) 默认贪婪
R
# 默认贪婪:.* 匹配尽可能多
str_match("<b>text1</b><b>text2</b>", "<b>(.*)</b>")
# [,1] [,2]
# "<b>text1</b><b>text2</b>" "text1</b><b>text2"
# ↑ 贪婪匹配到最后一个 </b>
(2) 非贪婪 *?
R
# 加 ? 变非贪婪:.*? 匹配尽可能少
str_match("<b>text1</b><b>text2</b>", "<b>(.*?)</b>")
# [,1] [,2]
# "<b>text1</b>" "text1" ← 匹配到第一个 </b>
💡 提示:HTML 解析不要用正则——用专用解析器(xml2 第 13 课)。正则适合简单模式匹配。
6. stringr 正则函数
(1) 函数速查表
| 函数 | 作用 | 返回 |
|---|---|---|
str_detect() |
是否匹配 | 逻辑向量 |
str_extract() |
提取第一个匹配 | 向量 |
str_extract_all() |
提取所有匹配 | 列表 |
str_match() |
提取第一个匹配 + 捕获组 | 矩阵 |
str_match_all() |
提取所有匹配 + 捕获组 | 列表 |
str_replace() |
替换第一个匹配 | 向量 |
str_replace_all() |
替换所有匹配 | 向量 |
str_split() |
按匹配分割 | 列表 |
(2) str_detect() 检测
R
# 哪些字符串以 "a" 开头
str_detect(c("apple", "banana", "cherry"), "^a")
# [1] TRUE FALSE FALSE
# 包含数字
str_detect(c("abc", "a1c", "123"), "\\d")
# [1] FALSE TRUE TRUE
(3) str_extract() 提取
R
# 提取数字
str_extract(c("abc 123", "def 456", "no digits"), "\\d+")
# [1] "123" "456" NA
# 提取所有数字(列表)
str_extract_all(c("a1b2c3", "no digits"), "\\d")
# [[1]] "1" "2" "3"
# [[2]] character(0)
(4) str_match() 捕获组
R
# 提取日期年月日
str_match("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})")
# [,1] [,2] [,3] [,4]
# [1,] "2024-01-15" "2024" "01" "15"
# 向量化
dates <- c("2024-01-15", "2024-02-20", "2024-03-25")
str_match(dates, "(\\d{4})-(\\d{2})-(\\d{2})")
# [,1] [,2] [,3] [,4]
# [1,] "2024-01-15" "2024" "01" "15"
# [2,] "2024-02-20" "2024" "02" "20"
# [3,] "2024-03-25" "2024" "03" "25"
(5) str_replace_all() 替换
R
# 把所有数字替换为 X
str_replace_all("abc 123 def 456", "\\d+", "X")
# [1] "abc X def X"
# 复杂替换:用捕获组
str_replace_all("John Smith", "(\\w+) (\\w+)", "\\2, \\1")
# [1] "Smith, John" ← 姓在前
7. 实战:常见验证模式
(1) 各种验证正则
R
# 手机号
phone_pat <- "^1[3-9]\\d{9}$"
# 邮箱
email_pat <- "^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"
# 身份证号(18 位)
id_pat <- "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$"
# URL
url_pat <- "^https?://[\\w.-]+(:\\d+)?(/\\S*)?$"
# IPv4
ipv4_pat <- "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$"
# 日期
date_pat <- "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$"
# 测试
str_detect("13800001234", phone_pat) # TRUE
str_detect("user@example.com", email_pat) # TRUE
str_detect("110101199003078888", id_pat) # TRUE
str_detect("https://example.com", url_pat) # TRUE
(2) 提取 URL 的各部分
R
url <- "https://www.example.com:8080/path/to/page?query=1#section"
# 提取协议、域名、端口、路径、参数
pattern <- "^(https?)://([^:/]+)(?::(\\d+))?(/[^?#]*)?(?:\\?([^#]*))?(?:#(.*))?$"
matches <- str_match(url, pattern)
# [,1] [,2] [,3] [,4] [,5] [,6] [,7]
# "https://..." "https" "www.example.com" "8080" "/path/to/page" "query=1" "section"
8. 实战:Nginx 日志分析
下面是一个完整工作流示例,把本课所有正则知识串起来。
▶ 示例:Nginx 访问日志分析
R
📖 仅展示
# ============================================
# Nginx 访问日志分析
# 功能:用正则解析 @,000 rows of日志,统计关键指标
# ============================================
library(stringr)
library(dplyr)
# 1. 模拟 1000 行 Nginx 日志(实际项目用 readLines 读文件)
set.seed(42)
n <- 1000
ips <- paste0("192.168.", sample(1:255, n, replace = TRUE), ".",
sample(1:255, n, replace = TRUE))
methods <- sample(c("GET", "POST", "PUT", "DELETE"), n, replace = TRUE,
prob = c(0.7, 0.2, 0.05, 0.05))
paths <- paste0("/api/", sample(c("users", "products", "orders", "login"),
n, replace = TRUE), "/",
sample(1:1000, n, replace = TRUE))
status_codes <- sample(c(200, 201, 301, 400, 404, 500), n, replace = TRUE,
prob = c(0.7, 0.05, 0.05, 0.05, 0.1, 0.05))
sizes <- sample(100:5000, n)
# 拼接成日志行
log_lines <- sprintf(
'%s - - [15/Jan/2024:10:30:%02d +0800] "%s %s HTTP/1.1" %d %d',
ips, sample(0:59, n, replace = TRUE), methods, paths, status_codes, sizes
)
# 2. 写出日志文件
writeLines(log_lines, "nginx_demo.log")
cat("已生成 1000 行日志\n")
# 3. 用正则解析日志
# 格式:IP - - [时间] "方法 URL 协议" 状态码 大小
log_pattern <- '^(\\S+) - - \\[([^\\]]+)\\] "(\\S+) (\\S+) (\\S+)" (\\d+) (\\d+)$'
# 提取所有捕获组
matches <- str_match(log_lines, log_pattern)
colnames(matches) <- c("full", "ip", "time", "method", "path", "protocol",
"status", "size")
# 转数据框
logs <- as_tibble(matches) |> select(-full) |>
mutate(
status = as.integer(status),
size = as.integer(size)
)
cat("\n=== 前 6 条解析结果 ===\n")
print(head(logs, 6))
# 4. 分析 1:状态码分布
cat("\n=== 状态码分布 ===\n")
status_summary <- logs |>
group_by(status) |>
summarise(次数 = n(), 占比 = round(n() / nrow(logs) * 100, 2)) |>
arrange(desc(次数))
print(status_summary)
# 5. 分析 2:找出 404 错误的所有 URL
cat("\n=== 404 错误 Top 5 ===\n")
errors_404 <- logs |>
filter(status == 404) |>
group_by(path) |>
summarise(次数 = n()) |>
arrange(desc(次数)) |>
head(5)
print(errors_404)
# 6. 分析 3:每个 IP 的访问量
cat("\n=== Top 5 活跃 IP ===\n")
top_ips <- logs |>
group_by(ip) |>
summarise(
访问次數 = n(),
错误数 = sum(status >= 400),
总流量 = sum(size)
) |>
arrange(desc(访问次數)) |>
head(5)
print(top_ips)
# 7. 分析 4:每小时访问量
cat("\n=== 每分钟访问量(前 10 分钟)===\n")
logs <- logs |>
mutate(minute = str_sub(time, 15, 16))
per_minute <- logs |>
group_by(minute) |>
summarise(访问次數 = n(), 错误数 = sum(status >= 400)) |>
head(10)
print(per_minute)
# 8. 提取邮箱(如果日志含邮箱)
text_with_emails <- "Contact alice@example.com or bob@test.org for support."
email_pat <- "[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}"
emails <- str_extract_all(text_with_emails, email_pat)
cat("\n=== 提取的邮箱 ===\n")
print(emails)
# 9. 清理
file.remove("nginx_demo.log")
预期输出(节选):
TEXT
📖 仅展示
=== 状态码分布 ===
# A tibble: 6 × 3
status 次数 占比
<int> <int> <dbl>
1 200 698 70
2 404 105 10
3 201 52 5
...
=== Top 5 活跃 IP ===
# A tibble: 5 × 4
ip 访问次數 错误数 总流量
<chr> <int> <int> <int>
1 192.168.97.1 8 1 18499
2 192.168.52.244 7 2 18438
...
❓ 常见问题
Q
\\d 为什么是两个反斜杠?A R 字符串
"\\d" 实际是正则的 \d——R 把 \\ 解析为单 \。同理 "\\s" 才是正则的 \s。Q 怎么匹配中文?
A R 4.x UTF-8 直接
[\u4e00-\u9fff] 或 "中文" 字面量。如 GBK 文件先转 UTF-8。📖 小节
- 正则是模式匹配语言,由字面字符 + 元字符 + 量词 + 锚点组成
- 元字符
.\d\s\w^$\b是核心 - R 字符串里
\是转义,正则\d写成"\\d" - 4 大量词
*+?{n,m}控制重复次数 - 捕获组
(...)+ 反向引用\1提取和复用 - stringr 正则函数:
str_detect/str_extract/str_match/str_replace_all/str_split - 贪婪匹配最长字符串,加
?变非贪婪 - HTML/XML 解析不要用正则——用 xml2 解析器
📝 作业
-
基础题:用正则从字符串
"价格:$99.50,数量:3 件,总计:$298.50"提取所有金额($XX.XX格式),输出数字列表。 -
基础题:用正则验证以下数据是否符合邮箱格式:①
user@example.com②user.name+tag@sub.example.co.uk③invalid@④@example.com。输出逻辑向量。 -
基础题:用正则从
"今天是 2024-01-15,天气晴朗;明天是 2024-01-16,多云"提取所有日期,输出字符向量。 -
进阶题:模拟 100 条"订单确认邮件"文本(含订单号、金额、日期、商品名),用正则提取这 4 个字段到数据框,验证每条都成功提取。
-
挑战题:写一段脚本读 1000 行 Nginx 访问日志,用正则:① 解析每行字段(IP/时间/方法/URL/状态码/大小)② 统计 4xx 错误的 URL Top 10 ③ 找出访问频率最高的 IP ④ 算出每小时平均流量。截图保存分析结果。