在Ruby中,将字符串分成给定长度的块的最佳方法是什么?

101

我一直在寻找一种优雅高效的方式,将一个字符串按给定长度切割成子字符串。

到目前为止,我能想到的最好方法是:

def chunk(string, size)
  (0..(string.length-1)/size).map{|i|string[i*size,size]}
end

>> chunk("abcdef",3)
=> ["abc", "def"]
>> chunk("abcde",3)
=> ["abc", "de"]
>> chunk("abc",3)
=> ["abc"]
>> chunk("ab",3)
=> ["ab"]
>> chunk("",3)
=> []

如果您希望chunk("", n)返回[""]而不是[]。那么,只需在方法的第一行添加以下内容:

return [""] if string.empty?

你会推荐更好的解决方案吗?

编辑

感谢Jeremy Ruten提供这个优雅而高效的解决方案:[编辑:不高效!]

def chunk(string, size)
    string.scan(/.{1,#{size}}/)
end

编辑

使用string.scan的方法将512k的字符串分成10000个1k块需要约60秒,而原始的基于切片的解决方案只需要2.4秒。


1
你的原始解决方案已经尽可能地高效和优雅:不需要检查字符串的每个字符以确定在哪里切割它,也不需要将整个字符串转换为数组,然后再转换回来。 - android.weasel
10个回答

176

使用 String#scan 方法:

>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{4}/)
=> ["abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx"]
>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{1,4}/)
=> ["abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx", "yz"]
>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{1,3}/)
=> ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]

5
def chunk(string, size); string.scan(/.{1,#{size}}/); end这段代码的作用是将字符串按指定大小分块,并返回分块后的结果。 - MiniQuark
1
哇,现在我感觉好蠢啊。我甚至从来没有去检查过 scan 如何工作。 - Chuck
20
请注意这个解决方案;它是一个正则表达式,其中/.部分表示它将包括除换行符\n外的所有字符。如果您想包括换行符,请使用string.scan(/.{4}/m) - professormeowingtons
1
多么聪明的解决方案!我喜欢正则表达式,但我从未想过使用量词来实现这个目的。谢谢Jeremy Ruten。 - Cec
这个有效吗? - juliangonzalez
显示剩余2条评论

24
这里有另一种做法:
"abcdefghijklmnopqrstuvwxyz".chars.to_a.each_slice(3).to_a.map {|s| s.to_s }

或者,

"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)

要么:

=> ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]

25
另一种写法:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join) - Finbarr
3
我喜欢这个函数,因为它可以处理包含换行符的字符串。 - Steve Davis
1
这应该是被接受的解决方案。如果长度不匹配_pattern_,使用scan可能会丢失最后一个标记。 - count0
Finbarr的替代方案为我返回了此答案中的输出(一个包含9个字符串对象的数组,最大长度为3)。答案中的代码本身返回了8个每个都有3个字母和最后一个只有两个字母的数组:["y", "z"]。顺便说一下,我使用的是Ruby 3.0.1。 - Tyler James Young

6

以下是另一种解决方案,适用于处理大型字符串且不需要同时存储所有块的略微不同情况。采用这种方式,每次只存储单个块,比切片字符串更快:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end

对于非常大的字符串,这绝对是最佳方法。这将避免将整个字符串读入内存并出现Errno :: EINVAL错误,例如Invalid argument @ io_freadInvalid argument @ io_write - Joshua Pinter

6

如果你知道你的字符串是分块大小的倍数,那么我认为这是最有效的解决方案。

def chunk(string, size)
    (string.length / size).times.collect { |i| string[i * size, size] }
end

并且针对零部件进行处理。
def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end

4
如果你将string.length / size替换为(string.length + size - 1) / size,那么你的字符串长度不必是分块大小的倍数。这种模式在需要处理整数截断的C代码中很常见。 - nitrogen

6

我做了一个小测试,将大约593MB的数据分成了18991个32KB的块。 您使用的切片+映射版本在使用100% CPU超过15分钟后我按下了ctrl+C。而使用String#unpack的这个版本只用了3.6秒:

def chunk(string, size)
  string.unpack("a#{size}" * (string.size/size.to_f).ceil)
end

你会如何建议处理UTF8字符串?(在解包中,“a”说明符似乎不能很好地处理UTF8) - user1070300

1
test.split(/(...)/).reject {|v| v.empty?}

拒绝是必要的,因为它否则会包含集合之间的空格。我的正则表达式技能不足以立即看到如何解决这个问题。


扫描方法会忽略不匹配的字符,例如:如果您尝试在长度为10的字符串上切割成3个部分,则会得到3个部分,但其中1个元素将被丢弃。而您的方法不会这样做,因此更好。 - vinicius gati

1

一种更好的解决方案,考虑到字符串的最后一部分可能小于块大小:

def chunk(inStr, sz)  
  return [inStr] if inStr.length < sz  
  m = inStr.length % sz # this is the last part of the string
  partial = (inStr.length / sz).times.collect { |i| inStr[i * sz, sz] }
  partial << inStr[-m..-1] if (m % sz != 0) # add the last part 
  partial
end

0

你还有其他的限制吗?否则我会很想做一些简单的事情,比如

[0..10].each {
   str[(i*w),w]
}

除了要简单、优雅和高效之外,我实际上没有任何限制。我喜欢你的想法,但你介意将其转化为一个方法吗?[0..10]可能会变得稍微复杂一些。 - MiniQuark
我修正了我的示例,使用了str[iw,w]而不是str[iw...(i+1)*w]。谢谢。 - MiniQuark
应该使用(1..10).collect而不是[0..10].each。 [1..10]是一个只有一个元素-范围的数组。 (1..10)是范围本身。 +each+返回调用它的原始集合(在这种情况下为[1..10]),而不是块返回的值。我们需要使用+map+。 - Chuck

0
只需要使用text.scan(/.{1,4}/m)即可解决该问题。

0

我个人遵循了user8556428的想法,避免了大多数提案引入的昂贵中间值,并避免修改输入字符串。我希望能够将其用作生成器(例如使用s.each_slice.with_index)。

我的用例实际上是关于字节而不是字符的。对于字符大小,strscan是一个很好的解决方案。

class String
    # Slices of fixed byte-length.  May cut multi-byte characters.
    def each_slice(n = 1000, &block)
        return if self.empty?

        if block_given?
            last = (self.length - 1) / n
            (0 .. last).each do |i|
                yield self.slice(i * n, n)
            end
        else
            enum_for(__method__, n)
        end
    end
end


p "abcdef".each_slice(3).to_a # => ["abc", "def"]   
p "abcde".each_slice(3).to_a  # => ["abc", "de"]    
p "abc".each_slice(3).to_a    # => ["abc"]          
p "ab".each_slice(3).to_a     # => ["ab"]           
p "".each_slice(3).to_a       # => []               

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