基于重叠模式删除字符串的一部分

7

我有以下数据:

dat <- data.frame(x               = c("this is my example text", "and here is my other text example", "my other text is short"),
                  some_other_cols = c(1, 2, 2))

此外,我有以下模式向量:

my_patterns <- c("my example", "is my", "my other text")

我希望实现的目标是删除my_patternsdat$x中出现的任何文本。
我尝试了以下解决方案,但问题是一旦我从文本中删除第一个模式(这里是“我的例子”),我的解决方案就无法再检测到第二个(这里是“是我的”)或第三个模式。
错误的解决方案:
library(tidyverse)
my_patterns_c <- str_c(my_patterns, collapse = "|")

dat_new <- dat %>%
  mutate(short_x = str_replace_all(x, pattern = my_patterns_c, replacement = ""))

我想我可以这样做,循环遍历所有模式,在dat$x中收集与我的模式匹配的字符串位置,然后将它们合并成一个范围,并从文本中删除该范围。例如,我会像dat数据框中添加列一样添加start_pattern_1end_pattern_1等列。所以对于第一行1,我得到了第一种模式的9(开始)和18(结束),第二种模式的6/10。然后我需要检查任何end位置是否与任何start位置重叠(这里是开始9和结束10),并将它们合并为一个范围6-18,并从文本中删除该范围。
问题在于我可能有很多新的起始/结束列(在我的情况下可能有几百种模式),如果我需要逐对比较重叠的范围,我的计算机可能会崩溃。
所以我想知道如何使它工作或者最好的方法是什么。也许(我希望如此)有更好/更优雅/更简单的解决方案。 dat的期望输出应该是:
x                                    some_other_cols    short_x
this is my example text              1                  this text
and here is my other text example    2                  and here example
my other text is short               2                  is short

感谢您的帮助!谢谢。


2
你怎么得到 this text?如果根据你的第一个模式消除了 my example,那么你就剩下了 this is text,它没有单词 my...所以 is 是如何被消除的? - Sotos
2
我喜欢这个问题。它似乎归结为另一个问题:“如何通过重叠将字符串粘合在一起?”如果你能做到这一点,我认为你可以通过创建重叠连接的字符串并将它们添加到模式向量来解决这个问题(这样就会有c("is my example", "is my other text", "my example", "is my", "my other text"))。 - Georgery
1
@Sotos:这正是我的问题,我不想按顺序遍历我的dat$x文本,即我不想从剩余文本中删除第一个模式,然后再删除第二个模式。相反,我想检查原始文本中出现了哪些模式,检查是否有重叠,如果有,则删除“组合”模式。 - deschen
虽然会非常复杂,但起点可以是 matches <- lapply(my_patterns,function(x) regexpr(x,dat$x)),接下来你需要计算每个匹配的开始和结尾位置(start+length),然后判断它们是否重叠以创建一个单一的“范围”来从字符串中移除。 - Tensibai
@deschen 我还在试验一下,我正在使用两个嵌套的lapply调用,这已经有点“怪味”,但这边可能有可行的解决方案。 - Tensibai
显示剩余8条评论
2个回答

6

在问题下评论中由Uwe提到的str_locate_all新选项,极大地简化了代码:

library(stringr)
# Create function to remove matching part of text
# First argument is text, second argument is a list of start and length 
remove_matching_parts <- function(text, positions) {
  if (nrow(positions) == 0) return(text)
  ret <- strsplit(text,"")[[1]]
  lapply(1:nrow(positions), function(x) { ret[ positions[x,1]:positions[x,2] ] <<- NA } )
  paste0(ret[!is.na(ret)],separator="",collapse="")
}

# Loop over the data to apply the pattern
# row = length of vector, columns = length of pattern
matches <- lapply(dat$x, function(x) {
  do.call(rbind,str_locate_all(x, my_patterns)) # transform the list output of str_locate in a table of start/end
})

# Avoid growing a vector in a for loop, create it beforehand, it will be the same length as teh vector we work against
dat$result <- vector("character",length(dat$x))
# Loop on each value to remove the matching parts
for (i in 1:length(dat$x)) {
 dat$result[i] <- remove_matching_parts(as.character(dat$x[i]),matches[[i]])
}

如果您可以手动控制模式定义并创建它,则可以通过正则表达式解决方案实现:

> gsub("(is )?my (other text|example)?","",dat$x)
[1] "this  text"        "and here  example" " is short" 

这个想法是使用可选部分创建模式(在分组括号后面加上 ?)。

我们大致上有:

  • (is )? <= 可选的 "is" 后跟空格
  • my <= 文字 "my" 后跟空格
  • (other text|example)? <= "my " 后的可选文本,可以是 "other text" 或 "example"(用 | 表示)

如果您没有控制权,情况会变得混乱。我希望我的注释足够让人们理解,根据包含的循环数,不要指望它快速:

# Given datas
dat <- data.frame(x               = c("this is my example text", "and here is my other text example", "my other text is short","yet another text"),
                some_other_cols = c(1, 2, 2, 4))

my_patterns <- c("my example", "is my", "my other text")

# Create function to remove matching part of text
# First argument is text, second argument is a list of start and length 
remove_matching_parts <- function(text, positions) {
  ret <- strsplit(text,"")[[1]]
  lapply(positions, function(x) { ifelse(is.na(x),,ret[ x[1]:x[2] ] <<- NA ) } )
  paste0(ret[!is.na(ret)],separator="",collapse="")
}

# Create the matches between a vector and a pattern
# First argument is the pattern to match, second is the vector of charcaters
match_pat_to_vector <- function(pattern,vector) {
  sapply(regexec(pattern,vector), 
         function(x) {
           if(x>-1) { 
             c(start=as.numeric(x), end=as.numeric(x+attr(x,"match.length")) ) # Create a start/end vector from the index and length of the match
           }
         })
}

# Loop over the patterns to create a dataframe of matches
# row = length of vector, columns = length of pattern
matches <- sapply(my_patterns,match_pat_to_vector,vector=dat$x)

# Avoid growing a vector in a for loop, create it beforehand, it will be the same length as teh vector we work against
dat$result <- vector("character",length(dat$x))
# Loop on each value to remove the matching parts
for (i in 1:length(dat$x)) {
 dat$result[i] <- remove_matching_parts(as.character(dat$x[i]),matches[i,])
}

运行后的结果:

> dat
                                  x some_other_cols           result
1           this is my example text               1        this text
2 and here is my other text example               2 and here example
3            my other text is short               2         is short
4                  yet another text               4 yet another text

嗯,如果你能够直观地看出重叠部分并将其硬编码,那么这个方法是可行的。但如果不能呢? - Georgery
令人印象深刻。虽然我不理解所有部分,但已经让我接近解决方案了95%。然而,@Uwe是对的(我还没有想到这个问题),当任何模式在文本中出现多次时,它并不完美地工作(这确实可能发生在我的真实使用情况中)。但无论如何,非常感谢您的努力,我今天学到了不少。 - deschen
@Uwe,最终我使用了str_locate_all(感谢您在问题下的评论中提供的提示),这降低了复杂性并给出了正确的结果。 - Tensibai
@Tensibai 再次感谢您的努力。 我在matches <- str_locate_all中遇到了一些问题,在我的实际例子中,虽然dat$x是一个字符列,my_patterns是一个字符向量(dat有14k行,my_patterns有46个元素),但匹配列表为空。不确定原因,并需要深入研究此问题。 在此发布的示例确实有效。 FYI:@Uwe的更新解决方案仍然有效。因此不确定为什么您的str_locate_all无法正常工作。 - deschen
@deschen 我在Uwe的答案中遇到了一个问题(第4个),其中一个示例为空,因此修改了函数以返回如果没有匹配项。我对你的真实数据有疑问,data.frame默认情况下会生成因子,但是我对直接字符列也没有问题...匹配应该是3个元素列表(开始/结束),如果dat的特定行没有模式匹配,则为空。 - Tensibai
显示剩余15条评论

6
这里有两个关键点:
  1. 要从字符串中删除的模式可能会重叠
  2. 可能有多个重叠的模式需要从字符串中删除
下面的解决方案尝试使用我最喜欢的工具来解决这两个问题。
library(data.table)
setDT(dat)[, rn := .I] # add row numbers to join on later

library(stringr)
library(magrittr) # piping used to improve readability

pos <- 
  # find start and end positions for each pattern
  lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as.data.table) %>% 
           rbindlist(idcol = "rn")) %>% 
  rbindlist() %>% 
  # collapse overlapping positions
  setorder(rn, start, end) %>% 
  .[, grp := cumsum(cummax(shift(end, fill = 0)) < start), by = rn] %>% 
  .[, .(start = min(start), end = max(end)), by = .(rn, grp)]

现在,pos 已经变成:
    rn grp start end
 1:  1   1     6  18
 2:  2   1    10  25
 3:  3   1     1  13
 4:  5   1     6  10
 5:  5   2    24  28
 6:  6   1     1  13
 7:  6   2    15  27
 8:  7   1     3   7
 9:  8   1     1  10
10:  8   2    12  16
11:  8   3    22  34
12:  9   1     1  10
13:  9   2    19  31
# remove patterns from strings from back to front
dat[, short_x := x]
for (g in rev(seq_len(max(pos$grp)))) {
  # update join 
  dat[pos[grp == g], on = .(rn), short_x := `str_sub<-`(short_x, start, end, value = "")]
}
dat[, rn := NULL][   #remove row number
  , short_x := str_squish(short_x)][]   # remove whitespace 
                                             x some_other_cols                          short_x
1:                     this is my example text               1                        this text
2:           and here is my other text example               2                 and here example
3:                      my other text is short               2                         is short
4:                            yet another text               4                 yet another text
5: this is my text where 'is my' appears twice               5 this text where '' appears twice
6:                 my other text is my example               6                                 
7:                                 This myself               7                           Thself
8:          my example is my not my other text               8                              not
9:             my example is not my other text               9                           is not

合并重叠位置的代码改编自此答案

中间结果

lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as.data.table) %>% 
           rbindlist(idcol = "rn"))
[[1]]
   rn start end
1:  1     9  18
2:  6    18  27
3:  8     1  10
4:  9     1  10

[[2]]
   rn start end
1:  1     6  10
2:  2    10  14
3:  5     6  10
4:  5    24  28
5:  6    15  19
6:  7     3   7
7:  8    12  16

[[3]]
   rn start end
1:  2    13  25
2:  3     1  13
3:  6     1  13
4:  8    22  34
5:  9    19  31

该段文字表明模式1和2在第1行重叠,模式2和3在第2行重叠。第5、8和9行具有不重叠的模式。第7行旨在表明无论单词边界如何,都会提取模式。

编辑:dplyr版本

原帖作者已经提到他/她“成功避开了data.table”。因此,我感到挑战,添加了一个dplyr版本:

library(dplyr)
library(stringr)

pos <- 
  # find start end end positions for each pattern
  lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as_tibble) %>% 
           bind_rows(.id = "rn")) %>% 
  bind_rows() %>% 
  # collapse overlapping positions
  arrange(rn, start, end) %>% 
  group_by(rn) %>% 
  mutate(grp = cumsum(cummax(lag(end, default = 0)) < start)) %>% 
  group_by(rn, grp) %>% 
  summarize(start = min(start), end = max(end))
# remove patterns from strings from back to front
dat <- dat %>%
  mutate(rn = row_number() %>% as.character(),
         short_x = x %>% as.character())
for (g in rev(seq_len(max(pos$grp)))) {
  dat <- dat %>% 
    left_join(pos %>% filter(grp == g), by = "rn") %>% 
    mutate(short_x = ifelse(is.na(grp), short_x, `str_sub<-`(short_x, start, end, value = ""))) %>% 
    select(-grp, -start, -end)
}
# remove row number
dat %>% 
  select(-rn) %>% 
  mutate(short_x = str_squish(short_x))
                                            x some_other_cols                          short_x
1                     this is my example text               1                        this text
2           and here is my other text example               2                 and here example
3                      my other text is short               2                         is short
4                            yet another text               4                 yet another text
5 this is my text where 'is my' appears twice               5 this text where '' appears twice
6                 my other text is my example               6                                 
7                              This is myself               7                        This self
8          my example is my not my other text               8                              not
9             my example is not my other text               9                           is not
算法本质上是相同的。然而,在这里,dplyrdata.table 有两个挑战:
  • dplyr 需要明确从 factor 转换为 character
  • dplyr 中没有可用的 update join,因此,比起 data.table 的对应部分,for 循环变得更加冗长(也许,有人知道一种花哨的 purrr 函数或 map-reduce 技巧来完成相同的任务?)

编辑2

以上代码有一些漏洞修复和改进:

  1. 折叠位置已被更正,现在可以处理我添加到 dat 中的某些边缘情况。
  2. seq() 已被替换为 seq_len()
  3. str_squish() 可以减少字符串内重复的空格,并删除字符串开头和结尾的空格。

数据

我添加了一些用例来测试非重叠模式和完全删除,例如:

dat <- data.frame(
  x = c(
    "this is my example text",
    "and here is my other text example",
    "my other text is short",
    "yet another text",
    "this is my text where 'is my' appears twice",
    "my other text is my example",
    "This myself",
    "my example is my not my other text",
    "my example is not my other text"
  ),
  some_other_cols = c(1, 2, 2, 4, 5, 6, 7, 8, 9)
)
my_patterns <- c("my example", "is my", "my other text")

虽然我不太懂这段代码(到目前为止我成功地避开了data.table),但它运行得很好。明天我会检查一下它在我的更大的真实数据集上的表现情况(其中dat包含几千行,而my_patterns包含100或200个模式)。 - deschen
从第一眼看起来运行流畅。使用我的14k数据集并将其与46个模式进行比较,它需要1-2分钟运行时间,但我还没有发现任何问题。我刚尝试了数据表解决方案,现在正在运行tidyverse方法。 - deschen
仅供参考,data.table解决方案的性能比tidyverse方法要快得多。对于玩具数据集中的9行,两者仍处于相同的范围内,但如果我将数据集扩展到例如900行,则data.table的速度是tidyverse的两倍。优势在更大的数据集或更多模式下甚至可能会变得更大。 - deschen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接