用 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)
-
文中数据和知乎上张敬信老师的保持一致,同时,我也参考了他的一些代码,感谢张老师。 ↩︎