整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用 Go 如何实现精准统计文章字数

用 Go 如何实现精准统计文章字数

家好,我是站长 polarisxu。

今天要聊的内容应该可以当做一道面试题,你可以先想想该怎么实现。

统计字数是一个很常见的需求,很多人印象最深的应该是微博早些时候限制 140 字,而且边输入会边统计剩余字数。现在很多社区文章也会有字数统计的功能,而且可以依据字数来预估阅读时间。比如 Go语言中文网就有这样的功能。

01 需求分析

下手之前先分析下这个需求。从我个人经验看,在实际面试中,针对一个面试题,你的分析过程,循序渐进的解决方案,可以很好的展示你的思考过程。正所谓分析问题、解决问题。这会给你加分的。

我们采用类似词法分析的思路分析这个需求。

一篇文章通常包含如下元素,我们也称之为 token:

  • 普通文字
  • 标点符号
  • 图片
  • 链接(包含各种协议的链接)
  • 代码

其中普通文字通常会分为欧美和中日韩(CJK),因为 CJK 属于表意文字,和欧美字母的文字差异很大。同时这里还涉及到编码的问题。本文假设使用 UTF-8 编码。

对于标点符号,中文标点和英文标点也会很不一样。

此外还有全角和半角的问题。

根据以上分析,对于该需求作如下假定:

  • 空格(包括换行)不算字数;
  • HTML 标签需要剔除;
  • 编码方式:假定为 UTF-8 编码;
  • 标点符号算不算做字数。如果算,像括号这样的按 2 个字算;
  • 链接怎么算?一个链接约定为 1 个字可能更合适,大概阅读时只是把它当链接,而不太会关心链接由什么字母组成;
  • 图片不算做字数,但如果计算阅读时间,可能需要适当考虑图片的影响;
  • 对于技术文章,代码是最麻烦的。统计代码字数感觉是没多大意义的。统计代码行数可能更有意义;

本文的解决方案针对以上的假定进行。

02 Go 语言实现

先看最简单的。

纯英文

根据以上分析,如果文章只包含普通文本且是英文,也就是说,每个字(单词)根据空格分隔,统计是最简单的。

func TotalWords(s string) int {
 n := 0
 inWord := false
 for _, r := range s {
  wasInWord := inWord
  inWord = !unicode.IsSpace(r)
  if inWord && !wasInWord {
   n++
  }
 }
 return n
}

还有一种更简单的方式:

len(strings.Fields(s))

不过看 strings.Fields 的实现,性能会不如第一种方式。

回顾上面的需求分析,会发现这个实现是有 Bug 的。比如下面的例子:

s1 := "Hello,playground"
s2 := "Hello, playground"

用上面的实现,s1 的字数是 1,s2 的字数是 2。它们都忽略了标点符号。而且因为写法的多样性(不规范统一),导致计算字数会有误差。所以我们需要对写法进行规范。

规范排版

其实和写代码要有规范一样,文章也是有规范的。比如出版社对于一本书的排版会有明确的规定。为了让我们的文章看起来更舒服,也应该遵循一定的规范。

这里推荐一个 GitHub 上的排版指南:《中文文案排版指北》,它的宗旨,统一中文文案、排版的相关用法,降低团队成员之间的沟通成本,增强网站气质。这个规范开头关于空格的一段话很有意思:

有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。

建议大家可以看看这个指北,一些知名的网站就是按照这个做的。

因为 GCTT 的排版在这个规范做,但人为约束不是最好的方法,所以我开发了一个 Go 工具:https://github.com/studygolang/autocorrect,用于自动给中英文之间加入合理的空格并纠正专用名词大小写。

所以为了让字数统计更准确,我们假定文章是按一定的规范书写的。比如上面的例子,规范的写法是 s2 :="Hello, playground"。不过这里标点不算作字数。

刚去微博上试了一下,发现微博的字数计算方式有点诡异,竟然是 9 个字。

测试一下发现,它直接把两个英文字母算作一个字(两个字节算一个字)。而汉字是正常的。大家可以想想微博是怎么实现的。

中英文混合

中文不像英文,单词之间没有空格分隔,因此开始的那两种方式不适合。

如果是纯中文,我们怎么计算字数呢?

在 Go 语言中,字符串使用 UTF-8 编码,一个字符用 rune 表示。因此在标准库中查找相关计算方法。

func RuneCountInString(s string) (n int)

这个方法能计算字符串包含的 rune(字符)数,对于纯中文,就是汉字数。

str := "你好世界"
fmt.Println(utf8.RuneCountInString(str))

以上代码输出 4。

然而,因为很多时候文章会中英文混合,因此我们先采用上面的纯英文的处理方式,即:strings.Fields(),将文章用空格分隔,然后处理每一部分。

func TotalWords(s string) int {
 wordCount := 0
  
 plainWords := strings.Fields(s)
 for _, word := range plainWords {
  runeCount := utf8.RuneCountInString(word)
  if len(word) == runeCount {
   wordCount++
  } else {
   wordCount += runeCount
  }
 }

 return wordCount
}

增加如下的测试用例:

func TestTotalWords(t *testing.T) {
 tests := []struct {
  name  string
  input string
  want  int
 }{
  {"en1", "hello,playground", 2},
  {"en2", "hello, playground", 2},
  {"cn1", "你好世界", 4},
  {"encn1", "Hello你好世界", 5},
  {"encn2", "Hello 你好世界", 5},
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   if got := wordscount.TotalWords(tt.input); got != tt.want {
    t.Errorf("TotalWords() = %v, want %v", got, tt.want)
   }
  })
 }
}

发现 en1 和 encn1 测试不通过,因为没有按照上面说的规范书写。因此我们通过程序增加必要的空格。

// AutoSpace 自动给中英文之间加上空格
func AutoSpace(str string) string {
 out := ""

 for _, r := range str {
  out = addSpaceAtBoundary(out, r)
 }

 return out
}

func addSpaceAtBoundary(prefix string, nextChar rune) string {
 if len(prefix) == 0 {
  return string(nextChar)
 }

 r, size := utf8.DecodeLastRuneInString(prefix)
 if isLatin(size) != isLatin(utf8.RuneLen(nextChar)) &&
  isAllowSpace(nextChar) && isAllowSpace(r) {
  return prefix + " " + string(nextChar)
 }

 return prefix + string(nextChar)
}

func isLatin(size int) bool {
 return size == 1
}

func isAllowSpace(r rune) bool {
 return !unicode.IsSpace(r) && !unicode.IsPunct(r)
}

这样可以在 TotalWords 函数开头增加 AutoSpace 进行规范化。这时结果就正常了。

处理标点和其他类型

以上例子标点没计算在内,而且如果英文和中文标点混合在一起,情况又复杂了。

为了更好地实现开始的需求分析,重构以上代码,设计如下的结构:

type Counter struct {
 Total     int // 总字数 = Words + Puncts
 Words     int // 只包含字符数
 Puncts    int // 标点数
 Links     int // 链接数
 Pics      int // 图片数
 CodeLines int // 代码行数
}

同时将 TotalWords 重构为 Counter 的 Stat 方法,同时记录标点数:

func (wc *Counter) Stat(str string) {
 wc.Links = len(rxStrict.FindAllString(str, -1))
 wc.Pics = len(imgReg.FindAllString(str, -1))

 // 剔除 HTML
 str = StripHTML(str)

 str = AutoSpace(str)

 // 普通的链接去除(非 HTML 标签链接)
 str = rxStrict.ReplaceAllString(str, " ")
 plainWords := strings.Fields(str)

 for _, plainWord := range plainWords {
  words := strings.FieldsFunc(plainWord, func(r rune) bool {
   if unicode.IsPunct(r) {
    wc.Puncts++
    return true
   }
   return false
  })

  for _, word := range words {
   runeCount := utf8.RuneCountInString(word)
   if len(word) == runeCount {
    wc.Words++
   } else {
    wc.Words += runeCount
   }
  }
 }

 wc.Total = wc.Words + wc.Puncts
}

var (
 rxStrict = xurls.Strict()
 imgReg   = regexp.MustCompile(`<img [^>]*>`)
 stripHTMLReplacer = strings.NewReplacer("\n", " ", "</p>", "\n", "<br>", "\n", "<br />", "\n")
)

// StripHTML accepts a string, strips out all HTML tags and returns it.
func StripHTML(s string) string {
 // Shortcut strings with no tags in them
 if !strings.ContainsAny(s, "<>") {
  return s
 }
 s = stripHTMLReplacer.Replace(s)

 // Walk through the string removing all tags
 b := GetBuffer()
 defer PutBuffer(b)
 var inTag, isSpace, wasSpace bool
 for _, r := range s {
  if !inTag {
   isSpace = false
  }

  switch {
  case r == '<':
   inTag = true
  case r == '>':
   inTag = false
  case unicode.IsSpace(r):
   isSpace = true
   fallthrough
  default:
   if !inTag && (!isSpace || (isSpace && !wasSpace)) {
    b.WriteRune(r)
   }
  }

  wasSpace = isSpace

 }
 return b.String()
}

代码过多的细节不讨论。此外,关于文章内的代码行数统计未实现(目前没有想到特别好的方法,如果你有,欢迎交流)。

03 总结

通过本文的分析发现,精准统计字数没那么容易,这里涉及到很多的细节。

当然,实际应用中,字数不需要那么特别精准,而且对于非正常文字(比如链接、代码)怎么处理,会有不同的约定。

本文涉及到的完整代码放在 GitHub:https://github.com/polaris1119/wordscount。

常在很多网站编辑文本回答或发博客时,经常会遇到有最大或最小字数限制的问题,字数如果少了或多了,可能发不出去或没有平台奖励,好不容易编辑好的回答,又要重新修改,如此反复,很浪费时间和精力,如果字数少还好,可以自己数,但是字数多了,不仅费时间,而且自己很容易数错,唉,所以,达芬奇今天就同大家分享几个可以快速得到字数多少的方法,望能帮助到需要的朋友。

方法一:通过登录字数计数网站进行快速查询

打开网络浏览器,然后在窗口网址输入栏输入以下任一网址,摁“enther”键进入网站 ,然后复制你所编辑的网页回答或博客文章,然后粘贴进上述网址相应位置(见下图),即可得到文本字数。

网址一:文本计数网(word counter):https://wordcounter.net/

如下图所示,word counter是一个专业的文本计数网站,仅需把所复制文本,粘贴在下述框图里,便自动计数,且不仅包含文本的字数,包括行数,段数以及相应的关键词,及关键词使用次数都可得到,通过word counter不仅可得到文本的详细信息,同时通过对关键词及相应的关键词使用次数,可使文本编辑者优化及丰富自己所用“关键词”,使文本遣词造句更加合理丰富。

网址2:javascriptkit

http://www.javascriptkit.com/script/script2/countwords.shtml

如下图所示,javascriptkit提供的文本计数服务,虽不如wordcounter那么丰富,但对于我们的计数需求完全可以胜任。

方法二:借助软件,进行文本计数。

  1. 利用Notepad++(记事本升级版)软件。

下载地址:https://wwc.lanzouw.com/i7OcY0b1nude,密码:7mj3

使用方法:如下图所示,安装完成后,打开“notepad++”,然后粘贴复制的文本,接着点击上方工具栏的“视图”按钮,然后下拉至“摘要”选项,点击后,即可显示文本的“统计信息”,统计信息即包括字符数,字数,行数,文档长度等。

  1. Word

打开word,输入文字后,如图,在左下方即可显示文本字数多少。

3, 如果你经常性需要知道文本字数大小,那么上述方式就显得麻烦了,要么需要输入网址或打开软件,给大家推荐一个文本计数软件(dragking),解压后双击.ahk文件,可在软件起动后,如果复制文本可自动显示文本字数大小行数列数等,快捷方便。

下载地址:https://wwc.lanzouw.com/iXxGV0b1nuub,密码:asuy

同时如下图所示,此软件还可进行多种设置,包括是否开机起动,取词的快捷键,以及展示内容和位置进行设置。

好啦,今天达芬奇的分享就到这里啦,希望上述文本计数方式能帮到大家,同时如果你有使用过程中的问题也欢迎大家多多留言讨论,点赞关注。

时候需要控制下文字数,不然就会溢出,页面就会变样不美观。这时我们就可以用css控制字数,超出部分显示省略号。可以不换行,超出部分显示省略号,也可以可以换行,多行,超出部分显示省略号。

1.不换行,超出部分显示省略号

<!Doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>
<title>用css控制字数,超出部分显示省略号</title>
<style type="text/css">
*{margin:0;padding:0;}
body{width:1000px;margin:100px auto;}
.box{
width:260px;
/*超出部分就隐藏*/
overflow:hidden;
/*不换行设定*/
white-space:nowrap;
/*超出部分的文字显示省略号*/
text-overflow:ellipsis;
}
</style>
</head>
<body>
<div class="box">用css控制字数,超出部分显示省略号用css控制字数,超出部分显示省略号</div>
</body>
</html>

效果图如下:

2.可以换行,多行,超出部分显示省略号

<!Doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>
<title>可以换行,多行,超出部分显示省略号</title>
<style type="text/css">
*{margin:0;padding:0;}
body{width:1000px;margin:100px auto;}
.box{
width:260px;
display: -webkit-box;
-webkit-box-orient: vertical;
/*2行*/
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>
</head>
<body>
<div class="box">1.用css控制字数,超出部分显示省略号用css控制字数,超出部分显示省略号</div>
<div class="box">2.用css控制字数,超出部分显示省略号用css控制字数,超出部分显示省略号</div>
</body>
</html>

效果图如下:

注:此方法适用于WebKit浏览器及移动端。

除注明外的文章,均为来源:汤久生博客,转载请保留本文地址!

原文地址:http://tangjiusheng.com/divcss/169.html