3 min read

R语言实例:提取酒店房间床型大小的数字

背景

在之前的某个项目中,需要处理从去哪儿网爬取到的房间床型大小数据。

床型大小数据是类似于“双床1.2m”这样的字符串。

产品需要将床型的具体长度数字提取出来展示。

原始的床型大小数据并不理想,有缺失、有特殊符号、不同单位,以及包含多个床型等情况,需要清洗。

示例样本数据如下:

library(dplyr)

# dplyr 中的 tribble 可以按照如下 row-by-row 来定义数据框
df <- tribble(
  ~room_id, ~bed_size,
  217668, "双床1.2m",
  217670, "大床1.8米",
  217671, "1张大床",
  217672, "3张单人床1.2m",
  215450, "",
  215451, "大床2米",
  294252, "大床1.8m/双床1.2m",
  294253, "圆床直径1.8m",
  294254, "1张大床1.8m和1张单人床1.35m",
  294255, NA
)

# 查看对象类型:数据框
class(df) 
## [1] "tbl_df"     "tbl"        "data.frame"

# 打印(输出)到屏幕上看数据是什么样的
print(df)
## # A tibble: 10 × 2
##    room_id bed_size                     
##      <dbl> <chr>                        
##  1  217668 "双床1.2m"                   
##  2  217670 "大床1.8米"                  
##  3  217671 "1张大床"                    
##  4  217672 "3张单人床1.2m"              
##  5  215450 ""                           
##  6  215451 "大床2米"                    
##  7  294252 "大床1.8m/双床1.2m"          
##  8  294253 "圆床直径1.8m"               
##  9  294254 "1张大床1.8m和1张单人床1.35m"
## 10  294255  <NA>

目标

将非规范的原始床型大小数据,处理为:

  • 床型大小只包含“数字”(单位为),保留小数点后两位数字
  • 多于一种床型大小的情况,用逗号分隔,保留多个数值
  • 床型大小缺失或者没有可用的有效数值时统一为“”(空字符串)

R语言实现

单个向量处理逻辑

把示例样本中多种不同床型的情况,尽可能整合到一个长字符串中(长度为1的字符向量),便于逻辑判断及调试:

x <- "1张大床1.8m和1张单人床1m和1.4米和m及米"

在该示例床型大小的字符中,包含多个数字,但只有在 “m” 或者“米”之前的数值才是有效的数值。

提取字符串中符合某个规则的字符,通常使用 stringr 包中的 str_extract() 函数。

当需要提出一个字符串中的多个符合规则的子字符串时,使用该包中的 str_extract_all() 函数。

str_extract() 函数第一个匹配到符合规则的子字符串,因为只有一个值,故而返回的是向量。

str_extract_all() 函数返回所有匹配到符合规则的子字符串,有可能超过一个值,故而返回的是列表。

提取字符串的正则表达式

这里需要匹配的模式是:“m”或者“米”前面的数字,包含小数点符号。

该匹配模式,在R语言中可通过该 ((\\d+)|\\d+\\.\\d+)(?=(m|米)) 正则表达式实现,说明如下:

  • | 是逻辑或的关系,m|米表示“m”或者“米”
  • \\ 是转义字符,通用的正则表达式中的转义是 \,在R语言中是 \\转义就是改变原定义的含义
  • \\d 表示匹配**数字**,原本的字符 d 转义后在正则表达式中表示 [0-9] 十个数字字符
  • \\. 表示匹配 . 小数点符号,转义是因为原本的 . 在正则表达是中被定义为任意单个字符
  • (?=(m|米)) 表示要匹配字符串,后面要跟着“m”或者“米”,但提取的字符串不包括“m”或者“米”这个用来辅助匹配的字符; 其中`(?=)look arounds 模式,后面要跟着 (m|米) ,但最终匹配的结果不要 (m|米)

关于通用的正则表达式基础知识和用法,可自行在网上搜索“正则表达式”,有很多教程,以及在线测试工具。

在R语言环境中使用正则表达式,推荐参考 RStudio 官网提供的 “Work with Strings with stringr :: CHEAT SHEET”

分步实现处理逻辑

载入处理字符常用的包 stringr

library(stringr) # 载入处理字符常用的包

回顾一下之前用来测试验证的整合后的“床型大小”样本数据:

print(x)
## [1] "1张大床1.8m和1张单人床1m和1.4米和m及米"

用正则表达式提取符合条件的数字(字符串),结果是列表,用 str_extract_all() 函数:

str_extract_all(x, "((\\d+)|\\d+\\.\\d+)(?=(m|米))")
## [[1]]
## [1] "1.8" "1"   "1.4"

将这个列表结果转为向量,用 unlist() 函数:

unlist(str_extract_all(x, "((\\d+)|\\d+\\.\\d+)(?=(m|米))"))
## [1] "1.8" "1"   "1.4"

保留小数点后两位有效数字:使用format()函数,其参数 nsmall 是小数点后保留的位数。

format() 要格式化的是数字的小数点位数,故而先将格式化的对象转为数值(之前从字符串中提取出来的结果是字符串),用 as.numeric() 函数:

format(as.numeric(unlist(str_extract_all(x, "((\\d+)|\\d+\\.\\d+)(?=(m|米))"))), nsmall = 2)
## [1] "1.80" "1.00" "1.40"

将多个床型大小的数字首尾相连拼接成长字符串,使用 str_c() 函数,这里需要指定参数 collapse 作为连接字符串中间的分隔符号。

stringr::str_c() 函数与基础包中的 base::paste() 函数用法相同。

这里使用 str_c() 函数,是要保持所有字符处理都使用 stringr 包处理的统一风格,并强化 stringr 的使用。

启用 collapse 参数,表示将字符向量中的元素拼接在一起,变成汇总函数(对包含多个元素的向量应用函数后返回只有一个元素的向量结果)。

使用另外一个参数 sep,表示多个向量字符串各元素拼接,是向量化函数(每个元素分别对应拼接,并返回等长的向量结果)。

主要注意的是, collapsesep 两个参数是互斥的,不能同时使用。

#多个床型大小的情况用逗号连接作为一个字符串
str_c(format(as.numeric(unlist(str_extract_all(x, "((\\d+)|\\d+\\.\\d+)(?=(m|米))"))), nsmall = 2), collapse = ",") 
## [1] "1.80,1.00,1.40"

在进行以上处理后,原始床型大小数据变成了规范的结果。

处理逻辑函数化

为了使得处理过程更为方便的应用在多个元素的向量上,需要将处理逻辑函数化。

另一方面,为了使得处理过程的代码可读性更强,在如下函数中引入 %>% 这个 pipeline 符号(管道符):其作用是将左边的对象(上一个对象),传递给右边的函数(下一个函数),作为后续函数的第一个参数的值。

%>% 管道符号最初是由 magrittr 包开发,现已流行成为R语言中的一个重要功能符号,以致于很多包都内置了这个 %>% 管道符号,例如在 stringr 包中也已经内置了该 %>% 管道符号可直接使用,无需再额外引用 magrittr 包。

#### 函数:提取床型大小数值  ####

library("stringr") # 字符串处理看,提取、连接
# 并且引入管道符 %>% ,在 stringr 包已包含

fetch_bed_size <- function(x) {
  
  # 床型大小示例
  # x <- "1张大床1.8m和1张单人床1m和1.4米和m及米"
  # x <- "大床"
  # x <- ""
  
  # 引入 %>% 符号使得代码逻辑清晰、可读写增强
  result <- x %>% 
    str_extract_all("((\\d+)|\\d+\\.\\d+)(?=(m|米))") %>% 
    unlist() %>% 
    as.numeric() %>% 
    format(nsmall = 2) %>%
    str_c(collapse = ",")
  
  # 上面结果为空的情况处理为空字符串,之后再返回
  return(ifelse(length(result) != 0, result, "")) 
}

在单个字符向量上应用 fetch_bed_size() 函数:

x1 <- "1张大床1.8m和1张单人床1m和1.4米和m及米"
x2 <- "大床1.8米"
x3 <- "大床"
x4 <- ""

fetch_bed_size(x1)
## [1] "1.80,1.00,1.40"
fetch_bed_size(x2)
## [1] "1.80"
fetch_bed_size(x3)
## [1] ""
fetch_bed_size(x4)
## [1] ""

在函数应用到多个元素的向量时,函数中 unlist() 会将所有的单个元素拆分的结果合并成了一个长度为一的单元素向量,虽然结果没有报错,但并不与输入的每个元素一一对应。

x5 <- c(x1,x2,x3,x4)

print(x5)
## [1] "1张大床1.8m和1张单人床1m和1.4米和m及米"
## [2] "大床1.8米"                             
## [3] "大床"                                  
## [4] ""

# 输入的是包含4个元素的向量
# 返回的是只有1个元素的向量
fetch_bed_size(x5)
## [1] "1.80,1.00,1.40,1.80"

正确的用法是使用 sapply() 来应用 fetch_bed_size() 函数在向量的每个元素上。

# 输入的是包含4个元素的向量
# 返回的是还是4个元素的向量
sapply(x5, fetch_bed_size, USE.NAMES = FALSE)
## [1] "1.80,1.00,1.40" "1.80"           ""               ""

函数向量化

为了使得所定义的函数更简化及便于调用,将 sapply 过程内嵌在 fetch_bed_size 内部使其向量化:

#### 函数:提取床型大小数值  ####

library("stringr") # 字符串处理看,提取、连接
# 并且引入管道符 %>% ,在 stringr 包已包含

fetch_bed_size <- function(y) {
  sapply(y, function(x) {
  
  # 床型大小示例
  # x <- "1张大床1.8m和1张单人床1m和1.4米和m及米"
  # x <- "大床"
  # x <- ""
  
  # 引入 %>% 符号使得代码逻辑清晰、可读写增强
  result <- x %>% 
    str_extract_all("((\\d+)|\\d+\\.\\d+)(?=(m|米))") %>% 
    unlist() %>% 
    as.numeric() %>% 
    format(nsmall = 2) %>%
    str_c(collapse = ",")
  
  # 上面结果为空的情况处理为空字符串,之后再返回
  return(ifelse(length(result) != 0, result, "")) 
  }, USE.NAMES = FALSE
  )
}

在将函数向量化之后,就可以直接像普通的函数一样使用 fetch_bed_size() 函数 :

x5
## [1] "1张大床1.8m和1张单人床1m和1.4米和m及米"
## [2] "大床1.8米"                             
## [3] "大床"                                  
## [4] ""
fetch_bed_size(x5)
## [1] "1.80,1.00,1.40" "1.80"           ""               ""

在数据框中调用函数:

df %>% # df 是最初定义的样例数据(数据框)
  mutate(res_bed_size = fetch_bed_size(bed_size)) 
## # A tibble: 10 × 3
##    room_id bed_size                      res_bed_size
##      <dbl> <chr>                         <chr>       
##  1  217668 "双床1.2m"                    "1.20"      
##  2  217670 "大床1.8米"                   "1.80"      
##  3  217671 "1张大床"                     ""          
##  4  217672 "3张单人床1.2m"               "1.20"      
##  5  215450 ""                            ""          
##  6  215451 "大床2米"                     "2.00"      
##  7  294252 "大床1.8m/双床1.2m"           "1.80,1.20" 
##  8  294253 "圆床直径1.8m"                "1.80"      
##  9  294254 "1张大床1.8m和1张单人床1.35m" "1.80,1.35" 
## 10  294255  <NA>                         "NA"
# mutate 用来增加字段(变量),是 dplyr 包中的函数,在最初已经加载该包

总结

该案例中主要使用了 stringr 包中的 str_extract_all() 函数,配合正则表达式及 look arounds 模式来提前所需的字符。

在函数定义中使用了 %>% 管道符合增加代码逻辑的可读写,并最后将函数向量化便于调用。