/** * Go語言詞頻統計,運行命令go run src/code/main.go test/words.txt * @author unknown * @since 2019-12-18 * 文件內容: hello tom glad to meet you yes me glad to meet you how are you * 輸出結果: Word Frequency are 1 glad 2 hello 1 how 1 me 1 meet 2 to 2 tom 1 yes 1 you 3 Frequency → Words 1 are, hello, how, me, tom, yes 2 glad, meet, to 3 you */ package main import ( "bufio" "fmt" "io" "log" "os" "path/filepath" "runtime" "sort" "strings" "unicode" "unicode/utf8" ) func main() { //命令輸入錯誤或須要幫助時提示 if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { fmt.Printf("usage: %s <file1> [<file2> [... <fileN>]]\n", filepath.Base(os.Args[0])) os.Exit(1) } //建立單詞=>頻次map frequencyForWord := map[string]int{} // 與:make(map[string]int)相同 //讀取文件內容 for _, filename := range commandLineFiles(os.Args[1:]) { //更新每一個單詞的頻次 updateFrequencies(filename, frequencyForWord) } //打印單詞=>頻次 reportByWords(frequencyForWord) //反轉單詞=>頻次 爲 頻次=>單詞(多個) wordsForFrequency := invertStringIntMap(frequencyForWord) //打印頻次=>單詞(多個) reportByFrequency(wordsForFrequency) } func commandLineFiles(files []string) []string { /** * 由於 Unix 類系統(如 Linux 或 Mac OS X 等)的命令行工具默認會自動處理通配符 *(也就是說,*.txt 能匹配任意後綴爲 .txt 的文件,如 README.txt 和 INSTALL.txt 等), * 而 Windows 平臺的命令行工具(CMD)不支持通配符,因此若是用戶在命令行輸入 *.txt,那麼程序只能接收到 *.txt。 * 爲了保持平臺之間的一致性,這裏使用 commandLineFiles() 函數來實現跨平臺的處理,當程序運行在 Windows 平臺時,實現文件名通配功能 */ if runtime.GOOS == "windows" { args := make([]string, 0, len(files)) for _, name := range files { if matches, err := filepath.Glob(name); err != nil { args = append(args, name) // 無效模式 } else if matches != nil { args = append(args, matches...) } } return args } return files } /** * updateFrequencies() 函數純粹就是用來處理文件的,它打開給定的文件,並使用 defer 在函數返回時關閉文件, * 這裏咱們將文件做爲一個 *bufio.Reader(使用 bufio.NewReader() 函數建立)傳給 readAndUpdateFrequencies() 函數, * 由於這個函數是以字符串的形式一行一行地讀取數據的,因此實際的工做都是在 readAndUpdateFrequencies() 函數裏完成的 */ func updateFrequencies(filename string, frequencyForWord map[string]int) { var file *os.File var err error if file, err = os.Open(filename); err != nil { log.Println("failed to open the file: ", err) return } defer file.Close() readAndUpdateFrequencies(bufio.NewReader(file), frequencyForWord) } /** * 第一部分的代碼咱們應該很熟悉了,用了一個無限循環來一行一行地讀一個文件, * 當讀到文件結尾或者出現錯誤的時候就退出循環,將錯誤報告給用戶但並不退出程序,由於還有不少其餘的文件須要去處理。 * 任意一行均可能包括標點、數字、符號或者其餘非單詞字符,因此咱們須要逐個單詞地去讀, * 將每一行分隔成若干個單詞並使用 SplitOnNonLetters() 函數忽略掉非單詞的字符,而且過濾掉字符串開頭和結尾的空白。 * 只須要記錄含有兩個以上(包括兩個)字母的單詞,能夠經過使用 if 語句,如 utf8.RuneCountlnString(word) > 1 來完成。 * 上面描述的 if 語句有一點性能損耗,由於它會分析整個單詞,因此在這個程序裏咱們增長了一個判斷條件, * 用來檢査這個單詞的字節數是否大於 utf8.UTFMax(utf8.UTFMax 是一個常量,值爲 4,用來表示一個 UTF-8 字符最多須要幾個字節) */ func readAndUpdateFrequencies(reader *bufio.Reader, frequencyForWord map[string]int) { for { line, err := reader.ReadString('\n') for _, word := range SplitOnNonLetters(strings.TrimSpace(line)) { if len(word) > utf8.UTFMax || utf8.RuneCountInString(word) > 1 { frequencyForWord[strings.ToLower(word)] += 1 } } if err != nil { if err != io.EOF { log.Println("failed to finish reading the file: ", err) } break } } } /** * 用來在非單詞字符上對一個字符串進行切分,首先咱們爲 strings.FieldsFunc() 函數建立一個匿名函數 notALetter, * 若是傳入的是字符那就返回 false,不然返回 true, * 而後返回調用函數 strings.FieldsFunc() 的結果,調用的時候將給定的字符串和 notALetter 做爲它的參數 */ func SplitOnNonLetters(s string) []string { notALetter := func(char rune) bool { return !unicode.IsLetter(char) } return strings.FieldsFunc(s, notALetter) } /** * 首先建立一個空的映射,用來保存反轉的結果,可是咱們並不知道它到底要保存多少個項, * 所以咱們假設它和原來的映射容量同樣大,而後簡單地遍歷原來的映射,將它的值做爲鍵保存到反轉的映射裏,並將鍵增長到對應的值裏去, * 新的映射的值就是一個字符串切片,即便原來的映射有多個鍵對應同一個值,也不會丟掉任何數據 */ func invertStringIntMap(intForString map[string]int) map[int][]string { stringsForInt := make(map[int][]string, len(intForString)) for key, value := range intForString { stringsForInt[value] = append(stringsForInt[value], key) } return stringsForInt } func reportByWords(frequencyForWord map[string]int) { words := make([]string, 0, len(frequencyForWord)) wordWidth, frequencyWidth := 0, 0 for word, frequency := range frequencyForWord { words = append(words, word) if width := utf8.RuneCountInString(word); width > wordWidth { wordWidth = width } if width := len(fmt.Sprint(frequency)); width > frequencyWidth { frequencyWidth = width } } sort.Strings(words) /** * 通過排序以後咱們打印兩列標題,第一個是 "Word",爲了能讓 Frequency 最後一個字符 y 右對齊, * 須要在 "Word" 後打印一些空格,經過 %*s 能夠實現的打印固定長度的空白,也能夠使用 %s 來打印 strings.Repeat(" ", gap) 返回的字符串 */ gap := wordWidth + frequencyWidth - len("Word") - len("Frequency") fmt.Printf("Word %*s%s\n", gap, " ", "Frequency") for _, word := range words { fmt.Printf("%-*s %*d\n", wordWidth, word, frequencyWidth, frequencyForWord[word]) } } /** * 首先建立一個切片用來保存頻率,並按照頻率升序排列,而後再計算須要容納的最大長度並以此做爲第一列的寬度,以後輸出報告的標題, * 最後,遍歷輸出全部的頻率並按照字母升序輸出對應的單詞,若是一個頻率有超過兩個對應的單詞則單詞之間使用逗號分隔開 */ func reportByFrequency(wordsForFrequency map[int][]string) { frequencies := make([]int, 0, len(wordsForFrequency)) for frequency := range wordsForFrequency { frequencies = append(frequencies, frequency) } sort.Ints(frequencies) width := len(fmt.Sprint(frequencies[len(frequencies)-1])) fmt.Println("Frequency → Words") for _, frequency := range frequencies { words := wordsForFrequency[frequency] sort.Strings(words) fmt.Printf("%*d %s\n", width, frequency, strings.Join(words, ", ")) } }