您現在的位置是:首頁 > 棋牌

用 Go 如何實現精準統計文章字數

  • 由 Go中國 發表于 棋牌
  • 2022-05-06
簡介為了更好地實現開始的需求分析,重構以上程式碼,設計如下的結構:type Counter struct { Total int總字數 = Words + Puncts Words int只包含字元數 Puncts

word怎麼輸入全形符號

大家好,我是站長 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 如何實現精準統計文章字數

測試一下發現,它直接把兩個英文字母算作一個字(兩個位元組算一個字)。而漢字是正常的。大家可以想想微博是怎麼實現的。

中英文混合

中文不像英文,單詞之間沒有空格分隔,因此開始的那兩種方式不適合。

如果是純中文,我們怎麼計算字數呢?

在 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(`]*>`) stripHTMLReplacer = strings。NewReplacer(“\n”, “ ”, “

”, “\n”, “
”, “\n”, “
”, “\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。

用 Go 如何實現精準統計文章字數

Top