字数统计 — 同一段字的三种长度,和两种切词口径

一段文字有三个字符数:存下来占的码元、编号表上的码点、读者看出来的写出来的单位。词也数两遍——一遍按字母数字的规则切,一遍让这个浏览器自带的分词切,两边切开不一样的地方逐条列出来。中文、日文、韩文只有分词那一路给得出数,规则那一路一个也找不到。你打进来或粘进来的内容不会发送到任何地方。

四段文字,四个数

文字码元码点写出来的个数按规则数的词
Hello, world!1313132
🇨🇳 flag9761
你好世界4440
Don't split over-hyphen words2929294
同一段字的三种长度与两种切词口径

同一段文字有三个长度,三个都对

"👨‍👩‍👧‍👦 ok"——四个小人用看不见的连接符拼成的一家人,一个空格,两个字母——按码元量是 14,按码点量是 10,按写出来的个数量是 4。文字没有变,变的是问法。码元是 length 数出来的东西:JavaScript 的字符串按 16 位的存储槽来数,所以一个辅助平面上的字占两个槽。码点是 Unicode 那张编号表上的一个号,也就是照着字符表一个一个数出来的"一个字"。字素簇是读者眼里算一整块的那个东西,重音符号、由一对区域符号拼成的旗帜、用 ZWJ 连起来的一家人,都在这里合成一个。

纯 ASCII 的三个数是一样的;文字里一出现由零件拼起来的东西,三个数就开始分家。"é" 用 e 加 U+0301 写出来是 2 个码元、2 个码点、1 个写出来的;"🇨🇳" 是 4 个码元、2 个码点、1 个写出来的。只印一个"字符数"的那个框,其实是替你把问法选好了;这里把三个问法一起印出来,选没选就看得见。

两条词规则,两条都不是答案

规则那一路用的是最常见的写法:字母和数字连着算一个,中间允许夹一个撇号或连字符。它和多数人脑子里英文的"词"对得上,和英文以外的东西对不上:

文字 按规则数到的词 按这个浏览器数到的词
don't stop 2 2
a-b-c 1 3
1.5 + 2,000 4 2
foo_bar baz 3 2
你好吗 0 2
我的名字叫小明 0 4
あいうえお 0 3
한글 어챌 0 2

1.5 + 2,000 数出四个词,是规则照着它被写出来的样子办事:它在点和逗号处断开,因为一条只管字母数字的规则不可能知道数字里那个点是什么。foo_bar baz 是反过来的:下划线不在规则的粘合清单里,一个标识符成了两个词。中文、日文、韩文不是"规则挑食"这种情况——一条关于字母和撇号的规则根本没有"中文的词在哪里结束"这个概念,所以一个也找不到。给中文文本的任何词数都是一次分词,不管背后有没有词典引擎在管。

分段不等于词,第一版最容易在这里翻车

浏览器的分词把文字里的每一段都交出来,空格和标点也在内,每一段带一个标记说它像不像词。don't stop 交回来三段、两个词。" \n " 交回来三段、一个词都没有。直接数分段,前一种多一个,后一种多三个。那个标记才是区别的来源:don't 回来是一个词,因为分词引擎知道缩约号里的撇号不是断点;a-b-c 回来是三个,因为那里的连字符是断点。同一个字符,两种都站得住的答案——所以两边切开不一样的那一段是列出来的,不是和成一个数交出去。

是哪个引擎在回答,两个引擎在哪里分开

词的那两个数出自 Intl.Segmenter,而它是编译进陪你读这段文字的那台 JavaScript 引擎里的,所以这个数一半是这块文字的事实,一半是那台引擎的事实。同一批样本实测下来,Chrome 150 和 Node 22.20 在上面那八行全对得上,只多出一行分歧:https://a.b/c 里的域名,Chrome 在点上断开(https、a、b、c),Node 那一版把 a.b 当整体留着。用英文问、用中文问、干脆不声明语言去问分词,这几种问法一个结果都没有改变——CJK 的分词来自编译进去的那份词典,跟你报的那个 locale 无关。

一个字算不算汉字,手写的范围表会漏掉什么

汉字的个数问的是字符属性,不是范围表:这个码点是不是汉字的成员。差别量得出来,而且只朝一个方向偏。𠀀𠀀x——扩展 B 上的两个字加一个拉丁字母——里面有 2 个这样的字,而只数到 U+FFFF 的范围表给出 0,因为扩展 B 从 U+20000 才开始,那上面的字每一个都占两个码元。〇⿰。( 里正好 1 个:〇是汉字,而表意文字描述字符、句号、全角括号都不是,不管它们在中文句子里看着多顺眼。平假名、片假名、谚文都是 0,它们是另外的文字、写的是另外的语言;把那些也折进"汉字"的个数里,数的是别的东西。

太老的引擎答不了属性问题,就会退回那份范围表;只要文字伸到了 U+FFFF 以上,这个数就会带着一句提醒印出来——它可能少算了字,这话不会闷着不说。

行数,以及不算换行的那两个字符

数行是在 CRLF、CR、LF 处断的,所以 a 是一行,a\n 是两行——第二行是空的,这正是文字以换行结尾时"行数"的含义——而空框报 0,不是把空串切一刀还回来的那个 1。

这只框在数行之前就替人拿过一次主意:文本框会把每一个 CR、每一个 CRLF 都改写成一个 LF,所以 a\r\nb\rc 进来是五个字符、三行,这是在 Chrome 150 上量出来的,不管是把字塞进去还是敲进去,结果都一样。Windows 的文字粘进来,和 Unix 的文字在这儿断在同一处。

U+2028 和 U+2029 在这里不算换行:两个数字中间夹一个段落分隔符,仍然是一行,编辑器怎么排版是编辑器的事。这是一条定义,不是漏掉了——规则点名的三种换行,是键盘和文件格式真会产出来的,而带那两个字符的文字少到值得为它和编辑器不一样。

至于"到底有没有输入过东西",判断用的是空白字符类,这一类比空格键宽:不换行空格、中文输入法打出来的表意空格 U+3000、几种窄空格、U+2028、U+2029 和字节序标记都在里面。一个只装了这些的框按"没有输入"来回答——那些句子不说了,但七个数照样印出来,因为一把看不见的字符确实有长度。这里每一个数都不去掉空白,量之前什么都不裁,所以码元那个数是文字占掉的格子数,不是敲过的键数。

什么东西会离开这台设备

计数是在这个标签页里对框中那段文字做的,用的就是上面写出来的那几条规则;算式里唯一来自外部的东西是浏览器自带的分词引擎,而交到它手里的那段文字从来没有出去过。你打进来或粘进来的内容不会发送到任何地方,也不会落在哪里。

问答

问:两个词数里哪个是真的? 都不真。规则那个数是一条字母数字规则给出的,也是大多数写作工具印出来的那个;浏览器那个数是一次分词,也是两个里唯一对中文、日文词在哪里结束有概念的那个。按用途挑:那个限制是某个人用英文的"词"说出来的,就用前者;想知道的是"这一大段有多少内容",而那段文字是规则读不懂的语言,就用后者。

问:我的中文在规则那一列是 0,是坏了吗? 是规则在正常干活。它匹配拉丁字母和数字的连续段,中文里没有可匹配的东西。另一列才是数中文的那一列,它数的方式是分词——那是词典的决定,不是一个字符类。

问:为什么满是数字的文档里,两个词数差得最远? 因为 1.5 和 2,000 在分词引擎那里各是一个词,在字母数字规则那里分别是三块和两块。数字、编号、千分位分隔符混在一页上,就是两边分岔最大的地方。

问:文字多大才会撑不住? 这一遍走的是前面 262,144 个码元,停下来时会说明;分歧表列前 24 段,真实段数写在它上面。两个上限都是为了打字时不卡——框里粘满这个规模,在 Chrome 150 里一次数完大约半秒,而粘进来的文字不会被改动。

问:写进合同的"字数"该用哪一个? 用对方读起来和你一致的那一个。写出来的个数是读者在纸面上数出来的;码元是一块缓冲区装的;码点是 Unicode 那张表编的号。三句看起来一模一样的话可以有三种长度,这就是三个数并排印出来的全部理由。

微信
微信
支付宝
支付宝