当前位置: 首页 > 科技观察

如何使用Go准确统计一篇文章的字数

时间:2023-03-16 13:23:37 科技观察

大家好,我是站长polarisxu。今天要说的应该是一道面试题,大家可以先想想怎么实现。统计字数是一个很常见的需求。很多人印象最深的就是早前微博的140字限制,打字的时候会统计剩余字数。现在很多社区文章也有统计字数的功能,可以根据字数估算阅读时间。比如Go语言中文网就有这样的功能。01需求分析在开始之前分析这个需求。从我个人的经验来看,在实际的面试中,你对一道面试题的分析过程和一步一步的解法,能够很好的展示出你的思考过程。也就是所谓的问题分析和问题解决。这会给你加分。我们用类似词法分析的思路来分析这个需求。一篇文章通常包含以下元素,我们也称之为令牌:普通文本标点符号图片链接(包括各种协议的链接)代码其中,普通文本通常分为欧美和中日韩(CJK),因为CJK属于表意文字,与欧美字母的文字有很大区别。同时,还涉及到编码的问题。本文假定采用UTF-8编码。对于标点符号,中文标点符号和英文标点符号会有很大的不同。此外,还有全角和半角的问题。基于以上分析,对该需求做如下假设:空格(包括换行符)不计为单词;需要移除HTML标签;编码方式:假设为UTF-8编码;标点符号不算作单词。如果算的话像括号一样算2个词;如何计算链接?链接是1个词可能更合适,阅读时大概只是作为链接使用,不太关心链接由哪些字母组成;图片不计入字数,但如果计算阅读时间,可能需要适当考虑图片的影响;技术文章,代码是最麻烦的。统计代码中的字数意义不大。统计代码行数可能更有意义;本文中的解决方案基于上述假设。02Go语言实现先看最简单的。PureEnglish根据上面的分析,如果文章只包含普通的文字,而且是英文的,即每个字符(词)之间用空格隔开,统计起来是最容易的。funcTotalWords(sstring)int{n:=0inWord:=falsefor_,r:=ranges{wasInWord:=inWordinWord=!unicode.IsSpace(r)ifinWord&&!wasInWord{n++}}return}有一个更简单的方法:len(strings.Fields(s))但是,根据strings.Fields的实现,性能将不如第一种方法。回过头来看上面的需求分析,你会发现这个实现是有bug的。比如下面这个例子:s1:="Hello,playground"s2:="Hello,playground"使用上面的实现,s1中的字符数为1,s2中的字符数为2,它们都忽略标点。并且因为写法的多样性(不规范统一),计算字数也会有误差。所以我们需要规范写作。其实排版规范化和写代码要规范化一样,文章也是规范化的。比如出版社对一本书的排版会有明确的规定。为了让我们的文章看起来更舒服,我们也应该遵循一定的规范。这里有一份GitHub上的排版指南:《中文文案排版指北》,其目的是统一中文文案排版的相关用法,减少团队成员之间的沟通成本,提升网站气质。本规范开头一段关于空格的内容很有意思:研究表明,不喜欢在打字时在中英文之间加空格的人,在人际关系中会遇到困难,70%的人会在34岁。嫁给自己不爱的人,剩下的百分之三十,只能把遗产留给自己的猫。毕竟爱情和写作都需要适时留白。我建议你可以看看这个指南,一些知名网站都是基于这个的。因为GCTT的排版是在这个规范中完成的,但是人为的约束并不是最好的方法,所以我开发了一个Go工具:https://github.com/studygolang/autocorrect,用于自动在汉字之间添加合理的空格英语和正确的专有名词大写。因此,为了使字数统计更加准确,我们假设文章是按照一定的规范来写的。比如上面的例子,标准的写法是s2:="Hello,playground"。但是,标点符号在这里不算作单词。刚刚在微博上试了一下,发现微博字数的计算方式有点奇怪,竟然是9个字。测试了一下,发现它直接把两个英文字母算作一个单词(两个字节算一个单词)。而汉字是正常的。大家可以想想微博是怎么实现的。MixedChineseandEnglish中文不像英文,单词之间没有空格,所以开头的两种方式都不适合。如果是纯中文,我们怎么统计字数呢?在Go语言中,字符串使用UTF-8编码,字符用符文表示。于是在标准库中查找相关的计算方法。funcRuneCountInString(sstring)(nint)这个方法可以计算字符串中包含的符文(字符)的个数。对于纯中文,就是汉字的个数。str:="Helloworld"fmt.Println(utf8.RuneCountInString(str))上面的代码输出4。但是因为很多文章会中英文混合,所以我们先用上面的纯英文处理方式,即:strings.Fields(),将文章用空格隔开,然后对每一部分进行处理。funcTotalWords(sstring)int{wordCount:=0plainWords:=strings.Fields(s)for_,word:=rangeplainWords{runeCount:=utf8.RuneCountInString(word)iflen(word)==runeCount{wordCount++}else{wordCount+=runeCount}}returnwordCount}添加以下测试用例:funcTestTotalWords(t*testing.T){tests:=[]struct{namestringinputstringwantint}{{"en1","hello,p??layground",2},{"en2","hello,playground",2},{"cn1","Helloworld",4},{"encn1","Hellohelloworld",5},{"encn2","Hellohelloworld",5},}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){ifgot:=wordscount.TotalWords(tt.input);got!=tt.want{t.Errorf("TotalWords()=%v,want%v",got,tt.want)}})}}发现en1和encn1测试失败是因为没有按照上述规范编写。所以我们以编程方式添加必要的空间。//AutoSpace自动在中英文之间添加空格(prefix)==0{returnstring(nextChar)}r,size:=utf8.DecodeLastRuneInString(prefix)ifisLatin(size)!=isLatin(utf8.RuneLen(nextChar))&&isAllowSpace(nextChar)&&isAllowSpace(r){returnprefix+""+string(nextChar)}returnprefix+string(nextChar)}funcisLatin(sizeint)bool{returnsize==1}funcisAllowSpace(rrune)bool{return!unicode.IsSpace(r)&&!unicode.IsPunct(r)}这样在TotalWords函数的开头加上AutoSpace进行归一化。那么结果就正常了。处理标点符号和其他类型的标点符号在上面的例子中不算,如果英汉标点符号混用,情况又复杂了。为了更好的实现一开始的需求分析,重构上面的代码,设计如下结构:LinknumberPicsint//图片数量CodeLinesint//代码行数}同时将TotalWords重构到Counter的Stat方法中,同时记录标点数量:func(wc*Counter)Stat(strstring){wc.Links=len(rxStrict.FindAllString(str,-1))wc.Pics=len(imgReg.FindAllString(str,-1))//去除HTMLstr=StripHTML(str)str=AutoSpace(str)//去除普通链接(非HTML标签链接)str=rxStrict.ReplaceAllString(str,"")plainWords:=strings.Fields(str)for_,plainWord:=rangeplainWords{words:=strings.FieldsFunc(plainWord,func(rrune)bool{ifunicode.IsPunct(r){wc.Puncts++returntrue}returnfalse})for_,word:=rangewords{runeCount:=utf8.RuneCountInString(word)iflen(word)==runeCount{wc.字++}else{wc.Words+=runeCount}}}wc.Total=wc.Words+wc.Puncts}var(rxStrict=xurls.Strict()imgReg=regexp.MustCompile(`]*>`)stripHTMLReplacer=strings.NewReplacer("\n","","

","\n","
","\n","
","\n"))//StripHTMLacceptsastring,stripsoutallHTMLtagsandreturnsit.funcStripHTML(sstring)string{//Shortcutstringswithnotagsinthemif!strings.ContainsAny(s,"<>"){returns}s=stripHTMLReplacer.Replace(s)//遍历stringremovingalltagsb:=GetBuffer()deferPutBuffer(b)varinTag,isSpace,wasSpaceboolfor_,r:=ranges{if!inTag{isSpace=false}switch{caser=='<':inTag=truecaser=='>':inTag=falsecaseunicode.IsSpace(r):isSpace=truefallthroughdefault:if!inTag&&(!isSpace||(isSpace&&!wasSpace)){b.WriteRune(r)}}wasSpace=isSpace}returnb.String()}代码太多细节就不讨论了,另外文章中代码行数的统计也没有实现(目前还没想到特别好的方法,有的话欢迎交流)03总结通过这篇文章的分析,发现准确统计字数并不是那么容易的,其中有很多细节,当然在实际应用中,字数并没有那么简单需要这么精确,对于如何处理异常文本(如链接和代码)会有不同的约定。本文涉及的完整代码放在GitHub上:https://github.com/polaris1119/wordscount。本文转载自微信公众号「polarisxu」,可关注下方二维码。转载本文请联系polarisxu公众号。