字符数统计工具

同一段文本,同时按码点、UTF-16 码元、UTF-8 字节、字素簇四种口径数一遍,再分成含空白与不含空白两栏,顺手核对目标平台的单条上限还剩多少余量。

码点(含空白)136
码点(不含空白)106
UTF-16 码元140
UTF-8 字节298
字素簇132
行数6
段落数2
空白字符30

提示:统计随输入自动完成,不必点按钮;文本很长时可以按 Ctrl + Enter(macOS 用 Cmd + Enter)立刻重算一次——单独的 Enter 仍然是换行,不会被本页拦截。文本框为空时不会自动填示例,点「载入示例」可以随时还原上面这段样例。

使用方法

  1. 把要统计的文本粘进上面的文本框,或直接在里面输入。结果是实时更新的,页面打开时框里已经预置了一段 6 行的示例,统计卡上显示的就是它的结果。
  2. 先看码点(含空白)与码点(不含空白):示例 136 与 106,相差的 30 是半角空格(25 个)与换行(5 个)。平台说「不含空格」时看的都是后一个数。
  3. 要跟后端交代存储体积,看UTF-8 字节。示例文本 136 个码点占 298 字节,平均一个码点 2.19 字节——中文比英文「重」,原因写在下面的原理一节里。
  4. 要排查「粘贴后提示超长」,看UTF-16 码元:示例 140,比码点多 4,多出来的正是表情和 ZWJ 家庭序列;不少平台的限制按码元算。
  5. 字素簇是「肉眼看到的字符个数」,示例 132,比码点少 4;要数用户感知的长度时用这一栏。
  6. 在目标平台的单条上限里填限制值(默认 280),下面立刻算出还差多少或超了多少;填非法值会告诉你允许的范围。

这个字符数统计工具能解决什么问题

一是核对数据库字段够不够长。建表时 VARCHAR(255) 能放多少内容,取决于数据库按字符还是按字节算。示例文本 136 个码点占 298 字节,光看码点会严重低估。

二是排查「粘贴后提示超长」。同一段话在原编辑器里没超,粘到后台却提示超限,常见原因是文本里夹着 Word 带来的不换行空格与全角空格(都被 \s 排进空白),或者平台按 UTF-16 码元计数、把每个表情算成 2 个。码点与码元两栏并排显示,一眼能看出是哪一栏超了。

三是校验接口的字符上限。短信、推送标题、第三方 title 字段大多有硬上限,写脚本前把样例文案量一遍,比上线后被截断再回头改省事。

四是和 CSV / Excel 的 LEN() 对账。表格里 LEN() 数的是 UCS-2 码元,和本页「UTF-16 码元」栏同源;含表情时两边会同时比码点口径大,知道这点就不会误判为表格算错。

本页的边界也要说清楚:这个字符数统计工具不按语言单位做拆分——不数中文字个数、不数英文单词、不做词频排行,也不估算朗读耗时。需要按词计的口径或词频排行,请用页面底部的「词频统计工具」。

统计原理:码元、码点、UTF-8 字节与字素簇

四种口径对应四个不同的函数,不是一个数换个名字。

其一,UTF-16 码元数,就是 s.length。ECMAScript 规范把字符串定义成 UTF-16 码元序列,length 返回的就是码元个数。U+0000 到 U+FFFF 之间的字符占 1 个码元;U+10000 以上的字符在 UTF-16 里没有单码元表示,必须拆成一对代理码元(高代理 U+D800–U+DBFF、低代理 U+DC00–U+DFFF),因此占 2 个码元。示例里的 😀 是 U+1F600,码元数是 2;一段 ZWJ 家庭序列 U+1F468 U+200D U+1F469 U+200D U+1F467 占 8 个码元。这就是码元 140 比码点 136 多 4 的来源。

其二,码点数,用 [...s].length。数组展开语法会调用字符串的默认迭代器,该迭代器按码点走:遇到代理对时一次吞掉两个码元,只产出一个值。所以它数的是「Unicode 码点个数」,也是大多数人心里那个「字符数」。

其三,UTF-8 字节数,用 new TextEncoder().encode(s).length。TextEncoder 由 W3C《Encoding Standard》定义,输出格式固定 UTF-8,没有参数能改成别的编码——所以说「字节数」必须同时说编码。UTF-8 是变长编码:U+007F 及以下 1 字节,U+0080–U+07FF 2 字节,U+0800–U+FFFF 3 字节,U+10000–U+10FFFF 4 字节。常用汉字在第三档,一个字 3 字节;😀 在第四档,4 字节。换成 GB18030 就完全不同:同一个「字」(U+5B57)在 GB18030 里是 D7 D6 共 2 字节,UTF-8 里是 E5 AD 97 共 3 字节;韩文音节 U+D55C 在 GB18030 里反而是 4 字节。

其四,字素簇数,用 Intl.Segmenter。字素簇是「用户感知到的一个书写单位」,按 UAX #29 切分:把「基字符 + 组合附加符号」合成一个,把 ZWJ 表情序列合成一个,把区域指示符组成的国旗合成一个。示例里家庭序列有 5 个码点、字素簇只有 1 个,所以总数从 136 降到 132。

中间量的流动。文本框的值先被 v.replace(/\s/g, '') 复制并删掉全部空白,得到非空白码点;\s 覆盖的不只是半角空格和换行,还包括制表符、不换行空格 U+00A0、中文全角空格 U+3000 与 BOM U+FEFF——从 Word 粘过来的文本经常夹带后两种。行数用 v.split(/\r?\n/),段落数用 v.trim().split(/(?:\r?\n\s*){2,}/),只有连续两个以上换行才算分段。

一个坑:按字符校验的正则要加 u 标志。不带 u 时 . 匹配不了代理对,/^.$/.test('😀') 返回 false,加上才是 true。

完整示例(数值仅作演示)

示例条件就是页面打开时文本框里那段文字,共 6 行 2 段,含中英文混排、一个表情和一段 ZWJ 家庭序列。八项指标实测结果:

把 136 个码点按构成拆开,正好对得上总数,也能看出中文字多时字节数为什么涨得快:

构成码点数UTF-8 字节说明
汉字(CJK 基本区)63189每个 3 字节,是字节数的主要来源
数字(含 U+FF10 全角数字)1414半角数字 1 字节
拉丁字母1111UTF-8 下 ASCII 恒为 1 字节
中文与全角标点1030顿号、句号、逗号、冒号,各 3 字节
半角空格 U+00202525计入码点,被「不含空白」排除
换行 U+000A555 个换行撑出 6 行、2 段
ASCII 连字符 -22来自 UTF-8、UTF-16 两个词
emoji 😀142 码元 1 码点,字节数再次放大
emoji 家庭人物3123 个码点,各自 4 字节
零宽连接符 ZWJ26不可见,却计入码点与字节
合计136298与统计卡完全一致

把示例文本填进默认的 280 上限,按码元口径还剩 140 个,按码点口径还剩 144 个——两者不等,正是码元与码点这 4 个差造成的。留安全余量请按小的那个算。

逐字符口径对照表

下表把容易「数错」的字符逐个列出,四种口径并排。想验证某个平台的计数规则,可以拿它对照:

字符码位码元码点字节字素簇说明
AU+00411111基本拉丁字母,四种口径全为 1
字U+5B571131常用汉字,UTF-8 下 3 字节,GB18030 下 2 字节
龘U+9F981131笔画最多的汉字之一,仍只占 1 个码元
〇U+30071131属 CJK 符号区,不是表意文字区
한U+D55C1131韩文音节;做 NFD 分解会变成 3 个码点
é(预组合)U+00E911211 码元 1 码点的紧凑写法
e + U+0301(组合)U+0065 U+03012231看起来和上面一样,码点却翻倍,字素簇仍是 1
😀U+1F6002141补充平面字符:码元比码点多 1
𠮷U+20BB72141CJK 扩展 B 的生僻字,与表情同属补充平面
👍🏽U+1F44D U+1F3FD4281表情加肤色修饰符,2 个码点合成 1 个字素簇
🇨🇳U+1F1E8 U+1F1F34281两个区域指示符拼出的国旗
👨‍👩‍👧U+1F468 U+200D …85181ZWJ 家庭序列:8 码元 5 码点,只是 1 个字素簇
VS16U+FE0F1131变体选择符:不改变码点,却会改变字素簇边界
ZWJU+200D1131零宽连接符:本身不可见,却把多个码点粘成一个字素簇
半角空格U+00201111计入码点,被「不含空白」排除
全角空格U+30001131中文全角空格,同样被 \s 匹配,占 3 字节
NBSPU+00A01121不换行空格,从网页和 Word 复制时最常见
制表符U+000911111 码元 1 字节,也属于 \s
换行 LFU+000A1111类 Unix 换行,占 1 个码元
回车换行 CRLFU+000D U+000A2221Windows 换行占 2 个码元,本页按 1 个换行处理
零宽空格U+200B1131肉眼不可见,却不属于 \s,会算进「不含空白」
BOMU+FEFF1131字节序标记属于 \s,粘贴时常被一起带进来
带圈数字 ①U+24601131看起来是「一个符号」,仍占 1 个码点 3 个字节
罗马数字 ⅫU+216B1131单码点表示十二,不是两个字母
全角字母 AU+FF211131全角字母占 3 字节,而不是 1 字节

输入范围、边界与精度说明

下面这些不是「限制」,而是刻意划出来的边界,知道它们能避免把本页当成万能工具:

常见问题

同一段文本,为什么两个统计页给出的数不一样?

因为口径不同。本页给的是码点、码元、UTF-8 字节和字素簇四个数,按语言单位划分的口径算的是另一套东西。示例文本在本页是码点 136、码元 140、字节 298、字素簇 132;你在别处看到的别的数,多半是按汉字或按词计的口径。先确认口径名字,再比数字。

一个表情到底算几个字符?

看口径。示例里的 😀:码点 1 个、UTF-16 码元 2 个、UTF-8 字节 4 个、字素簇 1 个。平台说「最多 280 个字符」时,多数是按码元算的,要按 2 来估;本页的「目标上限」默认就是 280,并按码元口径算余量。

为什么「UTF-16 码元」比「码点」大?

因为 U+10000 以上的字符必须用一对代理码元表示,占 2 个码元却只是 1 个码点。示例里表情让码元多 1,ZWJ 家庭序列多 3,合计多出 4,136 就变成 140。纯中文或纯英文文本里这两个数完全相等。

字素簇什么时候比码点少?

只要文本里有组合附加符号、ZWJ 序列、肤色修饰符或区域指示符,字素簇就会少于码点。示例中家庭序列从 5 个码点压成 1 个字素簇,总数便从 136 降到 132。做「用户眼里的长度」校验看字素簇,做存储容量估算看字节。

字节数为什么必须说明编码?

因为同一个字符在不同编码下字节数不同。常用汉字在 UTF-8 里 3 字节,在 GB18030 里 2 字节;韩文音节 U+D55C 在 UTF-8 里 3 字节,在 GB18030 里反而是 4 字节。本页字节栏由 TextEncoder 产生,固定 UTF-8,所以报数要写成「UTF-8 字节 298」。

空格、换行、中文标点算不算?

都计入「码点(含空白)」。示例 136 与 106 的差额 30 就是 25 个半角空格加 5 个换行。中文标点同样计入码点与 UTF-8 字节(各 3 字节),但不影响空白相关的两个数字。

CSV 或 Excel 里的 LEN() 为什么和这里对不上?

它们数的是 UCS-2 码元,和本页的「UTF-16 码元」同一口径,这个数应该相等;对不上通常是两种原因:一是单元格里的文本和这里粘贴的不完全一样,二是你拿 LEN() 的结果去比本页的码点栏,而这两栏在含表情时本来就差着几个。

文本会上传或保存吗?上限是多少?

不会。这个字符数统计工具的全部运算都在当前页面完成,没有网络请求,也不写浏览器存储,刷新后文本框回到预置示例。上限是 100 万码元,超过后输入框不再接收新内容,已有文字不会被截断。想自己确认,打开开发者工具的 Network 面板,输入时观察有没有请求发出即可。

相关工具