切片包含Unicode字符的字符串

时间:2018-08-23 09:52:29

标签: string unicode rust slice

我有一段文字,字符的字节长不同。

let text = "Hello привет";

我需要从给定的开始(包括)和结束(不包括)字符索引中提取字符串的一部分。我尝试过了

let slice = &text[start..end];

并出现以下错误

thread 'main' panicked at 'byte index 7 is not a char boundary; it is inside 'п' (bytes 6..8) of `Hello привет`'

我想发生这种情况是因为西里尔字母是多字节的,并且[..]表示法使用 byte 索引接受字符。如果我想使用 character 索引进行切片,就像在Python中一样,该怎么办?

slice = text[start:end]吗?

我知道我可以使用chars()迭代器并手动遍历所需的子字符串,但是还有更简洁的方法吗?

3 个答案:

答案 0 :(得分:24)

可能的代码点切片解决方案

  

我知道我可以使用chars()迭代器并手动遍历所需的子字符串,但是还有更简洁的方法吗?

如果您知道确切的字节索引,则可以对字符串进行切片:

let text = "Hello привет";
println!("{}", &text[2..10]);

这将显示“ lloпр”。因此,问题在于找出确切的字节位置。您可以使用char_indices()迭代器轻松地做到这一点(或者,您可以将chars()char::len_utf8()一起使用)

let text = "Hello привет";
let end = text.char_indices().map(|(i, _)| i).nth(8).unwrap();
println!("{}", &text[2..idx]);

作为另一种选择,您可以首先将字符串收集到Vec<char>中。然后,建立索引很简单,但是要将其打印为字符串,则必须再次收集它或编写自己的函数来进行索引。

let text = "Hello привет";
let text_vec = text.chars().collect::<Vec<_>>();
println!("{}", text_vec[2..8].iter().cloned().collect::<String>());

为什么这样不容易?

如您所见,这些解决方案都不是那么好。这是故意的,原因有两个:

由于str是一个简单的UTF8缓冲区,因此按Unicode代码点进行索引是O(n)操作。通常,人们期望[]运算符是O(1)运算。 Rust使这种运行时复杂性变得明确,并且不会尝试将其隐藏。在以上两种解决方案中,您都可以清楚地看到它不是O(1)。

但更重要的原因:

Unicode代码点通常不是有用的单位

Python的功能(以及您认为想要的功能)并不是那么有用。一切都归结为语言的复杂性,从而也归因于unicode的复杂性。 Python切片Unicode codepoints 。这就是Rust char所代表的。它是32位大(少了几位就足够了,但是我们舍入为2的幂)。

但是您实际上想要做的是切片用户感知的字符。但这是一个明确松散定义的术语。不同的文化和语言将不同的事物视为“一个字符”。最接近的近似是“字素簇”。这样的群集可以由一个或多个unicode码点组成。考虑以下Python 3代码:

>>> s = "Jürgen"
>>> s[0:2]
'Ju'

令人惊讶,对吧?这是因为上面的字符串是:

  • 0x004A拉丁文大写字母J
  • 0x0075拉丁文小写字母U
  • 0x0308结合诊断
  • ...

这是一个合并字符的示例,该字符作为前一个字符的一部分呈现。 Python切片在这里做“错误”的事情。

另一个例子:

>>> s = "fire"
>>> s[0:2]
'fir'

也不是您所期望的。这次,fi实际上是连字,它是一个代码点。

还有更多的示例,其中Unicode表现出令人惊讶的方式。有关更多信息和示例,请参见底部的链接。

因此,如果您想使用应该可以在任何地方使用的国际字符串,请不要进行代码点切片!如果您确实需要在语义上将字符串视为一系列字符,请使用字素簇。为此,板条箱unicode-segmentation非常有用。


有关此主题的其他资源:

答案 1 :(得分:7)

UTF-8编码的字符串可能包含由多个字节组成的字符。在您的情况下,п从索引6(含)开始,到位置8(不含)结束,因此索引7不是字符的开始。这就是您发生错误的原因。

您可以使用str::char_indices来解决这个问题(请记住,到达UTF-8的位置是O(n)):

fn get_utf8_slice(string: &str, start: usize, end: usize) -> Option<&str> {
    assert!(end >= start);
    string.char_indices().nth(start).and_then(|(start_pos, _)| {
        string[start_pos..]
            .char_indices()
            .nth(end - start + 1)
            .map(|(end_pos, _)| &string[start_pos..end_pos])
    })
}

playground

如果可以使用String,则可以使用str::chars()

let string: String = text.chars().take(end).skip(start).collect();

答案 2 :(得分:0)

这是一个检索utf8切片的函数,具有以下优点:

  • 处理所有边缘情况(空输入,0宽度输出范围,范围外范围);
  • 从不惊慌;
  • 使用包含开始和结束的范围。
coord_trans(y = "log10")