字数统计功能
今天在开发个人博客时,有一个博客文章字数统计的功能需要实现。我在网上搜索,并没有找到特别完善的方法,于是便想着自己实现一个。
.length
首先我想到的最简单方法就是 str.length
,但是这种方法并不好,为什么?因为 str.length
是用于统计字符串长度的,在统计文章字数时会遇到几个问题:
- 统计英文单词时,会把一个单词计算成多个字符
- 会把空白符统计进去,并且连续的多个空白符不会被合并
- 由于 JavaScript 采用 UTF-16 编码规范,一些字符可能会以多个码元表示
(实际上,字符串的 length 属性就是在访问这个字符串所占的码元,UTF-16 使用 16 位码元)
function strLength(str){
return str.length
}
strLength("你好") // 2
strLength("Hello there") // 11
strLength(" ") // 3
strLength("\n\n\n") //3
strLength("👨👩👧👦") //11
拆分功能
我们先来拆分要实现的需求,看看各类字符如何统计
汉字统计
包含汉字字符的字符串,除非存在一些罕见汉字,否则字符数直接使用 str.length
就可以完成统计。当然,如果要单独计算汉字字符数,我们需要使用正则匹配。因为基本汉字的 Unicode 编码是 4E00-9FA5
,所以匹配单个汉字的正则表达式写为 /[\u4e00-\u9fa5]/
。
function matchChineseCharacter(str){ // 匹配所有汉字,返回数组
return str.match(/[\u4e00-\u9fa5]/g)
}
const text = "你好,世界!"
strLength(text) // 6
matchChineseCharacter(text) // ['你', '好', '世', '界']
英文单词统计
英文单词的计算稍微复杂一点。如果是纯英文文本,不包含汉字,那么去掉单词之间的空白符,提取所有的单词组成数组,通过 str.split(/\s+/)
方法就可以实现。
"hello world".split(/\s+/) // ['hello', 'world']
如果是汉字和英文夹杂呢?
可以通过先 split
后 filter
的方法实现。
function matchWords(str){
return str.split(/\s+/).filter(word => word.match(/[a-zA-Z]/))
}
const text = "人生没有 Ctrl + Z ,每一步都要 Own it"
matchWords(text) // ['Ctrl', 'Z', 'Own', 'it']
但是这样子有一个问题,那就是当英文字符和标点符号或者汉字连在一起时,无法识别出来。
const textTight = "人生没有Ctrl+Z,每一步都要Own it."
matchWords(textTight) // ['人生没有Ctrl+Z,每一步都要Own', 'it.']
一个正确的方法是使用 Intl.Segmenter,JavaScript 内置的分词器对象。
function matchWordsFinal(str){
// 创建分词器实例,设置语言为英语,分割粒度为单词
const segmenter = new Intl.Segmenter('en', { granularity: 'word' })
// 分割文本并过滤出单词
const segments = Array.from(segmenter.segment(str))
const words = segments.filter(s => s.isWordLike).map(s => s.segment)
// 再次过滤出非英文单词
return words.filter(word => word.match(/^[a-zA-Z]+$/))
}
matchWordsFinal(textTight) // ['Ctrl', 'Z', 'Own', 'it']
最终实现
最终,我们希望实现的效果是,在一段文字中,既统计英文单词,又统计汉字。一个比较合理的方法是, 总的字数 = 汉字数 + 英文单词数 + 数字和标点符号数
如果要在统计英文单词数量的同时,统计汉字、标点或其他字符的数量,使用循环遍历 + 正则判断会方便一点,我们这里使用 RegExp.prototype.test()
方法,这个方法会执行一个检索,用来查看正则表达式与指定的字符串是否匹配,返回 true 或 false。
一个逻辑完整的代码如下
function wordCount(str){
let res = 0
let iTotal = 0 // 汉字数
let eTotal = 0 // 英文字母个数
let nTotal = 0 // 数字数
let sTotal = 0 // 空白符数
let oTotal = 0 // 其他字符数
for (let i = 0; i < str.length; i++) {
let c = str.charAt(i)
if (/[\u4e00-\u9fa5]/.test(c)) { // 匹配中文字符
iTotal++
} else if (/[a-zA-Z]/.test(c)) { // 匹配英文字符
eTotal++
} else if (/[0-9]/.test(c)) { // 匹配数字
nTotal++
} else if (/\s+/.test(c)) { // 匹配空白符
sTotal++
} else { // 匹配标点和其他字符
oTotal++
}
}
let wTotal = 0 // 英文单词数
const segmenter = new Intl.Segmenter('en', { granularity: 'word' })
const segments = Array.from(segmenter.segment(str))
const words = segments.filter(s => s.isWordLike).map(s => s.segment)
wTotal = words.filter(word => word.match(/^[a-zA-Z]+$/)).length
res = iTotal + nTotal + oTotal + wTotal
return res
}
const text = "人生没有Ctrl+Z,每一步都要Own it."
wordCount(text) // 16
但是我们又发现,汉字的个数用 str.length
就可以统计在内,所以利用这个特点,发现一个等式:iTotal + nTotal + oTotal + wTotal === str.length - eTotal + wTotal - sTotal
。等式的左边是原来的计算公式,右边是新的计算公式
新公式如下:
精确总字数 = 字符串长度 - 英文字母数 + 英文单词数 - 空白符数
最终,我们的字数统计函数简写成如下的形式
function wordCount(str){
let eTotal = 0
let sTotal = 0
for (let i = 0; i < str.length; i++) {
let c = str.charAt(i)
if (/[a-zA-Z]/.test(c)) {
eTotal++
} else if (/\s+/.test(c)) {
sTotal++
}
}
let wTotal = 0
const segmenter = new Intl.Segmenter('en', { granularity: 'word' })
const segments = Array.from(segmenter.segment(str))
const words = segments.filter(s => s.isWordLike).map(s => s.segment)
wTotal = words.filter(word => word.match(/^[a-zA-Z]+$/)).length
return str.length - eTotal + wTotal - sTotal
}
P.S.
当然,以上的函数并没有实现将一些特殊的 emoji 符号统计为单个字符的功能。这当然也可以使用 Intl.Segmenter
分词器实现。
wordCount("🤣") // 2
wordCount("👨👩👧👦") // 11
function emojiCount(emoji){
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return [...segmenter.segment(emoji)].length
}
emojiCount("🤣") // 1
emojiCount("👨👩👧👦") // 1