项目开发中遇到的前端字数统计问题

字数统计功能

今天在开发个人博客时,有一个博客文章字数统计的功能需要实现。我在网上搜索,并没有找到特别完善的方法,于是便想着自己实现一个。

.length

首先我想到的最简单方法就是 str.length,但是这种方法并不好,为什么?因为 str.length 是用于统计字符串长度的,在统计文章字数时会遇到几个问题:

  1. 统计英文单词时,会把一个单词计算成多个字符
  2. 会把空白符统计进去,并且连续的多个空白符不会被合并
  3. 由于 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']

如果是汉字和英文夹杂呢?

可以通过先 splitfilter 的方法实现。

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值