楚新元 | All in R

Welcome to R Square

用 R 整理随意命名的照片文件

楚新元 / 2024-06-12


今天在知乎上看到一个问题:excel 的人员列表和一个装了照片的文件夹,如何匹配?,具体需求如下:

Excel 表上有 1000 多人,文件夹里有 2000 多人的照片,该怎么样才能知道表格上的 1000 多人,在文件夹里有对应的照片? 照片的命名是不规范的,可能是人名,也可能是人名+其它,但是一定包含了人名。

关于文件重命名问题,我之前写过一篇《按照花名册序号对简历文件重命名》,但是那篇文章假定文件命名比较规范,只是给文件名前面加个序号,而这次这个问题文件命名比较随意,是一个现实中经常碰到的很经典的问题了。哦,我想将来的我也一定会遇到类似的问题。下面是我的代码供读者参考1

加载相关 R 包

library(dplyr)
library(purrr)
library(tidyr)
library(stringr)
library(openxlsx)
library(fuzzyjoin)

创建示例数据

# 创建 data 文件夹
dir.create("./data")

# 生成照片文件
image = c(
  "阿紫﹣中", "张三的个人照片", "照片﹣段誉", 
  "照片(王语嫣)", "2019年个人照片 虚竹", 
  "(段正明)", "段延庆", "刀凤", 
  "倚天张无忌", "曾阿牛牛", "张三峰峰"
)
file.create(paste0("./data/", image, ".jpeg"))

# 生成人员信息表
name = c(
  "张三","段誉","王语嫣","虚竹","段正明",
  "阿紫","段延庆","刀白凤","张三峰"
)
name %>% 
  as.data.frame() %>% 
  set_names("姓名") %>% 
  write.xlsx("./data/roster.xlsx")

梳理解决思路

首先,我们先看看人员信息表和待处理的照片的文件名长啥样:

path = "./data"
roster = read.xlsx(file.path(path, "roster.xlsx"))
print(roster)
#>     姓名
#> 1   张三
#> 2   段誉
#> 3 王语嫣
#> 4   虚竹
#> 5 段正明
#> 6   阿紫
#> 7 段延庆
#> 8 刀白凤
#> 9 张三峰
path %>% 
  list.files(
    pattern = "\\.jpeg$",
    full.names = TRUE
  )
#>  [1] "./data/(段正明).jpeg"            "./data/2019年个人照片 虚竹.jpeg"
#>  [3] "./data/阿紫﹣中.jpeg"            "./data/曾阿牛牛.jpeg"           
#>  [5] "./data/刀凤.jpeg"                "./data/段延庆.jpeg"             
#>  [7] "./data/倚天张无忌.jpeg"          "./data/张三的个人照片.jpeg"     
#>  [9] "./data/张三峰峰.jpeg"            "./data/照片﹣段誉.jpeg"         
#> [11] "./data/照片(王语嫣).jpeg"

根据题意,表上的人都有对应的照片文件,也就是说表上的人名都要匹配到一张照片,但是照片的文件虽然都有名字,但是有些名字和人员信息表里的不完全一致,因此精确匹配在这里是不可行的,只能寻求模糊匹配。模糊匹配目前比较好用的包是 fuzzyjoin

path %>% 
  list.files(
    pattern = "\\.jpeg$",
    full.names = TRUE
  ) %>% 
  as.data.frame() %>% 
  set_names("image") %>% 
  fuzzy_left_join(
    y = roster, 
    by = c("image"="姓名"), 
    match_fun = \(x, y) str_detect(x, y)
  ) 
#>                              image   姓名
#> 1             ./data/(段正明).jpeg 段正明
#> 2  ./data/2019年个人照片 虚竹.jpeg   虚竹
#> 3             ./data/阿紫﹣中.jpeg   阿紫
#> 4             ./data/曾阿牛牛.jpeg   <NA>
#> 5                 ./data/刀凤.jpeg   <NA>
#> 6               ./data/段延庆.jpeg 段延庆
#> 7           ./data/倚天张无忌.jpeg   <NA>
#> 8       ./data/张三的个人照片.jpeg   张三
#> 9             ./data/张三峰峰.jpeg   张三
#> 10            ./data/张三峰峰.jpeg 张三峰
#> 11          ./data/照片﹣段誉.jpeg   段誉
#> 12        ./data/照片(王语嫣).jpeg 王语嫣

从上表我们发现:1.仍然有匹配不到的情况,比如“刀凤”就无法匹配到姓名中的“刀白凤”。2.张三和张三峰峰都匹配到了张三,不唯一。

因此,考虑将照片文件名的每个字符拆开,列表列展开为行,然后文件名里的逐个字符去匹配人员信息表里的人名,根据匹配到的字的多少选择匹配度最高的,从而使问题得以完美解决。

完整实现过程

# 照片和姓名匹配
path %>% 
  list.files(
    pattern = "\\.jpeg$",
    full.names = TRUE
  ) %>% 
  as.data.frame() %>% 
  set_names("image") %>% 
  
  # 去掉文件名中和姓名无关的无用字符
  mutate(
    img_name = gsub(".*/(.+)\\.jpeg", "\\1", image),
    img_name = str_remove_all(img_name, "[(﹣) ]")
  ) %>% 
  
  # 对文件名逐字符进行拆分
  mutate(
    img_name_split = strsplit(img_name, "")
  ) %>% 
  
  # 列表列展开为行
  unnest(cols = img_name_split) %>%
  
  # 文件名逐字符去匹配人员名单的姓名
  fuzzy_left_join(
    y = roster, 
    by = c("img_name_split"="姓名"), 
    match_fun = \(x, y) str_detect(y, x)
  ) %>% 
  
  # 删除没有匹配到的行
  drop_na() %>%
  
  # 按照文件名分组统计姓名被匹配到的次数
  # 例如:次数为 3 表示文件名里有 3 个字符匹配到了姓名里的字符  
  group_by(image) %>% 
  count(姓名) %>% 
  
  # 删除这是匹配到 1 次的情况(人名字至少两个字)
  filter(n != 1) %>% 
  
  # 取和姓名能匹配次数最多的行
  # 这里文件名里的张三和张三峰峰都有 2 个字匹配到了姓名里的张三
  slice_max(n, n = 1) %>%
  ungroup() %>% 
  
  # 姓名多次出现,根据姓名分组,取匹配字数最多的行
  slice_max(n, n = 1, by = 姓名) %>% 
  select(-n) %>% 
 
  # 给图片一个规范的名字
  mutate(
    new_image = paste0(path, "/", 姓名, ".jpeg")
  ) -> result
print(result)
#> # A tibble: 9 × 3
#>   image                           姓名   new_image         
#>   <chr>                           <chr>  <chr>             
#> 1 ./data/(段正明).jpeg            段正明 ./data/段正明.jpeg
#> 2 ./data/2019年个人照片 虚竹.jpeg 虚竹   ./data/虚竹.jpeg  
#> 3 ./data/刀凤.jpeg                刀白凤 ./data/刀白凤.jpeg
#> 4 ./data/张三峰峰.jpeg            张三峰 ./data/张三峰.jpeg
#> 5 ./data/张三的个人照片.jpeg      张三   ./data/张三.jpeg  
#> 6 ./data/段延庆.jpeg              段延庆 ./data/段延庆.jpeg
#> 7 ./data/照片(王语嫣).jpeg        王语嫣 ./data/王语嫣.jpeg
#> 8 ./data/照片﹣段誉.jpeg          段誉   ./data/段誉.jpeg  
#> 9 ./data/阿紫﹣中.jpeg            阿紫   ./data/阿紫.jpeg

锦上添花

顺手把随意的文件名重命名成规范的文件名,例如:“张三.jpeg”

file.rename(result$image, result$new_image)

  1. 文中数据和知乎上张敬信老师的保持一致,同时,我也参考了他的一些代码,感谢张老师。 ↩︎