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

昨天线上炸了一回,排查半天才发现凶手是个笑脸emoji,离谱。就这?

事情是这样的,我们接口对用户输入做了个长度校验加截断,逻辑写得挺自信:超长就substring截掉。结果有个用户在昵称里塞了个笑哭的表情,前端显示好好的,一到后端某个拿字节长度判断的地方,直接把一个汉字切成两半,返回一堆乱码,下游解析直接挂。

你说怪不怪,肉眼看就一个表情,length()一算告诉你这是俩字符。emoji大多是代理对,再不济也是多码点组合,按码元数去数根本不靠谱。我后来把截断逻辑全改成按code point走,校验长度也用码点算,才算消停。

我去说真的,字符串这玩意儿真不能想当然。中文按字节截会出半个字,emoji按length()数会多数一倍,老老实实用码点或字素簇处理才是正道。你们踩过类似的坑没,评论区聊聊。

penguin__us
[链接]

前端显示好好的后端直接炸,经典背锅侠 emoji。我之前也踩过这坑,按字节截断用户名字,一个中文没注意咔嚓切一半,页面全是方块,我还以为字体坏了,瞎折腾一通才反应过来是截断的锅。嘿嘿还是按码点走踏实,早改早安心。

phd
[链接]

顺着你末尾那句想补一句:码点能解决代理对被拆成两半的问题,但码点本身还不是“用户眼里一个字”的最小单位。像国旗那类表情是两个区域指示符码点拼出来的,家庭合影那种是多个码点加零宽连字(ZWJ)连写,按码点截断照样能把一个表情拆散。真要严格按“显示出来算一个字符”来切,得上字素簇(grapheme cluster),JS 里现在有 Intl.Segmenter 能干这事。其实

你开头说 length() 多数一倍,这也不是定数,ZWJ 序列一串能多数好几倍。所以“码点或字素簇才是正道”里,码点算够用,字素簇才真正对齐用户认知。你们后来截断用的是哪一层?

brainy__16
[链接]

其实按 code point 走只能算解决了截断那一半。你们乱码是因为码元截断把字符切两半了,换码点确实修好了截断。

但「校验长度也用码点算」这点还差一步:不少 emoji 一个肉眼字其实是多个 code point 拼出来的。带肤色的 👍🏽 是 U+1F44D 加 U+1F3FD,家庭那种 ZWJ 序列能串七八个码点。你按码点数,数出来照样比用户看到的「字数」多。

要真贴近用户直觉去限长,得按 grapheme cluster 来分,现在 JS 里可以用 Intl.Segmenter 做分段。truncate 用码点、count 用字素簇,两件事别混成一桩。

newton
[链接]

楼主把"凶手"归到emoji头上,这个归因其实可以再掰扯一下。严格说,炸掉接口的并不是那只笑脸,而是后端那处"拿字节长度判断"的逻辑本身就有问题——只要输入里混进任意一个多字节字符,中文也好、emoji也好,字节截断都可能在字符中间切一刀,产出非法字节序列。嗯emoji只是恰好把总长度顶过了截断阈值,让切口落到了多字节序列当中。换句话说,它是导火索,不是炸药。

再说"emoji大多是代理对"这个说法,稍微不够严谨。严格来说UTF-16里的代理对只覆盖基本多文种平面之外的码点,比如U+1F600那一批绘文字;可早期一批符号类emoji像U+263A、U+2665、U+27A1都还在BMP里,根本不走代理对。更麻烦的是ZWJ序列和组合附加符:国旗实际是两个区域指示符码点(U+1F1E8、U+1F1F3)拼出来的,家庭组合类靠零宽连接符串起好几个码点,肤色修饰符也是叠在基础码点上的附加符。

顺着这点我想补一层:改用码点计数固然比按码元数或字节数靠谱,但它依然不是"用户眼里的字符数"。同一个é,可以是U+00E9一个码点,也可以是e加组合重音两个码点——显示出来一模一样,码点计数却差了一倍。所以"按码点截断"能保你不切坏字节、不拆散代理对,却仍可能把国旗或一组组合字素从中间腰斩。真要做到显示层截断不出怪相,得用字素簇去切,JS里可以上Intl.Segmenter,服务端一般靠ICU这类库。

往深里看,这回事故的根子其实是各层对"一个字符算多长"各说各话。浏览器里JS的length按UTF-16码元数,Java的length()也是码元数,Python3的len()按码点,线上跑的字节又是UTF-8。同一个昵称,前端数出来8,后端数出来9,落库又变成20字节——emoji不过是让这几套账对不上的时候炸得最狠。所以除了把截断逻辑改成码点,更紧要的是在接口边界把"长度按什么单位算"定死,下游别再用另一套单位重新量一遍。

你们后端那块后来是只在入库前改了,还是连同返回给前端的字段也一起统一了?这个我有点好奇。

melody
[链接]

读到你说的那个被切成两半的汉字,心里忽然紧了一下。我们总以为看见的就是全部,一个笑哭的表情安安静静躺在昵称里,谁会想到底下还藏着代理对、码点这些看不见的骨骼。

想起小时候临帖,一笔没写完就搁笔,纸上留个半截的偏旁,怎么看都像句没说完的话。你那个乱码大概也是这滋味,明明想好好说一句,传到对面却成了谁也认不出的残片。

现在你们改成按码点走,听起来倒像在教人慢一点,把东西看清楚了再下手。

angel2002
[链接]

辛苦了,排查这种暗坑最磨人了。理解的我之前有回在某个网页填资料,昵称里顺手加了个表情,提交完名字直接变成一排小方块,还以为是那边服务器抽风了,后来才反应过来是字符没兜住。

楼主说的"肉眼看着好好的,后端却把字切半"真的让人后背一凉,本当に油断できないですね。现在昵称里表情包、特殊符号混着用,这种细节确实得按码点处理才稳妥。你们最后是把所有截断的地方都改了一遍,还是只动了昵称那块呀?

real66
[链接]

这事我居然也有点同感,前阵子捣鼓点东西就被半个汉字坑过。emmm说真的你们那个拿字节数判断的逻辑放现在简直在雷区蹦迪,中文叠emoji混着来,截断那一下不炸才怪。

楼主后半段说到点上了,length()这个数法就是个温柔陷阱,它告诉你的是码元数又不是你眼睛看到的东西。好多人写到位就觉得稳了,结果emoji一个代理对悄悄把数翻倍,下游直接傻眼。
笑死
不过我有点好奇,前端显示正常后端炸,是不是中间哪环编码没统一?光把截断改成按码点,我担心病根是GBK跟UTF

aurora_dog
[链接]

读到那个被生生切成两半的汉字,心里莫名咯噔一下。屏幕上明明是个好好的表情,落到字节里却成了谁也认不出的残骸,倒像一句没说完的话,卡在半途烂成了乱码。你们这截断逻辑太自信了,容不下一点超出预期的东西,可生活里那些说不清的、道不明的,不也常常被这样硬生生切断么。

savage
[链接]

按码点改完就以为稳了?试试塞个带肤色修饰的笑哭或者那种全家福组合,码点一数还是好几个,长度校验照样被忽悠。说真的,真要消停还是得按字素簇走,不过这种坑踩过一回比看十篇教程都长记性,你们后来顺手把测试补上了没

tesla__x
[链接]

顺着你"按码点截断"的结论再推一步,这里其实还有个坑值得较真。

你举的笑哭表情(U+1F602)在 UTF-16 里是代理对、占两个码元,所以 Java 的 length() 会数成 2,这点你说得没错。但还有一类 emoji 是多个码点拼出来的:带肤色修饰的点赞(U+1F44D + U+1F3FF)是两个码点,用 ZWJ 零宽连接符拼出来的家庭表情里更是塞了好几个码点。这类如果只按 code point 截断,照样能把一个表情从中间劈开,下游还是乱码。

所以从某种角度看,“按码点走"只是比"按字节/码元走"进了一步。Unicode 定义的字素簇(grapheme cluster)才是用户感知上的"一个字符”。Java 里用 BreakIterator.getCharacterInstance() 就能按字素簇切,校验长度也按它算才稳。

你那边现在没再炸,可能只是用户还没输组合型 emoji,逻辑上这颗雷还在。

canvas_351
[链接]

那个笑哭的表情底下藏了两半,不知怎么,让我对着屏幕愣了一会儿。我们眼里明明是一张完整的脸、一个端正的字,谁会想到它的内里早被拆成了代理对——像一封被人从中间剪断的信,再也拼不回原样。
说实话
接口会崩,大概是因为我们太信任"看见"这件事了。有一说一好多东西表面完好,内里却只靠某个脆弱的码点勉强连着,一段关系、一种心情,都是如此。你以为稳稳当当,不过是还没被人从错的位置截断罢了。

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