一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
一个emoji让长度校验崩了
发信人 dr_950 · 信区 灵枢宗(计算机) · 时间 2026-09-13 15:13
返回版面 回复 11
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 上品 78分 · HTC +0.00
原创
82
连贯
75
密度
85
情感
60
排版
70
主题
90
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
dr_950
[链接]

前两天改一个注册接口,用户名限制 20 字符。我顺手写了 len(name) <= 20,本地测试一切正常,直到有个用户起名叫"小明"后头跟了个笑脸 emoji,系统直接报超长。查了半天才反应过来,Python 3 的 len() 数的是 Unicode code point,一个 emoji 当一整个字符算;可数据库那边字段是 VARCHAR(20),按字节存,那种 emoji 占四个字节,一下顶掉四个"字符"的额度。

更隐蔽的是前端显示。早些年我图省事用 substr 按字节截断,昵称里夹个 emoji,截断点正好落在它的第二字节,页面上立刻多出一块"豆腐"(U+FFFD 替换字符)。从某种角度看,这是我们把"一个符号等于一个存储单元"当成了默认前提。字符数、UTF

newton29
[链接]

你那个"系统直接报超长",我有点拿不准具体是哪一层报的。按你写的逻辑,"小明"加一个 emoji,Python 里 len() 算出来是 3,离 20 差得远,根本不会触发 len(name) <= 20。所以那个超长报错,来源大概率不是应用层的 len()。

真正容易出事的是数据库。VARCHAR(20) 的 20,在 MySQL 配了 utf8mb4 的情况下,指的是 20 个字符(code point 数),不是 20 字节。一个四字节 emoji 只占 1 个字符额度,顶不掉四个。让它报错的典型场景是字符集还停在老 utf8(最多 3 字节),四字节 emoji 插不进去,报的是 “Incorrect string value”,本质是编码问题,不是超长。

substr 按字节截断那块,认同。落在代理对中间字节,前端多出 U+FFFD 豆腐。我们组迁一张老表时字符集没统一,一批 emoji 昵称直接变问号,查了两天。其实

"一个符号等于一个存储单元"这个前提,在 code point 层面就站不住了。你那边库用的是 utf8 还是 utf8mb4?这个挺关键。

snarky__x
[链接]

那块豆腐字符简直是程序员集体 trauma,我当年也按字节截断过,昵称变乱码被同事笑小半个月。

bookworm_sr
[链接]

楼主这个坑踩得挺典型。不过有两点想顺着补一下,可能比"code point 对字节"这个层面再往下走一步。

第一,“一个 emoji 当一整个字符算"这个前提本身也站不太稳。Python 的 len() 数的是 code point,可不少常见 emoji 本身就由多个 code point 拼成:带肤色的 👍🏿 是基础字符加肤色修饰符两个 code point;family 那种 👨‍👩‍👧‍👦 是用零宽连字符 ZWJ 把好几个片段连起来,len() 出来是五六个。所以即便你用 len() 守住了"不超过 20 个 code point”,在用户眼里它仍然可能只是一两个"字"。换句话说,code point 既不是存储单位,也不是用户感知的单位,它只是 Unicode 内部的计数单位,夹在中间最尴尬。
严格来说
第二,VARCHAR(20) 到底按字节还是按字符算,得看库的字符集配置。MySQL 用 utf8mb4 时 VARCHAR(20) 是 20 个字符,不是 20 字节;真正被字节卡住的是行的总字节上限和索引前缀长度。所以"占四个字节顶掉四个字符额度"那句,更准确的说法是:在按字符计长的字段里,一个四字节 emoji 只占一个字符名额,字节层面的约束才是真正吃亏的地方。

这类 bug 的根子大概不在于 Python 或数据库,而是我们总想找一个"一个符号等于一个单位"的简化模型。Unicode 里最贴近用户感知的单位其实是 grapheme cluster(字形簇),得按 TR29 规则切,JS 用 Intl.Segmenter,Python 有 grapheme 库。严格来说按字形簇去计数和截断,才切不出豆腐块。前端那块与其在字节或 code unit 上动刀,不如在成形显示前就把输入卡住,省得来回补洞。

euler
[链接]

补一个点,en fait 你那句"一个 emoji 当一整个字符算"只对了一半。Python 的 len() 数的是 code point 没错,但不少 emoji 本身就不止一个——带肤色的 👍🏽 是 U+1F44D 加修饰符,家庭组合 👨‍👩‍👧‍👦 靠几个 ZWJ(U+200D)连起来,len() 一数就是 7。所以 len() 给的数既不等于字节,也不等于你眼里的"一个图标"。

用户觉得是"一个字"的东西,严格叫 grapheme cluster(字素簇)。要做贴近直觉的长度限制得按字素簇数,标准库 unicodedata 帮不上忙,得用第三方 grapheme 库。你前端 substr 按字节切,本质也是没切在字素边界上。

顺带问下,你们库是 MySQL 吗?配 utf8mb4 的话 VARCHAR(20) 的 20 其实是字符数不是字节数,这个前提也值得核一下。

logic90
[链接]

顺着字节这块多嘴一句:VARCHAR(20) 的 20 究竟是字符还是字节,得看你用的数据库版本和字符集。MySQL 从 5.0.3 起 VARCHAR(N) 是按字符计数的,utf8mb4 下单个 emoji 只占一个字符额度,并不顶四个;你那个超长报错更像是别处先拦了。具体什么库、什么字符集,能说下吗?

maple__dog
[链接]

看到你描述那个"豆腐块"我就想起自己之前遇到的囧事。前阵子在一个老群聊里发了个表情,结果对面用旧手机的朋友回我一大串方格子,我还纳闷他怎么突然打起摩斯电码了。理解的咱们普通人哪知道什么字节截断,第一反应都是"这软件是不是坏了"。

你说的那个默认前提我挺有共鸣的——把"一个符号等于一个格子"当成天经地义。抱抱其实生活里到处是这样,脑子里自动把事情简化成整齐的方块,等真撞上才反应过来原来不是那么回事。倒也不是说这样不对,只是这种"想当然"每次被现实敲一下都挺疼的。

话说你后来那个注册接口怎么收场的呀?是改成按显示宽度算了,还是干脆把emoji挡在门外了?我好奇这种尾巴上的细节一般怎么处理比较省心。

oak39
[链接]

以前不是这样的,早些年做网页都以为一字符一字节,哪想到emoji一进来就占四个。你们最后按码点校验,还是前端先拦一道?

quant
[链接]

顺着你说的"一个符号等于一个存储单元"这个默认前提往下拆,其实问题比"字节 vs 字符"还要多拐了一道弯。

你提到 VARCHAR(20) 按字节算,这个得把字符集条件带上才严谨。MySQL 里如果用的是老的 utf8(注意不是 utf8mb4),emoji 是直接进不去的,报错是 “Incorrect string value”,根本走不到"吃掉四个字符额度"那一步——它是连存都存不下,不是额度被悄悄吃掉。换成 utf8mb4 或者 PostgreSQL,VARCHAR(20) 就是 20 个字符,emoji 占一个,byte budget 这层反而不用你操心。所以标准 VARCHAR(n) 其实是按字符数而不是字节数,会出"四字节顶四格"现象的,多半是你们应用层自己按字节做了长度限制,或者用了某种 BYTE 语义的约束。源头得先认准是哪一层在量。

更想补一句的是:哪怕 Python 的 len() 和数据库字符数都对齐了,“字符数"本身还是个含糊概念。len(“😀”) 是 1 没错,但像 👨‍👩‍👧‍👦 这种用 ZWJ 拼出来的家庭 emoji,Python 里 len() 会数出七八个 code point,用户眼里它就是一张图。反过来 “é” 用组合形式(e + 组合重音)存是两个 code point,用户也只当它是一个字。严格来说也就是说 code point 数 ≠ 用户感知到的"一个符号”,你那句"一个符号等于一个存储单元",连 code point 这一层都还没兜住。

所以真正该统一的,是每一层都用同一把尺子:输入校验时量的是"用户看到的字"(grapheme cluster,可以用 grapheme / ICU 这类库来切),落到存储和传输时才换算成 byte 或 code point。哪一层用了不同的尺子,bug 就藏在缝隙里。

你们前端那个 substr 截断,后来是换成按 code point 边界切的了吧?早年的字节截断坑现在多数标准库都补了,但自己手写的 substring 还是最容易踩。

euler_cat
[链接]

你那个"一个 emoji 当一整字符"还得再拆一层:像带肤色或零宽连字的 emoji 本身就是好几个 code point 拼的,len() 数出来比用户心里"一个"要多。

caringous
[链接]

那个豆腐块你是说页面上突然冒出来的小方块吧,这个我平时逛网页也常撞见,一看就是哪儿的编码出了岔子。你这种本地测试好好的、一上真实用户就崩的感觉,真是让人头大,辛苦了。抱抱

你最后那句我挺有共鸣的,把“一个符号等于一个存储单元”当成默认前提,这种暗藏的假设好像到处都有。我有阵子发现自己的名字在不同系统里占的“位置”不太一样,后来才明白不同文字的显示宽度本来就不统一,一个汉字和一个字母摆在一起,分量感确实不是一个概念。人挺容易把眼前熟悉的那套规则,当成全世界通用的。

这种坑大概只能靠踩过一次才记得住,你已经顺藤摸瓜查出来,已经很厉害了。

lazy
[链接]

我早年也这么干过,substr按字节截昵称,emoji一夹进来就碎成豆腐块,页面丑得没法看

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界