R 字符串处理完全指南:从基础操作到正则实战

发布时间:2026/11/2 1:49:02
R 字符串处理完全指南:从基础操作到正则实战

一、为什么 R 的字符串处理值得单独讲

在数据分析的日常工作中,字符串处理出现的频率远比想象中高:清洗问卷里的自由文本、解析日志文件、从文件名中提取日期、把变量名拼成公式、给图表批量生成标题……这些任务都离不开字符串。

R 的字符串能力由两部分组成:一部分是 base R 自带的函数,另一部分是stringr包提供的统一接口。前者无需安装、随处可用,后者命名规范、参数一致,适合处理复杂场景。本文会同时覆盖两者,并在每个知识点上给出可直接运行的代码。

本文所有示例基于 R 4.3 及以上版本,stringr版本为 1.5.x。示例中的中文文本默认使用 UTF-8 编码。

二、字符串的创建与基本属性

2.1 创建字符串

R 中没有单独的字符类型,单个字符串本质上就是长度为 1 的字符向量。用单引号或双引号都可以创建,两者在 R 中完全等价。

s1 <- "hello" s2 <- 'world' s3 <- "包含中文的字符串" 单双引号等价,但内部含引号时需要转义或换用另一种引号 s4 <- "He said: "R is great"" s5 <- 'He said: "R is great"' print(s4) print(s5)

2.2 字符向量与长度

需要特别注意:length()返回的是向量中元素的个数,而不是单个字符串的字符数。要获取字符数应使用nchar()。

x <- c("apple", "banana", "cherry") length(x) # 3,向量中有 3 个元素 nchar(x) # 6 6 6,每个字符串的字符数 nchar("数据分析") # 4,中文按字符计数 按字节计数,中文在 UTF-8 下通常占 3 字节 nchar("数据分析", type = "bytes")

2.3 缺失值与空字符串

NA表示缺失,""表示空字符串,两者语义完全不同,判断方式也不同。

v <- c("a", "", NA) is.na(v) # FALSE FALSE TRUE v == "" # FALSE TRUE NA nchar(v) # 1 0 NA 安全判断空字符串 is_empty <- function(x) !is.na(x) & nchar(x) == 0 is_empty(v) # FALSE TRUE FALSE

三、字符串拼接与拆分

3.1 拼接:paste 与 paste0

paste()默认用空格连接,paste0()等价于sep = "",是拼接时最常用的写法。

paste("R", "语言") # "R 语言" paste0("R", "语言") # "R语言" paste("a", "b", "c", sep = "-") # "a-b-c" 向量化拼接:长度不同时会自动循环补齐 paste0("第", 1:3, "章") "第1章" "第2章" "第3章" collapse 把整个向量压成一个字符串 paste(c("A", "B", "C"), collapse = ", ") "A, B, C"

3.2 拆分:strsplit

path <- "2024-05-18_report_final.csv" parts <- strsplit(path, "[-_.]")[[1]] print(parts) "2024" "05" "18" "report" "final" "csv" 注意 strsplit 返回的是 list,即使输入只有一个元素 class(strsplit(path, "_")) # "list" 批量拆分 files <- c("a_1.txt", "b_2.txt") strsplit(files, "_") [[1]] "a" "1.txt" [[2]] "b" "2.txt"

3.3 stringr 的对应写法

library(stringr) str_c("R", "语言", sep = " ") # "R 语言" str_c("第", 1:3, "章") # "第1章" "第2章" "第3章" str_c(c("A", "B", "C"), collapse = ", ") # "A, B, C" str_split(path, "[-.]")[[1]] str_split_fixed(path, "", n = 2) # 返回矩阵,固定列数

四、子串提取与替换

4.1 按位置截取

s <- "abcdefgh" substr(s, 1, 3) # "abc" substr(s, 4, 6) # "def" substring(s, 3) # "cdefgh",从第 3 位到末尾 向量化截取 substr(c("apple", "banana"), 1, 2) # "ap" "ba" stringr 写法,参数顺序更直观 str_sub(s, 1, 3) # "abc" str_sub(s, -3, -1) # "fgh",负数表示从末尾计数

4.2 替换子串

sub()只替换第一个匹配,gsub()替换全部匹配,这是初学者最容易踩坑的地方。

text <- "cat cat cat" sub("cat", "dog", text) # "dog cat cat" gsub("cat", "dog", text) # "dog dog dog" 删除特定字符:替换为空串 gsub("-", "", "2024-05-18") # "20240518" stringr 对应函数 str_replace(text, "cat", "dog") # 只替换第一个 str_replace_all(text, "cat", "dog") # 替换全部

4.3 大小写转换

toupper("hello R") # "HELLO R" tolower("HELLO R") # "hello r" 首字母大写(base R 无内置函数,可自行实现) simple_cap <- function(x) { paste0(toupper(substr(x, 1, 1)), tolower(substr(x, 2, nchar(x)))) } simple_cap("hello world") # "Hello world" stringr 提供 str_to_title str_to_title("hello world") # "Hello World" str_to_sentence("hello world") # "Hello world"

五、空白处理与填充

数据清洗中,前后空格是最常见的脏数据来源之一。

s <- " hello world " trimws(s) # "hello world" trimws(s, which = "left") # "hello world " trimws(s, which = "right") # " hello world" stringr 版本 str_trim(s) # 去两端 str_trim(s, side = "left") # 只去左侧 str_squish(" a b c ") # "a b c",同时压缩中间多余空格

填充在生成固定宽度报表或对齐输出时很有用:

str_pad("7", width = 3, pad = "0") # "007" str_pad("7", width = 3, side = "right") # "7 " str_pad("R", width = 5, pad = "*") # "****R"

六、字符串匹配与查找

6.1 是否包含与位置查找

s <- c("apple", "banana", "cherry") grepl("an", s) # FALSE TRUE FALSE,返回逻辑向量 grep("an", s) # 2,返回匹配元素的下标 grep("an", s, value = TRUE) # "banana",返回匹配的值 查找子串位置 regexpr("an", "banana") # 2,第一个匹配的起始位置 gregexpr("an", "banana") # 2 4,所有匹配位置 stringr 写法 str_detect(s, "an") # FALSE TRUE FALSE str_which(s, "an") # 2 str_subset(s, "an") # "banana" str_locate("banana", "an") # 起始 2,结束 3

6.2 开头与结尾匹配

files <- c("report_2024.csv", "data_2024.csv", "report_2023.xlsx") 以 report 开头 str_starts(files, "report") # TRUE FALSE TRUE 以 csv 结尾 str_ends(files, "csv") # TRUE TRUE FALSE 组合筛选 files[str_starts(files, "report") & str_ends(files, "csv")] "report_2024.csv"

七、正则表达式实战

R 的正则默认使用 POSIX 扩展语法,stringr则基于 ICU 引擎,支持更完整的语法。下面用几个真实场景说明。

7.1 提取数字与日期

library(stringr) log_line <- "2024-05-18 10:23:45 ERROR user_id=1024 cost=3.5s" 提取所有数字 str_extract_all(log_line, "\d+")[[1]] "2024" "05" "18" "10" "23" "45" "1024" "3" "5" 提取日期 str_extract(log_line, "\d{4}-\d{2}-\d{2}") # "2024-05-18" 提取键值对中的值 str_extract(log_line, "(?<=user_id=)\d+") # "1024",后行断言 str_extract(log_line, "(?<=cost=)[0-9.]+") # "3.5"

7.2 分组捕获与替换

# 把 yyyy-mm-dd 转成 dd/mm/yyyy date_str <- "2024-05-18" str_replace(date_str, "(\\d{4})-(\\d{2})-(\\d{2})", "\\3/\\2/\\1") # "18/05/2024" 批量处理 dates <- c("2024-01-02", "2023-12-31") str_replace(dates, "(\d{4})-(\d{2})-(\d{2})", "\3/\2/\1") "02/01/2024" "31/12/2023" 用 gsub 实现同样效果 gsub("(\d{4})-(\d{2})-(\d{2})", "\3/\2/\1", dates)

7.3 清洗不规范文本

raw <- c(" 张三 ", "李四(销售部)", "王五 - 技术部 ", NA) clean <- raw |> str_trim() |> str_remove("\s*-\s*.$") |> str_remove("\(.\)$") print(clean) "张三" "李四" "王五" NA 只保留中文字符 mixed <- c("订单号A123", "客户B456") str_extract_all(mixed, "[\u4e00-\u9fa5]+") [[1]] "订单号" [[2]] "客户"

7.4 常用正则速查

模式含义示例
\\d数字\\d{3}匹配三位数字
\\w字母、数字、下划线\\w+匹配标识符
\\s空白字符\\s+匹配连续空格
.任意字符(不含换行)a.c匹配 abc
^/$行首 / 行尾^R以 R 开头
[abc]字符集合[aeiou]匹配元音
*+?0 次以上 / 1 次以上 / 0 或 1 次a+至少一个 a
(?<=x)后行断言(?<=id=)\\d+

八、格式化输出:sprintf 与 glue

拼接字符串时,paste0在变量多的情况下可读性很差,sprintf()和glue包是更好的选择。

name <- "张三" score <- 92.456 sprintf("学生 %s 的得分为 %.1f", name, score) "学生 张三 的得分为 92.5" sprintf("%05d", 42) # "00042" sprintf("%.2f%%", 0.875) # "0.88%" glue 包:直接在字符串中嵌入表达式 library(glue) glue("学生 {name} 的得分为 {round(score, 1)}") 学生 张三 的得分为 92.5 循环生成标题 for (i in 1:3) { print(glue("第 {i} 季度销售额:{i * 100} 万元")) }

九、编码问题与中文处理

中文乱码是 R 字符串处理中最常见的坑,根源通常是文件编码与读取时声明的编码不一致。

# 查看当前会话编码 Sys.getlocale("LC_CTYPE") 读取时显式指定编码 df <- read.csv("data.csv", fileEncoding = "UTF-8") 编码转换 x <- "中文" iconv(x, from = "UTF-8", to = "GBK") iconv(x, from = "GBK", to = "UTF-8") 检测非法编码,返回 NA 表示无法转换 iconv("中文", from = "UTF-8", to = "ASCII", sub = NA) 用 sub 参数替换无法转换的字符 iconv("中文abc", from = "UTF-8", to = "ASCII", sub = "?") "??abc"

实践建议:统一在项目中使用 UTF-8,读取外部文件时始终显式声明fileEncoding,避免依赖系统默认编码。

十、综合案例:清洗一份用户信息表

下面把前面用到的技巧串起来,完成一个贴近真实工作的清洗流程。

library(stringr) users <- data.frame( id = 1:5, raw = c( " 张三 | 销售部 | 138-0000-1111 ", "李四|技术部|139-0000-2222", "王五 | 财务部 | 137-0000-3333", "赵六|市场部|136-0000-4444", NA ), stringsAsFactors = FALSE ) clean_users <- users |> transform( name = str_trim(str_split_fixed(raw, "\|", 3)[, 1]), dept = str_trim(str_split_fixed(raw, "\|", 3)[, 2]), phone = str_remove_all(str_split_fixed(raw, "\|", 3)[, 3], "-") ) print(clean_users[, c("id", "name", "dept", "phone")])

输出结果中,姓名与部门的前后空格被清除,手机号中的连字符被移除,缺失行保持NA而不会报错。这个流程可以进一步封装成函数,复用到其他类似表格上。

十一、性能与选型建议

  • 简单拼接与截取:优先用 base R 的paste0、substr,无需额外依赖。
  • 复杂正则与批量清洗:优先用stringr,函数命名统一,str_前缀便于记忆,且对NA的处理更一致。
  • 超大规模文本:考虑stringi包,stringr底层即基于它,直接调用可减少一层封装开销。
  • 避免在循环中反复拼接:用向量化函数一次性处理,或预先分配向量再赋值。
  • 正则尽量具体:用\\d+而不是.+,可显著减少回溯带来的性能损耗。

十二、小结

R 的字符串处理能力可以概括为三层:base R 提供基础能力,stringr提供一致的接口,stringi提供底层性能。掌握拼接、拆分、截取、替换、匹配这五类操作,再配合正则表达式,绝大多数文本清洗任务都能在几行代码内完成。

建议读者把本文的代码复制到 RStudio 中逐段运行,尤其是正则部分,配合str_view()观察匹配结果,理解会快很多。