将一个字符串分割成固定宽度字符元素的向量

67

我有一个包含文本字符串的对象:

x <- "xxyyxyxy"

我想将其拆分为一个矢量,每个元素包含两个字母:

[1] "xx" "yy" "xy" "xy"

看起来像是strsplit应该是我的解决之道,但由于我没有正则表达式知识,我无法弄清如何使该函数将字符串按照我想要的方式分成块。我该怎么做?


所以你想根据已知计数间隔拆分字符串,strsplit() 可以用于固定字符串或正则表达式,但听起来你想按长度完成? - Dan
没错,我想根据长度来做。strsplit需要匹配分隔符的正则表达式,而我没有分隔符。 - JD Long
1
两年后,在stackoverflow.com上有一个更快的答案。https://dev59.com/Jmgu5IYBdhLWcg3wDS4G#11619681。 - wind
@wind 你应该把那作为一个答案发布,我认为它会是回答的一个很好的补充。 - JD Long
7
这句话的意思是在字符串 x 中查找所有长度为2的子串。str_match_all(x, ".{2}") - Avinash Raj
13个回答

70

使用substring是最佳的方法:

substring(x, seq(1, nchar(x), 2), seq(2, nchar(x), 2))

但是这里有一个使用plyr的解决方案:

library("plyr")
laply(seq(1, nchar(x), 2), function(i) substr(x, i, i+1))

10
为了更通用,如果我们想要每个 n 个字符而不是每 2 个字符,可以使用以下代码:substring(x,seq(1,nchar(x),n),seq(n,nchar(x),n))。请注意,翻译尽力保持原意并使其易于理解,不包含解释或其他额外内容。 - MichaelChirico

27

这里有一个快速的解决方案,它将字符串分割为字符,然后将偶数元素和奇数元素粘合在一起。

x <- "xxyyxyxy"
sst <- strsplit(x, "")[[1]]
paste0(sst[c(TRUE, FALSE)], sst[c(FALSE, TRUE)])

基准测试设置:

library(microbenchmark)

GSee <- function(x) {
  sst <- strsplit(x, "")[[1]]
  paste0(sst[c(TRUE, FALSE)], sst[c(FALSE, TRUE)])
}

Shane1 <- function(x) {
  substring(x, seq(1,nchar(x),2), seq(2,nchar(x),2))
}

library("plyr")
Shane2 <- function(x) {
  laply(seq(1,nchar(x),2), function(i) substr(x, i, i+1))
}

seth <- function(x) {
  strsplit(gsub("([[:alnum:]]{2})", "\\1 ", x), " ")[[1]]
}

geoffjentry <- function(x) {
  idx <- 1:nchar(x)  
  odds <- idx[(idx %% 2) == 1]  
  evens <- idx[(idx %% 2) == 0]  
  substring(x, odds, evens)  
}

drewconway <- function(x) {
  c<-strsplit(x,"")[[1]]
  sapply(seq(2,nchar(x),by=2),function(y) paste(c[y-1],c[y],sep=""))
}

KenWilliams <- function(x) {
  n <- 2
  sapply(seq(1,nchar(x),by=n), function(xx) substr(x, xx, xx+n-1))
}

RichardScriven <- function(x) {
  regmatches(x, gregexpr("(.{2})", x))[[1]]
}

基准测试1:

x <- "xxyyxyxy"

microbenchmark(
  GSee(x),
  Shane1(x),
  Shane2(x),
  seth(x),
  geoffjentry(x),
  drewconway(x),
  KenWilliams(x),
  RichardScriven(x)
)

# Unit: microseconds
#               expr      min        lq    median        uq      max neval
#            GSee(x)    8.032   12.7460   13.4800   14.1430   17.600   100
#          Shane1(x)   74.520   80.0025   84.8210   88.1385  102.246   100
#          Shane2(x) 1271.156 1288.7185 1316.6205 1358.5220 3839.300   100
#            seth(x)   36.318   43.3710   45.3270   47.5960   67.536   100
#     geoffjentry(x)    9.150   13.5500   15.3655   16.3080   41.066   100
#      drewconway(x)   92.329   98.1255  102.2115  105.6335  115.027   100
#     KenWilliams(x)   77.802   83.0395   87.4400   92.1540  163.705   100
#  RichardScriven(x)   55.034   63.1360   65.7545   68.4785  108.043   100

基准测试2:

现在,使用更大的数据。

x <- paste(sample(c("xx", "yy", "xy"), 1e5, replace=TRUE), collapse="")

microbenchmark(
  GSee(x),
  Shane1(x),
  Shane2(x),
  seth(x),
  geoffjentry(x),
  drewconway(x),
  KenWilliams(x),
  RichardScriven(x),
  times=3
)

# Unit: milliseconds
#               expr          min            lq       median            uq          max neval
#            GSee(x)    29.029226    31.3162690    33.603312    35.7046155    37.805919     3
#          Shane1(x) 11754.522290 11866.0042600 11977.486230 12065.3277955 12153.169361     3
#          Shane2(x) 13246.723591 13279.2927180 13311.861845 13371.2202695 13430.578694     3
#            seth(x)    86.668439    89.6322615    92.596084    92.8162885    93.036493     3
#     geoffjentry(x) 11670.845728 11681.3830375 11691.920347 11965.3890110 12238.857675     3
#      drewconway(x)   384.863713   438.7293075   492.594902   515.5538020   538.512702     3
#     KenWilliams(x) 12213.514508 12277.5285215 12341.542535 12403.2315015 12464.920468     3
#  RichardScriven(x) 11549.934241 11730.5723030 11911.210365 11989.4930080 12067.775651     3

尝试使用Ralf Stubner在他的答案https://stackoverflow.com/a/50999966/2371031中提供的C++函数。 - Brian D

21

怎么样?

strsplit(gsub("([[:alnum:]]{2})", "\\1 ", x), " ")[[1]]

基本上,添加一个分隔符(这里是“ ”),然后使用 strsplit 函数。


10

strsplit可能会有问题,看看这样的正则表达式:

strsplit(z, '[[:alnum:]]{2}')  

它将在正确的位置分割,但什么也不剩下了。

您可以使用substring及其相关方法。

z <- 'xxyyxyxy'  
idx <- 1:nchar(z)  
odds <- idx[(idx %% 2) == 1]  
evens <- idx[(idx %% 2) == 0]  
substring(z, odds, evens)  

这是一种很好的方法。我认为我让自己在srtsplit()上陷入了困境,因为它与我想要的strsplit(x,"")非常接近。 - JD Long
如果你需要在3个字符后截断字符串,那么子字符串该如何工作?看起来它只适用于2个字符的截断。 - MySchizoBuddy

8
这里有一种方法,但不使用正则表达式:
a <- "xxyyxyxy"
n <- 2
sapply(seq(1,nchar(a),by=n), function(x) substr(a, x, x+n-1))

7

注意使用substring函数时,如果字符串长度不是你请求的长度的倍数,则在第二个参数中需要加上+(n-1)

substring(x,seq(1,nchar(x),n),seq(n,nchar(x)+n-1,n)) 

你真是个天才!我之前用的是 x <- paste0(x, strrep(" ", n - (nchar(x) %% n))),但这个方法方便多了! - MS Berends

6

完全的黑客技巧,JD,但它完成了任务。

x <- "xxyyxyxy"
c<-strsplit(x,"")[[1]]
sapply(seq(2,nchar(x),by=2),function(y) paste(c[y-1],c[y],sep=""))
[1] "xx" "yy" "xy" "xy"

这正是我正在编写的黑客技巧。当然,我打算使用循环而不是sapply ;) - JD Long

4
一个辅助函数:
fixed_split <- function(text, n) {
  strsplit(text, paste0("(?<=.{",n,"})"), perl=TRUE)
}

fixed_split(x, 2)
[[1]]
[1] "xx" "yy" "xy" "xy"

3

使用C++可以更快。与GSee的版本相比:

GSee <- function(x) {
  sst <- strsplit(x, "")[[1]]
  paste0(sst[c(TRUE, FALSE)], sst[c(FALSE, TRUE)])
}

rstub <- Rcpp::cppFunction( code = '
CharacterVector strsplit2(const std::string& hex) {
  unsigned int length = hex.length()/2;
  CharacterVector res(length);
  for (unsigned int i = 0; i < length; ++i) {
    res(i) = hex.substr(2*i, 2);
  }
  return res;
}')

x <- "xxyyxyxy"
all.equal(GSee(x), rstub(x))
#> [1] TRUE
microbenchmark::microbenchmark(GSee(x), rstub(x))
#> Unit: microseconds
#>      expr   min     lq      mean median     uq       max neval
#>   GSee(x) 4.272 4.4575  41.74284 4.5855 4.7105  3702.289   100
#>  rstub(x) 1.710 1.8990 139.40519 2.0665 2.1250 13722.075   100

set.seed(42)
x <- paste(sample(c("xx", "yy", "xy"), 1e5, replace = TRUE), collapse = "")
all.equal(GSee(x), rstub(x))
#> [1] TRUE
microbenchmark::microbenchmark(GSee(x), rstub(x))
#> Unit: milliseconds
#>      expr       min        lq      mean    median       uq       max neval
#>   GSee(x) 17.931801 18.431504 19.282877 18.738836 19.47943 27.191390   100
#>  rstub(x)  3.197587  3.261109  3.404973  3.341099  3.45852  4.872195   100

1

好的,我使用以下伪代码来完成这个任务:

  1. 在长度为n的每个块中插入一个特殊序列。
  2. 通过该序列拆分字符串。

在代码中,我这样做:

chopS <- function( text, chunk_len = 2, seqn)
{
    # Specify select and replace patterns
    insert <- paste("(.{",chunk_len,"})", sep = "")
    replace <- paste("\\1", seqn, sep = "")

    # Insert sequence with replaced pattern, then split by the sequence
    interp_text <- gsub( pattern, replace, text)
    strsplit( interp_text, seqn)
}

这将返回一个包含分割向量的列表,而不是向量。

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