【问题标题】:Getting the actual length of a UTF-8 encoded std::string?获取 UTF-8 编码的 std::string 的实际长度?
【发布时间】:2011-05-03 01:34:12
【问题描述】:

我的 std::string 是 utf-8 编码的,很明显,str.length() 返回错误的结果。

我找到了此信息,但我不确定如何使用它来执行此操作:

以下字节序列是 用来表示一个字符。这 顺序是 使用取决于字符的 UCS 码数:

   0x00000000 - 0x0000007F:
       0xxxxxxx

   0x00000080 - 0x000007FF:
       110xxxxx 10xxxxxx

   0x00000800 - 0x0000FFFF:
       1110xxxx 10xxxxxx 10xxxxxx

   0x00010000 - 0x001FFFFF:
       11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

如何找到 UTF-8 编码的 std::string 的实际长度?谢谢

【问题讨论】:

  • C++ 对编码一无所知,因此您不能指望使用标准函数来执行此操作。如果您不想从头开始编写一个,某些操作系统(例如 Windows)可能会提供帮助解决此问题的功能。
  • 我明白这就是为什么我也标记了这个算法,我确实想从头开始写一个
  • 请注意,虽然 Michael 的声明在他编写时是正确的,但从 C++11 开始,标准库确实了解编码。见stackoverflow.com/questions/16863937/…
  • “实际长度”是什么意思?码点数?在 NFC、NFD、NFKC 或 NFKD 中?复合字符数?字素的数量?字素簇的数量?屏幕上给定字体的像素数?
  • 确实,这里的大部分答案都假设“实际长度”是指“代码点数”。

标签: c++ algorithm


【解决方案1】:

计算所有首字节(与 10xxxxxx 不匹配的字节)。

int len = 0;
while (*s) len += (*s++ & 0xc0) != 0x80;

【讨论】:

  • 你需要在& 部分加上括号。
  • 请注意,这将返回代码点的数量 - 假设这是“实际长度”。另外:仅在验证为有效 UTF-8 序列的字符串上使用此算法,因为它不会考虑可能导致错误结果的无效字节序列。
  • 这将是代码点的长度,而不是字形。对于估计屏幕空间,即使是固定长度的字体,这也是不够的。此外,它不适用于计算相应 UTF-16 缓冲区的大小,尽管只要您停留在基本多语言平面,这将起作用(但要注意那些讨厌的表情符号)。
  • 对于那些试图在命令行中对齐项目(使用等宽字体)的人来说,这对于 unicode 范围 1F300–1F5FF 中的符号在打印时将无法正常工作作为 2 个字符长的符号
【解决方案2】:

C++ 对编码一无所知,所以你不能期望使用 执行此操作的标准函数。

标准库确实确实以语言环境的形式承认字符编码的存在。如果您的系统支持语言环境,则使用标准库来计算字符串的长度非常容易。在下面的示例代码中,我假设您的系统支持语言环境 en_US.utf8。如果我编译代码并将其作为“./a.out ソニーSony”执行,则输出是 13 个字符值和 7 个字符。并且所有这些都没有对 UTF-8 字符代码的内部表示进行任何引用,也无需使用 3rd 方库。

#include <clocale>
#include <cstdlib>
#include <iostream>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
  string str(argv[1]);
  unsigned int strLen = str.length();
  cout << "Length (char-values): " << strLen << '\n';
  setlocale(LC_ALL, "en_US.utf8");
  unsigned int u = 0;
  const char *c_str = str.c_str();
  unsigned int charCount = 0;
  while(u < strLen)
  {
    u += mblen(&c_str[u], strLen - u);
    charCount += 1;
  }
  cout << "Length (characters): " << charCount << endl; 
}

【讨论】:

  • 对,如果你想安全的话,请使用 std::mbrlen(ish)
  • 您确定区域名称吗?在 ubuntu 上,正确的应该是“en_US.utf8”。
  • @user2781185,您是否打算将代码更改为“en_US.utf8”以响应 qed 的评论?因为我在上面的代码中仍然看到“en_US.UTF-8”。
  • 嗯,我确实认为我已经改变了它......但让我们再试一次。希望它现在是正确的。
  • 您能否解释一下为什么它会在 Windows 上为该字符串返回 5(字符):std::string str(u8"Fünf");?我希望得到 4,当我在 Linux (gcc) 上运行相同的代码时也是如此。
【解决方案3】:

这是一个幼稚的实现,但它应该有助于您了解它是如何完成的:

std::size_t utf8_length(std::string const &s) {
  std::size_t len = 0;
  std::string::const_iterator begin = s.begin(), end = s.end();
  while (begin != end) {
    unsigned char c = *begin;
    int n;
    if      ((c & 0x80) == 0)    n = 1;
    else if ((c & 0xE0) == 0xC0) n = 2;
    else if ((c & 0xF0) == 0xE0) n = 3;
    else if ((c & 0xF8) == 0xF0) n = 4;
    else throw std::runtime_error("utf8_length: invalid UTF-8");

    if (end - begin < n) {
      throw std::runtime_error("utf8_length: string too short");
    }
    for (int i = 1; i < n; ++i) {
      if ((begin[i] & 0xC0) != 0x80) {
        throw std::runtime_error("utf8_length: expected continuation byte");
      }
    }
    len += n;
    begin += n;
  }
  return len;
}

【讨论】:

    【解决方案4】:

    您可能应该听取 Omry 的建议,并为此查看专门的库。也就是说,如果您只是想了解执行此操作的算法,我将在下面发布。

    基本上,您可以将字符串转换为更宽的元素格式,例如wchar_t。请注意wchar_t 有一些可移植性问题,因为wchar_t 的大小取决于您的平台。在 Windows 上,wchar_t 是 2 个字节,因此非常适合表示 UTF-16。但在 UNIX/Linux 上,它是四字节的,因此用于表示 UTF-32。因此,对于 Windows,这仅在您不包含任何高于 0xFFFF 的 Unicode 代码点时才有效。对于 Linux,您可以在 wchar_t 中包含整个代码点范围。 (幸运的是,这个问题将通过 C++0x Unicode 字符类型得到缓解。)

    注意这一点,您可以使用以下算法创建转换函数:

    template <class OutputIterator>
    inline OutputIterator convert(const unsigned char* it, const unsigned char* end, OutputIterator out) 
    {
        while (it != end) 
        {
            if (*it < 192) *out++ = *it++; // single byte character
            else if (*it < 224 && it + 1 < end && *(it+1) > 127) { 
                // double byte character
                *out++ = ((*it & 0x1F) << 6) | (*(it+1) & 0x3F);
                it += 2;
            }
            else if (*it < 240 && it + 2 < end && *(it+1) > 127 && *(it+2) > 127) { 
                // triple byte character
                *out++ = ((*it & 0x0F) << 12) | ((*(it+1) & 0x3F) << 6) | (*(it+2) & 0x3F);
                it += 3;
            }
            else if (*it < 248 && it + 3 < end && *(it+1) > 127 && *(it+2) > 127 && *(it+3) > 127) { 
                // 4-byte character
                *out++ = ((*it & 0x07) << 18) | ((*(it+1) & 0x3F) << 12) |
                    ((*(it+2) & 0x3F) << 6) | (*(it+3) & 0x3F);
                it += 4;
            }
            else ++it; // Invalid byte sequence (throw an exception here if you want)
        }
    
        return out;
    }
    
    int main()
    {
        std::string s = "\u00EAtre";
        cout << s.length() << endl;
    
        std::wstring output;
        convert(reinterpret_cast<const unsigned char*> (s.c_str()), 
            reinterpret_cast<const unsigned char*>(s.c_str()) + s.length(), std::back_inserter(output));
    
        cout << output.length() << endl; // Actual length
    }
    

    该算法不是完全通用的,因为 InputIterator 需要是一个无符号字符,因此您可以将每个字节解释为具有 0 到 0xFF 之间的值。 OutputIterator 是通用的,(只是为了您可以使用 std::back_inserter 而不必担心内存分配),但它作为通用参数的使用是有限的:基本上,它必须输出到一个足够大的元素数组来表示UTF-16 或 UTF-32 字符,例如 wchar_tuint32_t 或 C++0x char32_t 类型。另外,我没有包含用于转换大于 4 字节的字符字节序列的代码,但是您应该从发布的内容中了解算法的工作原理。

    另外,如果您只想计算字符数,而不是输出到新的宽字符缓冲区,您可以修改算法以包含计数器而不是 OutputIterator。或者更好的是,只需使用Marcelo Cantos' answer 来计算第一个字节。

    【讨论】:

    • 在吹毛求疵的前面,是什么让您认为“être”字符串将使用 UTF8 编码?我认为在 C/C++ 中在源代码中使用非 ascii 是不标准的(事实上,一些编译器会选择另一种编码)。
    • @Bahbar,说得好。它实际上应该使用\u 十六进制表示法。
    【解决方案5】:

    我建议您使用UTF8-CPP。它是一个仅用于在 C++ 中使用 UTF-8 的标头库。使用这个库,它看起来像这样:

    int LenghtOfUtf8String( const std::string &utf8_string ) 
    {
        return utf8::distance( utf8_string.begin(), utf8_string.end() ); 
    }
    

    (代码来自我的脑海。)

    【讨论】:

    • 是跨平台解决方案吗?
    • 快速浏览链接会发现:“该库的设计目的是:...可移植:...跨不同的平台和编译器。唯一不可移植的代码是声明的一小部分不同大小的无符号整数:三个 typedef。...可以更改...如果它们与 [the] 平台不匹配。默认值...应该适用于 Windows(32 位和 64 位)和大多数 32 位和 64 位 Unix 衍生产品。”
    【解决方案6】:

    我的大部分个人 C 库代码只用英语进行了真正的测试,但这里是我实现 utf-8 字符串长度函数的方式。我最初基于this wiki page table 中描述的位模式。现在这不是最易读的代码,但我确实更喜欢我的编译器中的benchmark。也很抱歉这是 C 代码,它应该很容易转换为 C++ 中的 std::string 虽然稍作修改:)。

    size_t utf8len(const char* const str)
    {
        size_t len = 0;
        unsigned char c = str[0];
        for (size_t i = 1; c != 0; ++len, ++i)
        {
            if ((c & 0x80))
            {
                if (c < 0xC0)   // Invalid increment
                    return 0;
                c >>= 4;
                if (c == 12)
                    c++;
                i += c - 12;
            }
            c = str[i];
        }
        return len;
    }
    

    请注意,这不会验证任何字节(就像这里所有其他建议的答案一样)。就我个人而言,我会将字符串验证从我的字符串长度函数中分离出来,因为这不是它的责任。如果我们要将字符串验证移至另一个函数,我们可以进行如下验证。

    bool utf8valid(const char* const str)
    {
        if (str == NULL)
            return false;
        unsigned char c = str[0];
        for (size_t i = 1, inc = 0; c != 0; ++i)
        {
            if (inc > 1)
            {
                if ((c & 0xC0) != 0x80)
                    return false;
                inc--;
            }
            else
            {
                inc = 1;
                if ((c & 0x80))
                {
                    if (c < 0xC0 || c >= 0xF8)
                        return false;
                    c >>= 4;
                    if (c == 12)
                        c++;
                    inc += c - 12;
                }
            }
            c = str[i];
        }
        return true;
    }
    

    如果您追求可读性,我承认其他建议更具可读性哈哈!

    【讨论】:

      【解决方案7】:

      尝试使用像iconv 这样的编码库。 它可能得到了你想要的 api。

      另一种方法是实现您自己的 utf8strlen,它确定每个代码点的长度并迭代代码点而不是字符。

      【讨论】:

        【解决方案8】:

        一个稍微懒惰的方法是只计算前导字节,但访问每个字节。这节省了解码各种前导字节大小的复杂性,但显然您需要支付访问所有字节的费用,尽管通常没有那么多(2x-3x):

        size_t utf8Len(std::string s)
        {
          return std::count_if(s.begin(), s.end(),
            [](char c) { return (static_cast<unsigned char>(c) & 0xC0) != 0x80; } );
        }
        

        请注意,某些代码值作为前导字节是非法的,例如,那些表示比扩展​​ unicode 所需的 20 位更大的值的值,但是另一种方法无论如何都不知道如何处理该代码。

        【讨论】:

          【解决方案9】:

          UTF-8 CPP 库有一个功能可以做到这一点。您可以将库包含到您的项目中(它很小),也可以只查看函数。 http://utfcpp.sourceforge.net/

          char* twochars = "\xe6\x97\xa5\xd1\x88";
          size_t dist = utf8::distance(twochars, twochars + 5);
          assert (dist == 2);
          

          【讨论】:

            【解决方案10】:

            这段代码我从php-iconv移植到c++,你需要先使用iconv,希望有用:

            // porting from PHP
            // http://lxr.php.net/xref/PHP_5_4/ext/iconv/iconv.c#_php_iconv_strlen
            #define GENERIC_SUPERSET_NBYTES 4
            #define GENERIC_SUPERSET_NAME   "UCS-4LE"
            
            UInt32 iconvStrlen(const char *str, size_t nbytes, const char* encode)
            {
                UInt32 retVal = (unsigned int)-1;
            
                unsigned int cnt = 0;
            
                iconv_t cd = iconv_open(GENERIC_SUPERSET_NAME, encode);
                if (cd == (iconv_t)(-1))
                    return retVal;
            
                const char* in;
                size_t  inLeft;
            
                char *out;
                size_t outLeft;
            
                char buf[GENERIC_SUPERSET_NBYTES * 2] = {0};
            
                for (in = str, inLeft = nbytes, cnt = 0; inLeft > 0; cnt += 2) 
                {
                    size_t prev_in_left;
                    out = buf;
                    outLeft = sizeof(buf);
            
                    prev_in_left = inLeft;
            
                    if (iconv(cd, &in, &inLeft, (char **) &out, &outLeft) == (size_t)-1) {
                        if (prev_in_left == inLeft) {
                            break;
                        }
                    }
                }
                iconv_close(cd);
            
                if (outLeft > 0)
                    cnt -= outLeft / GENERIC_SUPERSET_NBYTES;
            
                retVal = cnt;
                return retVal;
            }
            
            UInt32 utf8StrLen(const std::string& src)
            {
                return iconvStrlen(src.c_str(), src.length(), "UTF-8");
            }
            

            【讨论】:

              【解决方案11】:

              另一个简单的实现来计算 UTF-8 字符串中的字符

              int utf8_strlen(const string& str)
              {
                  int c,i,ix,q;
                  for (q=0, i=0, ix=str.length(); i < ix; i++, q++)
                  {
                      c = (unsigned char) str[i];
                      if      (c>=0   && c<=127) i+=0;
                      else if ((c & 0xE0) == 0xC0) i+=1;
                      else if ((c & 0xF0) == 0xE0) i+=2;
                      else if ((c & 0xF8) == 0xF0) i+=3;
                      //else if (($c & 0xFC) == 0xF8) i+=4; // 111110bb //byte 5, unnecessary in 4 byte UTF-8
                      //else if (($c & 0xFE) == 0xFC) i+=5; // 1111110b //byte 6, unnecessary in 4 byte UTF-8
                      else return 0;//invalid utf8
                  }
                  return q;
              }
              

              【讨论】:

                猜你喜欢
                • 2013-09-11
                • 1970-01-01
                • 2013-09-26
                • 1970-01-01
                • 2018-01-16
                • 1970-01-01
                • 1970-01-01
                • 2012-01-20
                相关资源
                最近更新 更多