【问题标题】:Slice a string containing Unicode chars切片包含 Unicode 字符的字符串
【发布时间】:2019-01-29 16:01:43
【问题描述】:

我有一段包含不同字节长度字符的文本。

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() 迭代器并手动遍历所需的子字符串,但有更简洁的方法吗?

【问题讨论】:

  • 我认为chars() 是去这里的方式:text.chars().take(end).skip(start)
  • @TimDiekmann 如果 API 需要,我如何将 Take<Chars> 转换为 &str
  • 您应该致电collect()。看到这个问题stackoverflow.com/questions/37157926/…
  • @ozkriff collect() 将导致String,而不是&str。这就是为什么我没有将此标记为与您的链接问题重复。

标签: string unicode rust slice


【解决方案1】:

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();

【讨论】:

  • 这个函数没有按预期工作;恐慌示例:get_utf8_slice("héllo", 2, 3)。将最后一个范围 [start_pos..end_pos] 替换为 [start_pos..start_pos+end_pos] 可以解决此问题。
【解决方案2】:

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

  • 处理所有边缘情况(空输入、0 宽度输出范围、超出范围的范围);
  • 永远不要惊慌;
  • 使用包含起始、不包含结束的范围。
pub fn utf8_slice(s: &str, start: usize, end: usize) -> Option<&str> {
    let mut iter = s.char_indices()
        .map(|(pos, _)| pos)
        .chain(Some(s.len()))
        .skip(start)
        .peekable();
    let start_pos = *iter.peek()?;
    for _ in start..end { iter.next(); }
    Some(&s[start_pos..*iter.peek()?])
}

【讨论】:

    【解决方案3】:

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

    我知道我可以使用 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..end]);
    

    作为另一种选择,您可以先将字符串收集到Vec&lt;char&gt;。那么,索引就很简单了,但是要打印成字符串,就得重新收集或者写自己的函数来做。

    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 以令人惊讶的方式表现的例子要多得多。有关更多信息和示例,请参阅底部的链接。

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


    有关此主题的更多资源:

    【讨论】:

    • 为了使let end = text.char_indices().map(|(i, _)| i).nth(8).unwrap(); 工作,当我们想通过使用例如 12 作为排除边界来切片到字符串中的最后一个代码点(例如,索引为 11)时,我们需要做更多的工作。可以添加类似let end = if end_codepoint_idx == text.chars().count() {text.len()} else { text.char_indices().map(|(i, _)| i).nth(end_codepoint_idx).unwrap();};
    • unicode-segmentation crate 对"fire" 示例没有帮助——它不会将连字分成两个字符。 (我不是说应该——只是澄清一下,因为这个答案可能会给人不同的印象。)
    猜你喜欢
    • 2013-06-25
    • 2011-03-31
    • 2020-12-19
    • 2020-11-27
    • 1970-01-01
    • 1970-01-01
    • 2016-10-27
    • 1970-01-01
    • 2021-10-27
    相关资源
    最近更新 更多