字符串清洗经常需要批量修改一整列文本。stringr 为常见操作提供统一的 str_ 前缀,函数以字符向量为输入,能够直接放进 dplyr::mutate() 或管道中。
library(stringr)
转换大小写
大小写转换主要使用三个函数:
| 任务 | 函数 | 示例结果 |
|---|---|---|
| 全部转为小写 | str_to_lower() |
"Hello" → "hello" |
| 全部转为大写 | str_to_upper() |
"Hello" → "HELLO" |
| 单词首字母大写 | str_to_title() |
"hello world" → "Hello World" |
x <- c("bIoINFO", "hello world", "R语言", "张三")
str_to_lower(x)
# [1] "bioinfo" "hello world" "r语言" "张三"
str_to_upper(x)
# [1] "BIOINFO" "HELLO WORLD" "R语言" "张三"
str_to_title(x)
# [1] "Bioinfo" "Hello World" "R语言" "张三"
大小写规则与语言有关。需要按特定语言处理时,显式提供 locale:
str_to_upper("straße", locale = "de")
# [1] "STRASSE"
中文字符没有大小写,转换时通常保持不变。str_to_title() 适合英文标题或姓名,但它只执行通用的标题化规则,不能替代领域特定的书写规范。
在数据框中批量转换
单列可以直接在 mutate() 中处理:
library(dplyr)
people <- tibble(
name = c("zhang san", "LI Si"),
city = c("beijing", "SHANGHAI")
)
people |>
mutate(city = str_to_title(city))
多个字符列使用 across(),并通过 where(is.character) 避免误改数值等其他类型:
people |>
mutate(across(where(is.character), str_to_title))
大小写标准化会改变原始文本。若大小写本身可能携带意义,例如基因符号、样本编号或密码,不应不加区分地处理整个数据框。
清理空白
str_trim() 只移除字符串两端的空白;str_squish() 还会把内部连续空白压缩成一个空格。
str_trim(" hello ")
# [1] "hello"
str_squish(" hello world \n test ")
# [1] "hello world test"
前者适合只清理边界,后者适合规范自由输入的文本。对需要保留换行或对齐空格的内容,不要使用 str_squish()。
检测与替换
检测函数返回逻辑向量,可以用于筛选或创建标记列:
files <- c("sample.csv", "notes.txt", "report.csv")
str_starts(files, "sample")
str_ends(files, ".csv")
str_detect(files, "report|notes")
替换第一个匹配使用 str_replace(),替换所有匹配使用 str_replace_all():
str_replace("apple apple", "apple", "pear")
# [1] "pear apple"
str_replace_all("apple apple", "apple", "pear")
# [1] "pear pear"
这些函数默认把模式当作正则表达式。要匹配字面文本,可使用 fixed() 明确表达意图:
str_replace_all("a.b.c", fixed("."), "-")
# [1] "a-b-c"
正则表达式速查
正则表达式(regular expression)描述一类字符串模式。str_detect()、str_extract()、str_replace() 和 str_split() 等函数都可以把正则作为 pattern。
| 模式 | 含义 | 示例 |
|---|---|---|
. |
任意单字符 | "a.c" |
[abc] |
a、b 或 c |
"a[bc]d" |
[a-z] |
小写字母范围 | "[a-z]+" |
[^abc] |
除 a、b、c 外的字符 |
"[^abc]" |
\\d / \\D |
数字 / 非数字 | "\\d+" |
\\w / \\W |
单词字符 / 非单词字符 | "\\w+" |
\\s / \\S |
空白 / 非空白 | "\\s+" |
^ / $ |
字符串开头 / 结尾 | "^sample", "csv$" |
a|b |
a 或 b |
"cat|dog" |
(ab) |
分组 | "(ab)+" |
{n} |
恰好重复 n 次 | "a{3}" |
{n,m} |
重复 n 到 m 次 | "a{2,4}" |
? |
重复 0 或 1 次 | "a?" |
+ |
重复 1 次或更多 | "a+" |
* |
重复 0 次或更多 | "a*" |
正则中的反斜杠和 R 字符串中的转义叠加,因此匹配数字的正则 \d+ 在 R 代码里写作:
pattern <- "\\d+"
str_extract_all("2025年1月30日", pattern)
# [[1]] "2025" "1" "30"
检测、提取与替换
x <- c("R language", "Python", "Ruby")
str_detect(x, "^R")
# [1] TRUE FALSE TRUE
str_extract("sample_2025.csv", "\\d+")
# [1] "2025"
str_replace_all(c("abcda", "adcba"), "a", "A")
# [1] "AbcdA" "AdcbA"
str_extract() 取第一个匹配,str_extract_all() 取全部匹配;str_replace() 只替换第一个,str_replace_all() 替换全部。函数名已经表达了返回范围,不必先用位置函数再手动截取。
贪婪与非贪婪匹配
* 和 + 默认尽可能多地匹配:
text <- "<b>one</b><b>two</b>"
str_extract(text, "<b>.*</b>")
# [1] "<b>one</b><b>two</b>"
在量词后加 ?,让它尽可能少地匹配:
str_extract_all(text, "<b>.*?</b>")
# [[1]] "<b>one</b>" "<b>two</b>"
若目标只是固定边界内“不包含某字符”的内容,明确的字符类通常比非贪婪通配符更容易检查。
Base R 对应函数
Base R 也提供完整的正则工具:
| 函数 | 返回内容 |
|---|---|
grep() |
匹配元素的位置,或设置 value = TRUE 返回值 |
grepl() |
每个元素是否匹配的逻辑向量 |
sub() |
替换第一个匹配 |
gsub() |
替换全部匹配 |
regexpr() |
第一个匹配的位置和长度 |
gregexpr() |
全部匹配的位置和长度 |
regmatches() |
根据位置结果提取匹配文本 |
strsplit() |
按模式拆分文本 |
x <- "日期:2025年1月30日"
regmatches(x, gregexpr("\\d+", x))
grepl("^R", c("R语言", "Python", "Ruby"))
gsub("a", "A", c("abcda", "adcba"))
正则适合描述模式,不适合替代结构化解析。日期、URL、HTML、JSON 等内容若已有专门解析器,应优先使用解析器,而不是不断扩展一条难以验证的正则。
拆分、拼接与长度
str_split("A,B,C", ",")
str_c(c("sample", "control"), c("01", "02"), sep = "-")
str_length(c("R", "stringr"))
str_split() 通常返回 list,因为不同字符串可能拆出不同数量的片段。若一个数据框列需要拆成多列或多行,应使用 tidyr 的 separate_* 家族;那组函数会同时处理字符串和表格结构。
使用 str_sub() 提取子串
按字符位置截取一段文本:
x <- c("ABCDEF", "ghijkl", "123456")
str_sub(x, start = 2, end = 4)
# [1] "BCD" "hij" "234"
负数位置从字符串末尾开始计算,-1 表示最后一个字符:
str_sub(x, -3, -1)
# [1] "DEF" "jkl" "456"
这适合结构固定的样本编号、条码或文件名。如果字段长度不固定,应根据分隔符或正则模式解析,而不是假设某段内容永远位于相同字符位置。
Base R 的 substring() 使用从 1 开始的正数位置:
substring(x, first = 2, last = 4)
# [1] "BCD" "hij" "234"
str_c()、paste() 与 collapse
逐元素拼接两个向量:
str_c(c("sample", "control"), c("01", "02"), sep = "-")
# [1] "sample-01" "control-02"
Base R 中,paste() 默认以空格分隔,paste0() 默认不加分隔符:
paste("Hello", "World")
# [1] "Hello World"
paste0("Sample_", 1:5)
# [1] "Sample_1" "Sample_2" "Sample_3" "Sample_4" "Sample_5"
sep 控制每组输入之间的分隔符,collapse 则把逐元素生成的全部结果继续合成一个字符串:
paste(letters[1:3], 1:3, sep = "-", collapse = "; ")
# [1] "a-1; b-2; c-3"
输入长度不同时会使用向量回收规则:
paste(c("A", "B"), 1:3, sep = "-")
# [1] "A-1" "B-2" "A-3"
这种回收只有在意图明确时才安全。需要逐行对应的两个 ID 向量通常应先验证长度一致,不能让回收悄悄制造标签。
构造文件路径时使用 file.path(),而不是用 paste() 手动拼接斜杠;前者会处理操作系统路径分隔符。
Base R 对应方法
常见操作在 Base R 中也有对应函数:
| stringr | Base R |
|---|---|
str_to_lower() |
tolower() |
str_to_upper() |
toupper() |
str_split() |
strsplit() |
str_length() |
nchar() |
str_c() |
paste() / paste0() |
两套方法都可以向量化处理文本。项目已经使用 tidyverse 时,stringr 的统一命名和参数顺序更便于组合;只依赖 Base R 的脚本则没有必要仅为大小写转换增加依赖。