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

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 फि
http://www.xdnf.cn/news/13565.html

相关文章:

  • 【Linux指南】文件系统基础操作与路径管理
  • 爬虫+动态代理助力 AI 训练数据采集
  • [未验证]abaqus2022 更改内置python
  • 选择与方法(4) 职场内篇 沿着赤道走,到不了北极,找准职场方向,建立可迁移技能
  • 智谱的AI Agent :CoCo
  • GIS数据制备,空间分析与高级建模实践技术应用
  • 软件确认测试报告:如何评估软件功能及测试关键点?
  • 第二届“Parloo”CTF应急响应挑战赛(应急响应题目复盘)
  • ptyhon 导入本地模块 no module named Python Error几种解决方案
  • Excel文件数据的读取和处理方法——C++
  • 华为云Flexus+DeepSeek征文 | 基于华为云ModelArts Studio搭建AnythingLLM聊天助手
  • 支持在Windows电脑上使用的备忘录提醒小软件
  • 【大模型训练】中短序列attention 和MOE层并行方式
  • Java八股文——Spring「SpringBoot 篇」
  • 工业相机如何提高传输速度
  • 【从入门到精通】GIS数据制备,空间分析与高级建模实践应用
  • MySQL主从配置详细指南
  • leetcode 135. 分发糖果
  • 大模型Transformer触顶带来的“热潮退去”,稀疏注意力架构创新或是未来
  • HarmonyOSNext全栈数据存储双星解析:轻量级VS关系型存储终极指南
  • Linux 复制文件到另一个文件夹方法
  • 鹰盾视频加密器播放器Win32系统播放器兼容开发的技术要点与实践指南
  • [Linux入门] Linux安装及管理程序入门指南
  • VUE2个人博客系统
  • 禁止 Windows 更新后自动重启
  • 【鸿蒙表格组件】鸿蒙ArkTS轻量级表格高效渲染组件
  • Android Compose 自定义圆形取色盘
  • vscode 保存 js 时会自动格式化,取消设置也不好使
  • 运维之十个问题--2
  • ​​P值在双侧检验中的计算方法