【问题标题】:Find longest UTF-8 sequence without breaking multi-byte sequences在不破坏多字节序列的情况下查找最长的 UTF-8 序列
【发布时间】:2019-06-23 13:26:19
【问题描述】:

我需要将 UTF-8 编码的字符串截断为不超过预定义的字节大小。特定协议还要求,截断的字符串仍然形成有效的 UTF-8 编码,即不必拆分多字节序列。

鉴于structure of the UTF-8 encoding,我可以继续前进,计算每个代码点的编码大小,直到达到最大字节数。不过,O(n) 并不是很吸引人。是否有一种算法可以更快地完成,最好在(摊销的)O(1) 时间内完成?

【问题讨论】:

    标签: c++ algorithm utf-8


    【解决方案1】:

    2019 年 6 月 24 日更新: 睡了一夜之后,问题似乎比我第一次尝试看起来要容易得多。由于历史原因,我在下面留下了之前的答案。

    UTF-8 编码为self-synchronizing。这使得可以确定符号流中任意选择的代码单元是否是代码序列的开始。 UTF-8 序列可以拆分到代码序列开头的左侧。

    代码序列的开头可以是 ASCII 字符 (0xxxxxxxb),也可以是多字节序列中的前导字节 (11xxxxxxb)。尾随字节遵循模式10xxxxxxb。 UTF-8 编码的开头满足条件(code_unit & 0b11000000) != 0b10000000,换句话说:它不是尾随字节。

    不超过请求字节数的最长 UTF-8 序列可以通过应用以下算法在恒定时间 (O(1)) 内确定:

    1. 如果输入的长度不超过请求的字节数,则返回实际的字节数。
    2. 否则,循环到开头(从一个代码单元开始超过请求的字节数),直到找到序列的开头。返回序列开头左侧的字节数。

    输入代码:

    #include <string_view>
    
    size_t find_max_utf8_length(std::string_view sv, size_t max_byte_count)
    {
        // 1. Input no longer than max byte count
        if (sv.size() <= max_byte_count)
        {
            return sv.size();
        }
    
        // 2. Input longer than max byte count
        while ((sv[max_byte_count] & 0b11000000) == 0b10000000)
        {
            --max_byte_count;
        }
        return max_byte_count;
    }
    

    这个test code

    #include <iostream>
    #include <iomanip>
    #include <string_view>
    #include <string>
    
    int main()
    {
        using namespace std::literals::string_view_literals;
    
        std::cout << "max size output\n=== ==== ======" << std::endl;
    
        auto test{u8"€«test»"sv};
        for (size_t count{0}; count <= test.size(); ++count)
        {
            auto byte_count{find_max_utf8_length(test, count)};
            std::cout << std::setw(3) << std::setfill(' ') << count
                      << std::setw(5) << std::setfill(' ') << byte_count
                      << " " << std::string(begin(test), byte_count) << std::endl;
        }
    }
    

    产生以下输出:

    max size output
    === ==== ======
      0    0 
      1    0 
      2    0 
      3    3 €
      4    3 €
      5    5 €«
      6    6 €«t
      7    7 €«te
      8    8 €«tes
      9    9 €«test
     10    9 €«test
     11   11 €«test»
    

    此算法仅在 UTF-8 编码上运行。它不会尝试以任何方式处理 Unicode。虽然它总是会产生一个有效的 UTF-8 编码序列,但编码的代码点可能不会形成有意义的 Unicode 字形。

    算法在恒定时间内完成。考虑到每个 UTF-8 编码最多 4 个字节的当前限制,无论输入大小如何,最终循环最多旋转 3 次。该算法将继续工作并在恒定时间内完成,以防 UTF-8 编码被更改为每个编码代码点最多允许 5 或 6 个字节。


    上一个答案

    这可以在 O(1) 中完成,方法是将问题分解为以下几种情况:

    1. 输入的长度不超过请求的字节数。在这种情况下,只需返回输入即可。
    2. 输入长于请求的字节数。在索引max_byte_count - 1 处找出编码中的相对位置:
      1. 如果这是一个 ASCII 字符(最高位未设置 0xxxxxxxb),我们处于自然边界,可以在其后截断字符串。
      2. 否则,我们要么处于多字节序列的开头、中间或尾部。要找出位置,请考虑以下字符。如果它是 ASCII 字符 (0xxxxxxxb) 或多字节序列的开头 (11xxxxxxb),则我们位于多字节序列的尾部,即自然边界。
      3. 否则,我们要么处于多字节序列的开头或中间。迭代到字符串的开头,直到我们找到多字节编码的开头 (11xxxxxxb)。剪切该字符之前的字符串。

    以下代码在给定最大字节数的情况下计算截断字符串的长度。输入需要形成有效的 UTF-8 编码。

    #include <string_view>
    
    size_t find_max_utf8_length(std::string_view sv, size_t max_byte_count)
    {
        // 1. No longer than max byte count
        if (sv.size() <= max_byte_count)
        {
            return sv.size();
        }
    
        // 2. Longer than byte count
        auto c0{static_cast<unsigned char>(sv[max_byte_count - 1])};
        if ((c0 & 0b10000000) == 0)
        {
            // 2.1 ASCII
            return max_byte_count;
        }
    
        auto c1{static_cast<unsigned char>(sv[max_byte_count])};
        if (((c1 & 0b10000000) == 0) || ((c1 & 0b11000000) == 0b11000000))
        {
            // 2.2. At end of multi-byte sequence
            return max_byte_count;
        }
    
        // 2.3. At start or middle of multi-byte sequence
        unsigned char c{};
        do
        {
            --max_byte_count;
            c = static_cast<unsigned char>(sv[max_byte_count]);
        } while ((c & 0b11000000) != 0b11000000);
        return max_byte_count;
    }
    

    以下测试代码

    #include <iostream>
    #include <iomanip>
    #include <string_view>
    #include <string>
    
    int main()
    {
        using namespace std::literals::string_view_literals;
    
        std::cout << "max size output\n=== ==== ======" << std::endl;
    
        auto test{u8"€«test»"sv};
        for (size_t count{0}; count <= test.size(); ++count)
        {
            auto byte_count{find_max_utf8_length(test, count)};
            std::cout << std::setw(3) << std::setfill(' ') << count
                      << std::setw(5) << std::setfill(' ') << byte_count
                      << " " << std::string(begin(test), byte_count) << std::endl;
        }
    }
    

    产生this output:

    max size output
    === ==== ======
      0    0 
      1    0 
      2    0 
      3    3 €
      4    3 €
      5    5 €«
      6    6 €«t
      7    7 €«te
      8    8 €«tes
      9    9 €«test
     10    9 €«test
     11   11 €«test»
    

    【讨论】:

    • @lig:我认为这也很有用。甚至如此有用,以至于我确信,某人,某处必须以前写过这个。我从字面上搜索了几天,没有找到类似的东西。随着关于 UTF-8 自同步的大肆宣传,人们希望轻松找到利用此属性的实现。然而我什么也没找到。很高兴知道,这似乎对其他人有用,谢谢。
    • @IInspectable:你看不到这段代码的原因是你通常不需要它。在任意代码单元边界上拆分字符串(您必须给我不超过 X 个代码单元,但我需要它们来编码完整的代码点)在基本的 Unicode 处理方面并不是非常有用。您通常需要在代码点或字素簇边界上拆分字符串,这具有完全不同的处理要求。在 Unicode 处理之外,处理 UTF-8 文本通常是通过复制来完成的。
    • @nic:我不同意这种观点,即通常不需要这样做。每当您处理协议时,该协议以最大数据包大小打包信息单元,如果您可以建立单个数据包的编码有效性,它会大大简化处理。 Unicode 处理具有足够的挑战性;在混合中添加瞬态解码状态肯定不会让您的网络客户端变得更简单。你是对的,不过,我应该补充一点,这个实现对 Unicode 处理没有任何作用。
    • @IInspectable:为什么协议需要在数据包级别验证 UTF-8 序列的有效性?在更高的层次上,当事情组装好时,检查是有意义的。但是数据包级别的验证类似于 CRC 检查是否损坏,而不是 UTF-8 编码检查。 UTF-8 编码失败不是数据包或网络传输问题;这是一个“发件人不知道在数据包中放入什么”的问题。所以它应该在网络层之外处理。而且,如果您确实在数据包级别检测到它,则发送者无法在数据包级别修复它。
    • @nic:确实,协议不应该验证任何东西。它应该简单地要求编码有效性。如果是这样,本问答将解释如何编写制片人。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-05
    • 2011-01-26
    • 2021-05-18
    相关资源
    最近更新 更多