golang实现Porter词干提取算法的文本处理插件porter的使用

Golang实现Porter词干提取算法的文本处理插件porter的使用

Porter Stemmer for Go

这是一个对Martin Porter的C语言实现的Porter词干提取算法的直接移植。该移植基于的C语言版本是线程安全版本。

原始算法在以下论文中描述:

M.F. Porter, 1980, An algorithm for suffix stripping, Program, 14(3) pp
130-137.

虽然内部实现和接口与原始实现几乎相同,但Go接口已经大大简化。可以按以下方式调用词干提取器:

import "porter"
...
stemmed := porter.Stem(word_to_stem)

安装

go get github.com/a2800276/porter

使用goinstall安装后,导入方式为:

import "github.com/a2800276/porter"

完整示例

下面是一个完整的示例代码,展示如何使用porter库进行词干提取:

package main

import (
	"fmt"
	"github.com/a2800276/porter"  // 导入porter词干提取库
)

func main() {
	// 示例单词列表
	words := []string{
		"running",
		"jumps",
		"happily",
		"cats",
		"conclusion",
	}

	// 对每个单词进行词干提取
	for _, word := range words {
		stemmed := porter.Stem(word)  // 调用Stem函数提取词干
		fmt.Printf("Original: %-10s Stemmed: %s\n", word, stemmed)
	}
}

输出结果示例:

Original: running    Stemmed: run
Original: jumps      Stemmed: jump
Original: happily    Stemmed: happili
Original: cats       Stemmed: cat
Original: conclusion Stemmed: conclus

限制

虽然实现相当健壮,但这仍然是一个进行中的工作。特别是,可能会提供一个新的接口来防止在string[]byte之间进行过多的转换。目前,在调用Stem时,字符串参数会被转换为字节切片,算法处理后再转换回字符串返回。

此外,该实现在处理Unicode输入方面不是很健壮,目前只会忽略设置了高位的字节。调用者有责任确保字符串只包含ASCII字符。由于算法本身只对英语单词进行操作,这并不限制功能,但确实是一个不便之处。

TODO

  • 提供字节切片API以避免往返于字符串的转换

更多关于golang实现Porter词干提取算法的文本处理插件porter的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现Porter词干提取算法的文本处理插件porter的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang实现Porter词干提取算法

Porter词干提取算法是一种常用的英文词干提取算法,它通过一系列规则将单词转换为其基本形式(词干)。下面我将介绍如何在Go中实现和使用Porter词干提取算法。

使用porter库实现词干提取

Go语言中有一个常用的porter算法实现库github.com/a2800276/porter。以下是使用示例:

package main

import (
	"fmt"
	"github.com/a2800276/porter"
	"strings"
)

func main() {
	// 示例文本
	text := "The quick brown foxes are jumping over the lazy dogs"

	// 分割文本为单词
	words := strings.Fields(text)

	// 创建Porter词干提取器实例
	stemmer := porter.NewStemmer()

	// 对每个单词进行词干提取
	for _, word := range words {
		stem := stemmer.Stem(word)
		fmt.Printf("%s -> %s\n", word, stem)
	}
}

纯Go实现Porter算法

如果你想了解算法原理或需要自定义实现,下面是一个完整的Porter词干提取算法的Go实现:

package porterstemmer

import (
	"strings"
)

// 判断是否是辅音
func isConsonant(s []rune, i int) bool {
	switch s[i] {
	case 'a', 'e', 'i', 'o', 'u':
		return false
	case 'y':
		if i == 0 {
			return true
		}
		return !isConsonant(s, i-1)
	default:
		return true
	}
}

// 计算辅音序列数量
func measure(s []rune) int {
	count := 0
	i := 0
	n := len(s)

	// 跳过开头的辅音
	for i < n && isConsonant(s, i) {
		i++
	}

	// 计算VC的数量
	for i < n {
		// 找元音
		for i < n && !isConsonant(s, i) {
			i++
		}
		if i >= n {
			break
		}
		count++
		// 找辅音
		for i < n && isConsonant(s, i) {
			i++
		}
	}

	return count
}

// 判断是否包含元音
func containsVowel(s []rune) bool {
	for i := range s {
		if !isConsonant(s, i) {
			return true
		}
	}
	return false
}

// 判断是否以双辅音结尾
func endsWithDoubleConsonant(s []rune) bool {
	n := len(s)
	if n < 2 {
		return false
	}
	return s[n-1] == s[n-2] && isConsonant(s, n-1)
}

// 判断是否以cvc结尾
func endsWithCVC(s []rune) bool {
	n := len(s)
	if n < 3 {
		return false
	}
	return isConsonant(s, n-3) && !isConsonant(s, n-2) && isConsonant(s, n-1) && 
		s[n-1] != 'w' && s[n-1] != 'x' && s[n-1] != 'y'
}

// Porter词干提取主函数
func Stem(word string) string {
	s := []rune(strings.ToLower(word))

	if len(s) < 3 {
		return word
	}

	// 步骤1a
	if len(s) > 3 && s[len(s)-1] == 's' {
		switch s[len(s)-2] {
		case 'e':
			if len(s) > 4 && s[len(s)-3] == 'i' {
				s = s[:len(s)-3]
			} else if len(s) > 3 && s[len(s)-3] == 's' {
				// 什么都不做
			} else {
				s = s[:len(s)-1]
			}
		case 's':
			// 什么都不做
		default:
			s = s[:len(s)-1]
		}
	}

	// 步骤1b
	if measure(s) > 0 {
		if len(s) >= 4 && strings.HasSuffix(string(s), "eed") {
			s = s[:len(s)-1]
		} else if containsVowel(s[:len(s)-2]) && strings.HasSuffix(string(s), "ed") {
			s = s[:len(s)-2]
			// 处理特殊情况
			if strings.HasSuffix(string(s), "at") || strings.HasSuffix(string(s), "bl") || strings.HasSuffix(string(s), "iz") {
				s = append(s, 'e')
			} else if endsWithDoubleConsonant(s) && s[len(s)-1] != 'l' && s[len(s)-1] != 's' && s[len(s)-1] != 'z' {
				s = s[:len(s)-1]
			} else if measure(s) == 1 && endsWithCVC(s) {
				s = append(s, 'e')
			}
		} else if containsVowel(s[:len(s)-3]) && strings.HasSuffix(string(s), "ing") {
			s = s[:len(s)-3]
			// 处理特殊情况
			if strings.HasSuffix(string(s), "at") || strings.HasSuffix(string(s), "bl") || strings.HasSuffix(string(s), "iz") {
				s = append(s, 'e')
			} else if endsWithDoubleConsonant(s) && s[len(s)-1] != 'l' && s[len(s)-1] != 's' && s[len(s)-1] != 'z' {
				s = s[:len(s)-1]
			} else if measure(s) == 1 && endsWithCVC(s) {
				s = append(s, 'e')
			}
		}
	}

	// 步骤1c
	if len(s) > 0 && s[len(s)-1] == 'y' && containsVowel(s[:len(s)-1]) {
		s[len(s)-1] = 'i'
	}

	// 步骤2
	if measure(s) > 0 {
		switch {
		case strings.HasSuffix(string(s), "ational"):
			s = append(s[:len(s)-5], 'e')
		case strings.HasSuffix(string(s), "tional"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "enci"):
			s[len(s)-1] = 'e'
		case strings.HasSuffix(string(s), "anci"):
			s[len(s)-1] = 'e'
		case strings.HasSuffix(string(s), "izer"):
			s = s[:len(s)-1]
		case strings.HasSuffix(string(s), "abli"):
			s[len(s)-1] = 'e'
		case strings.HasSuffix(string(s), "alli"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "entli"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "eli"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ousli"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ization"):
			s = append(s[:len(s)-5], 'e')
		case strings.HasSuffix(string(s), "ation"):
			s = append(s[:len(s)-3], 'e')
		case strings.HasSuffix(string(s), "ator"):
			s = append(s[:len(s)-2], 'e')
		case strings.HasSuffix(string(s), "alism"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "iveness"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "fulness"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "ousness"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "aliti"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "iviti"):
			s = append(s[:len(s)-3], 'e')
		case strings.HasSuffix(string(s), "biliti"):
			s = append(s[:len(s)-5], 'e')
		}
	}

	// 步骤3
	if measure(s) > 0 {
		switch {
		case strings.HasSuffix(string(s), "icate"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ative"):
			s = s[:len(s)-5]
		case strings.HasSuffix(string(s), "alize"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "iciti"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ical"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ful"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ness"):
			s = s[:len(s)-4]
		}
	}

	// 步骤4
	if measure(s) > 1 {
		switch {
		case strings.HasSuffix(string(s), "al"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ance"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "ence"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "er"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ic"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "able"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "ible"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "ant"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ement"):
			s = s[:len(s)-5]
		case strings.HasSuffix(string(s), "ment"):
			s = s[:len(s)-4]
		case strings.HasSuffix(string(s), "ent"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ion"):
			if len(s) > 3 && (s[len(s)-4] == 's' || s[len(s)-4] == 't') {
				s = s[:len(s)-3]
			}
		case strings.HasSuffix(string(s), "ou"):
			s = s[:len(s)-2]
		case strings.HasSuffix(string(s), "ism"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ate"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "iti"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ous"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ive"):
			s = s[:len(s)-3]
		case strings.HasSuffix(string(s), "ize"):
			s = s[:len(s)-3]
		}
	}

	// 步骤5a
	if measure(s) > 1 && s[len(s)-1] == 'e' {
		s = s[:len(s)-1]
	} else if measure(s) == 1 && !endsWithCVC(s) && s[len(s)-1] == 'e' {
		s = s[:len(s)-1]
	}

	// 步骤5b
	if measure(s) > 1 && endsWithDoubleConsonant(s) && s[len(s)-1] == 'l' {
		s = s[:len(s)-1]
	}

	return string(s)
}

使用示例

package main

import (
	"fmt"
	"porterstemmer" // 假设上面的实现保存在porterstemmer包中
)

func main() {
	words := []string{
		"running", "jumps", "easily", "happily", "cats", "dogs", 
		"ponies", "caresses", "universal", "conditional",
	}

	for _, word := range words {
		stem := porterstemmer.Stem(word)
		fmt.Printf("%s -> %s\n", word, stem)
	}
}

性能考虑

对于大规模文本处理,建议:

  1. 重用Stemmer实例而不是每次创建新的
  2. 考虑并行处理多个单词
  3. 预处理文本时去除标点符号和特殊字符

Porter算法虽然简单高效,但它有一些局限性,比如无法处理不规则变形,也无法区分同形异义词。对于更高级的需求,可能需要考虑Snowball算法或其他更复杂的词干提取器。

希望这个实现对你有所帮助!

回到顶部