什么才算一个字符?
同一段文本,四套系统有四种不同的算法。对英文来说它们结果一致,所以这个问题一直隐而不见——直到有一天不再一致。
最后审阅:
一个表情符号,四种答案
拿一个加了肤色的点赞表情,或者一个家庭表情来说。用四种方法去数,会得到四个数字,而且没有一个是错的。
读者看到的是一个字符。 一个东西,按一下退格键就能删掉。
正则表达式可能看到七个。 它由几个码位通过不可见的连接符组合而成——一个基础表情、一个修饰符、一个连接符、另一个表情,依此类推。
JavaScript报告的是十一个。 它以16位单元存储文本,基本范围之外的字符都要占两个单元,所以其中每个码位都可能算作两个。
数据库字段看到的是二十五个字节。 按UTF-8编码存储时,每个码位最多占四个字节。
对普通英文来说,这四个数字完全相同。正因如此,这个问题一直隐而不见,直到有人输入一个带重音的名字、一句日文或一个表情符号——一个正常运行了三年的字段开始截断数据。
您的限制指的是哪个数
实际要问的从来不是“这段文本有多长”,而是“是什么在做限制”,常见的答案有四种。
数据库字段声明为VARCHAR时,在大多数数据库引擎中计算的是字节。这就是为什么一个能轻松容纳255个英文字符的字段,能容纳的日文字符却少得多——每个占三个字节——也是为什么一个带重音的名字偶尔放不下,而不带重音的版本却放得下。
短信在其自有的7位字母表中为160个字符,一旦有一个字符超出这个字母表,就会降到每条70个。从文字处理软件里粘贴过来的一个弯引号,或者一个表情符号,就可能让一条短信变成三条,费用也随之翻三倍。
JavaScript表单校验几乎总是按UTF-16单元计数,所以一个标明280个字符的输入框,可能会拒绝一段看起来短得多、却满是表情符号的文字。
人数的是自己看得见的东西,而对于标题、title标签或任何有显示空间限制的内容,这才是唯一重要的定义。
计数在哪里出现分歧
知道哪些输入会出问题,比了解理论更有用,因为这些输入是可以预料的。
表情符号,尤其是组合而成的——肤色、家庭、旗帜、职业。一面旗帜是两个区域指示字母;一个职业通常是一个人、一个连接符和一个物体。
带重音的字符和组合字符。 é既可以写成一个码位,也可以写成e加一个组合用的尖音符。它们看起来一模一样,却排序不同、比较时不相等、计数也不同——而且两种形式都会出现在真实数据中,常常就在同一列里。
非拉丁文字。 中文、日文和韩文字符在UTF-8中每个占三个字节。印地语、泰语、泰米尔语和阿拉伯语会组成字素簇,读者眼中是一个单位,实际却由好几个码位组成。
数学符号和音乐符号,它们位于基本范围之外,因此在UTF-16中算作两个。
该怎么办
按您的限制所用的方式计数。 一个能同时显示四种数字的计数工具可以直接回答这个问题,比自己推算快得多。在代码中,使用对应的函数:JavaScript中的Intl.Segmenter统计读者看到的字符数,Python中的len(s.encode("utf-8"))统计字节数,PHP中则用mb_strlen而不是strlen。
修改字段,而不是修改表单。 如果约束来自数据库字段,治本的办法在上游:在MySQL中声明为utf8mb4,在PostgreSQL中声明为text。MySQL旧的“utf8”出了名地每个字符只有三个字节,根本存不下表情符号——这是一个由来已久的陷阱,已经悄无声息地截断了大量真实数据。在表单上小心计数,只是对一个本该另作声明的字段的权宜之计。
在输入时进行规范化。 在文本进入系统时就统一转换成同一种规范形式,é的两种写法就会合而为一,比较、排序和计数也都会随之一致。每种编程语言都有相应的函数,在入口处处理一次,远比在各处同时应对两种形式容易。
相关情况:Base64
之所以值得一提,是因为它会带来同样的意外。把数据编码为Base64时,每次取三个字节,写成四个字符,所以结果会大约大出三分之一——这是算术,而不是效率低。一个6 MB的附件会变成约8 MB的文本,这就是为什么一个明明低于大小限制的文件,编码传输后却会被拒绝。
同样的算法也解释了为什么邮件附件会在看似满足的限制下被退回。如果要按上限来控制大小,请按编码后的大小来算。
常见问题
为什么我那个255个字符的字段放不下更短的文字?
几乎可以肯定,是因为它按字节而不是按字符计数。带重音的字母在UTF-8中占两个字节,中日韩字符占三个,所以255个字节可能只够85个日文字符。把字段声明为utf8mb4或text,才能真正解决问题。
一个表情符号真的会让我付三条短信的钱吗?
有可能。只包含GSM字母表字符的短信,每条可发160个字符;一旦有一个字符超出该字母表,整条短信就会切换到每条70个字符的编码。所以一条带一个表情符号的150字符短信会变成三条,而不是一条。
title标签该用哪种计数?
哪种都不精确——搜索引擎按像素宽度而不是字符数截断,所以由宽大写字母组成的标题会比由窄小写字母组成的更早被截断。约60个字符是通常的经验值,它只是参考,而不是限制。
为什么两个看起来一模一样的字符串却不匹配?
通常是因为某个带重音的字符有两种写法——一个是单个码位,另一个是基础字母加组合符号。比较前把两者规范化为同一种形式,它们就能匹配,而且值得在数据进入系统时就这样做。
字数比字符数更稳定吗?
对英文来说,大体如此。但并非处处如此:中文和日文不用空格分隔词语,泰文也不用,所以在这些文字中统计词数需要先分词,不同工具会给出不同的结果。