当前位置: 首页 > news >正文

Unicode:如何让用户东方不败和[Family: Man, Woman, Girl, Boy]顺利通过用户名长度检查?

给你这样一个需求:实现一个函数,我们将用这个函数来计算用户在注册账号时输入的用户名长度,当用户名长度超过限制5时,我们会阻止用户使用这个用户名。函数输入为UTF-8编码的std::string

这听起来似乎很容易,一行代码就能够搞定:

int stringLength(const std::string& s) { return s.size(); }

注册我们账号的第一位用户叫做Mary,stringLength不负众望地给我们返回了4,这让她通过了用户名长度检测,顺利注册了账号。

第二位用户叫做东方不败,他的名字看起来也只有4个字,应该也能够顺利通过用户名长度检测。然而事与愿违,用户界面提示用户名超长,通过一番调试,我们发现stringLength返回了12。

虽然初看起来有点奇怪,但是作为一个程序员,经过短暂的思考以后,我们不难弄清楚,一个英文字母和一个汉字虽然都对应一个Unicode code point,但是在使用UTF-8编码时,英文字母和汉字由于code point的范围不同,它们的“地位”是不同的。

First code pointLast code pointByte 1Byte 2Byte 3Byte 4
U+0000U+007F0yyyzzzz
U+0080U+07FF110xxxyy10yyzzzz
U+0800U+FFFF1110wwww10xxxxyy10yyzzzz
U+010000U+10FFFF11110uvv10vvwwww10xxxxyy10yyzzzz

之所以会得到12,是因为常用汉字的Unicode Block “CJK Unified Ideographs” code point范围为U+4E00~U+9FFF,使用UTF-8编码时占3个字节,3x4=12:

东方不败
U+4E1C U+65B9 U+4E0D U+8D25
0xE4 0xB8 0x9C 0xE6 0x96 0xB9 0xE4 0xB8 0x8D 0xE8 0xB4 0xA5

但是作为一名对Unicode/UTF-8一窍不通的普通用户,东方不败大概不会满意我们的解释,他只知道自己心爱的名字“东方不败”4个字因为超出长度限制5而无法注册,这太荒谬了。为了让这名用户满意,我们需要实现一个stringLength_v2来纠正这个问题。

stringLength_v2中,std::string::size将派不上用场,它代表的是字符串占用了几个字节,而不是里面有几个文字。由于C++标准库中并没有可靠的Unicode编解码接口,此时我们最好依赖三方库,使用icu将UTF-8编码的std::string转换成icu::UnicodeString,然后“数一数”里面到底有几个code point,按我们目前的理解,一个code point就是一个字,有几个code point就是有几个字。

int stringLength_v2(const std::string& s) {icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);int32_t length = 0;for (int32_t i = 0; i < ustr.length();) {UChar32 c;U16_NEXT(ustr.getBuffer(), i, ustr.length(), c);if (c >= 0) {++length;}}return length;
}

需要注意的是,icu::UnicodeString内部使用UTF-16存储数据,因此ustr.length()并不是code point的个数,我们必须自己来数。

将“东方不败”4个字传给stringLength_v2以后,我们如愿得到了4,用户东方不败也顺利注册了自己的账号。

又来了一位用户,他的名字叫做 👨‍👩‍👧‍👦,你不要觉得奇怪,这个世界发展很快,也许以后每个人都会把emoji放进自己的名字,毕竟emoji也是Unicode,而且还是很酷的Unicode。这个用户名看起来只有一个“字”,但他也没能顺利地通过用户名长度检测,我们惊奇地发现,stringLength_v2给我们返回了7。

这次又是哪里弄错了呢?stringLength_v2数出来有7个code point,但是这个用户的名字看起来只有一个“字”。把7个code point都打印出来,它们分别是:

👨‍👩‍👧‍👦
U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466

他的名字确实有7个code point,但显示时又只占据了一个字符的空间。经过一番学习,我们又了解到了grapheme cluster和combining character的概念,简而言之,用户看到的一个“字”,并不对应着一个code point,而是对应着一个grapheme cluster。而一个grapheme cluster可能是由多个code point组成的。除了emoji,还有下面这些例子,它们都是包含combining character的grapheme cluster,我们可以使用C++的\u转义来构造它们:

"\u092b\u093f" -> फि
"\u0061\u0308\u0303\u0323\u032d" -> ạ̭̈̃
"\u0e02\u0e36\u0e49" -> ขึ้

为了让用户👨‍👩‍👧‍👦能够顺利注册账号,我们需要再实现一个stringLength_v3,这个版本不再计算code point的个数,而是计算grapheme cluster的个数,它才是用户看到的字符的个数。

int stringLength_v3(const std::string& s) {UErrorCode status = U_ZERO_ERROR;icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);std::unique_ptr<icu::BreakIterator> bi(icu::BreakIterator::createCharacterInstance(icu::Locale::getDefault(),status));if (U_FAILURE(status) || !bi) {return -1;}bi->setText(ustr);int count = 0;for (int32_t start = bi->first(), end = bi->next();end != icu::BreakIterator::DONE; start = end, end = bi->next()) {++count;}return count;
}

stringLength_v3的帮助下,用户👨‍👩‍👧‍👦成功注册了账号,他对stringLength_v3表示了感谢。不过需要注意的是,Unicode并没有规定一个grapheme cluster中code point数量的上限,恶意用户完全可以构造一个超长的单一grapheme cluster来消耗我们宝贵的资源,因此有必要为s.size()也加上一个限制,比如说64。如果真的有善良的用户通过了stringLength_v3考验,但是没能通过s.size()的考验,我们可以考虑把他介绍给我们的竞争对手,他们拥有丰富的资源。

stringLength_v3让所有用户顺利注册了账号,皆大欢喜的结局!


一些有用的URL:

  1. https://www.compart.com/en/unicode/plane/U+0000: 拥有整套的Unicode数据集,可以让你获取到每个Unicode code point你想了解的所有信息
  2. https://www.compart.com/en/unicode/block/U+4E00: Unicode Block “CJK Unified Ideographs”
  3. https://home.unicode.org/: Unicode官网
  4. https://www.unicode.org/versions/latest/: Unicode标准最新版本
  5. https://unicode.org/emoji/charts/full-emoji-list.html: Emoji全集
  6. https://icu.unicode.org/: icu官网
  7. https://en.wikipedia.org/wiki/Unicode: Unicode维基百科
  8. https://en.wikipedia.org/wiki/UTF-8: UTF-8维基百科

最后附上完整的代码和输出。

代码

#include <unicode/brkiter.h>
#include <unicode/locid.h>
#include <unicode/unistr.h>
#include <unicode/utypes.h>#include <iostream>
#include <string>
#include <vector>int stringLength(const std::string& s) { return s.size(); }int stringLength_v2(const std::string& s) {icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);int32_t length = 0;for (int32_t i = 0; i < ustr.length();) {UChar32 c;U16_NEXT(ustr.getBuffer(), i, ustr.length(), c);if (c >= 0) {++length;}}return length;
}int stringLength_v3(const std::string& s) {UErrorCode status = U_ZERO_ERROR;icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);std::unique_ptr<icu::BreakIterator> bi(icu::BreakIterator::createCharacterInstance(icu::Locale::getDefault(),status));if (U_FAILURE(status) || !bi) {return -1;}bi->setText(ustr);int count = 0;for (int32_t start = bi->first(), end = bi->next();end != icu::BreakIterator::DONE; start = end, end = bi->next()) {++count;}return count;
}int main() {std::vector<std::string> names = {"Mary","东方不败","👨‍👩‍👧‍👦","\u0061\u0308\u0303\u0323\u032d","\u0e02\u0e36\u0e49","\u092b\u093f"};for (const std::string& name : names) {printf("%2d %2d %2d %s\n", stringLength(name), stringLength_v2(name),stringLength_v3(name), name.c_str());}return 0;
}

编译

g++ -licuuc main.cpp

输出

 4  4  4 Mary
12  4  4 东方不败
25  7  1 👨‍👩‍👧‍👦9  5  1 ạ̭̈̃9  3  1 ขึ้6  2  1 फि

相关文章:

  • 从字节到对象的漂流---JavaIO流篇
  • (46)课68:查看索引 SHOW INDEX FROM 表名;删除索引 DROP INDEX index_name ON 表名;
  • 青藏高原ASTER_GDEM数据集(2011)
  • Office 365下载安装教程(超详细图文教程)从零开始的完整安装指南
  • Nuttx之mm_extend
  • ISO/IEC 14443 防碰撞协议 Type A Type B
  • NIFI在Linux系统中的系统配置最佳实践(性能调优)
  • Shuffle流程
  • 【Linux系统编程】System V
  • 大模型呼叫系统——重塑学校招生问答,提升服务效能
  • 离线部署openstack 2024.1 neutron
  • 曼昆《经济学原理》第九版 第十八章生产要素市场
  • 离线部署openstack 2024.1 nova
  • 火山引擎大模型系列都有什么内容
  • Java高频面试之并发编程-27
  • Ubuntu24.04 onnx 模型转 rknn
  • 大语言模型智能体开发的技术框架与应用前景
  • 频域分析和注意力机制
  • 华测CGI-430配置
  • 离线部署openstack 2024.1 keystone
  • 深圳专业网站建设多少钱/世界排名前十位
  • 桂阳网站开发/网站代运营多少钱一个月
  • 仅有网站做app/微信怎么推广找客源
  • 一级a做爰片2202网站/内蒙古最新消息
  • 新闻发布会稿件/青岛网站seo诊断
  • 常见的网站建设技术有哪些/seo合作