Ruby:按字节长度限制 UTF-8 字符串

问题描述 投票:0回答:8

此 RabbitMQ 页面 指出:

队列名称最多可为 255 个字节的 UTF-8 字符。

在 ruby (1.9.3) 中,如何按字节数截断 UTF-8 字符串而不在字符中间中断?生成的字符串应该是符合字节限制的最长的有效 UTF-8 字符串。

ruby string utf-8 byte rabbitmq
8个回答
23
投票

对于 Rails >= 3.0,您有 ActiveSupport::Multibyte::Chars 限制方法。

来自 API 文档:

- (Object) limit(limit) 

将字符串的字节大小限制为多个字节而不破坏字符。当字符串的存储由于某种原因受到限制时可用。

示例:

'こんにちは'.mb_chars.limit(7).to_s # => "こん"

10
投票

bytesize
将为您提供字符串的长度(以字节为单位),而(只要字符串的编码设置正确)诸如切片之类的操作不会破坏字符串。

一个简单的过程就是迭代字符串

s.each_char.each_with_object('') do|char, result| 
  if result.bytesize + char.bytesize > 255
    break result
  else
    result << char
  end
end

如果你很狡猾,你可以直接复制前 63 个字符,因为任何 unicode 字符在 utf-8 中最多为 4 个字节。

请注意,这仍然不完美。例如,假设字符串的最后 4 个字节是字符“e”并结合了锐音符号。切片最后 2 个字节会生成一个仍然是 utf8 的字符串,但就用户所看到的内容而言,会将输出从“é”更改为“e”,这可能会改变文本的含义。当您只是命名 RabbitMQ 队列时,这可能不是什么大问题,但在其他情况下可能很重要。例如,在法语中,时事通讯标题“Un policier tué”的意思是“一名警察被杀”,而“Un policier tue”的意思是“一名警察杀人”。


5
投票

我想我找到了有用的东西。

def limit_bytesize(str, size)
  str.encoding.name == 'UTF-8' or raise ArgumentError, "str must have UTF-8 encoding"

  # Change to canonical unicode form (compose any decomposed characters).
  # Works only if you're using active_support
  str = str.mb_chars.compose.to_s if str.respond_to?(:mb_chars)

  # Start with a string of the correct byte size, but
  # with a possibly incomplete char at the end.
  new_str = str.byteslice(0, size)

  # We need to force_encoding from utf-8 to utf-8 so ruby will re-validate
  # (idea from halfelf).
  until new_str[-1].force_encoding('utf-8').valid_encoding?
    # remove the invalid char
    new_str = new_str.slice(0..-2)
  end
  new_str
end

用途:

>> limit_bytesize("abc\u2014d", 4)
=> "abc"
>> limit_bytesize("abc\u2014d", 5)
=> "abc"
>> limit_bytesize("abc\u2014d", 6)
=> "abc—"
>> limit_bytesize("abc\u2014d", 7)
=> "abc—d"

更新...

没有 active_support 的分解行为:

>> limit_bytesize("abc\u0065\u0301d", 4)
=> "abce"
>> limit_bytesize("abc\u0065\u0301d", 5)
=> "abce"
>> limit_bytesize("abc\u0065\u0301d", 6)
=> "abcé"
>> limit_bytesize("abc\u0065\u0301d", 7)
=> "abcéd"

使用 active_support 分解行为:

>> limit_bytesize("abc\u0065\u0301d", 4)
=> "abc"
>> limit_bytesize("abc\u0065\u0301d", 5)
=> "abcé"
>> limit_bytesize("abc\u0065\u0301d", 6)
=> "abcéd"

5
投票

Rails 6 将提供 String#truncate_bytes,其行为类似于

truncate
,但采用字节计数而不是字符计数。当然,它返回一个有效的字符串(它不会在多字节字符的中间盲目切割)。

摘自文档:

>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
=> 20
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
=> 80
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
=> "🔪🔪🔪🔪…"

1
投票

这个怎么样:

s = "δogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδogδog"
count = 0
while true
  more_truncate = "a" + (255-count).to_s
  s2 = s.unpack(more_truncate)[0]
  s2.force_encoding 'utf-8'

  if s2[-1].valid_encoding?
    break
  else
    count += 1
  end
end

s2.force_encoding 'utf-8'
puts s2

0
投票

无导轨

Fredrick Cheung 的回答是一个出色的

O(n)
起点,激发了这个
O(log n)
解决方案:

def limit_bytesize(str, max_bytesize)
  return str unless str.bytesize > max_bytesize

  # find the minimum index that exceeds the bytesize, then subtract 1
  just_over = (0...str.size).bsearch { |l| str[0..l].bytesize > max_bytesize }
  str[0..(just_over - 1)]
end

我相信这也实现了该答案中提到的自动

max_bytesize / 4
加速,因为
bsearch
从中间开始。


0
投票

Ruby 的 String#byteslice 可以与范围一起使用。我建议尝试以下方法:

string.bytslice(0...max_bytesize)

这三个点将允许 max_bytesize 值包含在内。


0
投票

我尝试了 Rails 的

String#truncate_bytes
,但发现由于它是迭代实现,它在大随机字符串上速度非常慢。所以我尝试做一些更快的事情,首先快速打破字符串,然后尝试删除末尾不完整的 unicode 的额外字节。结果在我的测试中,即使有多个字节需要删除,
.chop!
也做得很好。我在这里可能错过了一些边缘情况,但您可以尝试这个实现。另外,如果您确实发现了边缘情况,请告诉我:)

# body is a ~2MB random bytes string
# > Benchmark.ms { 100.times { body.truncate_bytes(500_000) } }
# => 13339.49963748455
# > Benchmark.ms { 100.times { str = body.byteslice(0...500_000); str.valid_encoding?; str.chop! } }
# => 167.4932986497879

def self.smart_truncate string, max_bytes: 10
  truncated = string.byteslice(0...max_bytes)
  truncated.chop! if !truncated.valid_encoding?
  return truncated
end
expect(Result.smart_truncate("����������")).to eq("���")
expect(Result.smart_truncate("���AB")).to eq("���A")
expect(Result.smart_truncate("🔪🔪🔪🔪")).to eq("🔪🔪")
© www.soinside.com 2019 - 2024. All rights reserved.