100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package common
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// NormalizeKgTerm 知识图谱术语归一化(实体名与关系谓词共用,别名表由调用方传入)。
|
||
// 幂等:NormalizeKgTerm(NormalizeKgTerm(x)) == NormalizeKgTerm(x)。
|
||
// 规则保守,只做高置信合并,不做模糊相似度合并,防误合并("人民法院"与"最高人民法院"绝不合并)。
|
||
// 返回空串表示归一化后无意义(调用方应丢弃);原本合法但归一化后为空时保留原名(如"中华人民共和国")。
|
||
func NormalizeKgTerm(name string, aliasMap map[string]string) string {
|
||
orig := name
|
||
s := strings.TrimSpace(name)
|
||
s = fullToHalf(s)
|
||
s = strings.Join(strings.Fields(s), " ")
|
||
s = strings.Trim(s, "《》")
|
||
// 去版本注记括号及内容:内容含 年/修正/修订/施行/数字 才剥(人名限定语"张三(北京分公司)"不剥)
|
||
s = stripVersionParens(s)
|
||
s = strings.TrimRight(s, "。,;、!?,;!?.·。、")
|
||
s = strings.TrimSpace(s)
|
||
// 去"中华人民共和国"前缀(仅前缀;去完为空则保留原名,防实体"中华人民共和国"被删)
|
||
if strings.HasPrefix(s, "中华人民共和国") {
|
||
if rest := strings.TrimSpace(strings.TrimPrefix(s, "中华人民共和国")); rest != "" {
|
||
s = rest
|
||
}
|
||
}
|
||
// 别名全串精确映射(非 contains 替换,防"民诉法"吃掉"民诉法解释")
|
||
if std, ok := aliasMap[s]; ok && std != "" {
|
||
s = std
|
||
}
|
||
if s == "" && orig != "" {
|
||
return orig
|
||
}
|
||
return s
|
||
}
|
||
|
||
// fullToHalf 全角转半角(ASCII 可见区 U+FF01~U+FF5E → U+21~U+7E,全角空格 → 半角空格)
|
||
func fullToHalf(s string) string {
|
||
return strings.Map(func(r rune) rune {
|
||
switch {
|
||
case r == ' ':
|
||
return ' '
|
||
case r >= '!' && r <= '~':
|
||
return r - '!' + '\x21'
|
||
}
|
||
return r
|
||
}, s)
|
||
}
|
||
|
||
// stripVersionParens 剥去含版本注记的括号及内容。注记判定:内容含 年/修正/修订/施行 或任一数字。
|
||
func stripVersionParens(s string) string {
|
||
pairs := [][2]rune{{'(', ')'}, {'(', ')'}, {'【', '】'}, {'[', ']'}, {'〔', '〕'}}
|
||
var out strings.Builder
|
||
runes := []rune(s)
|
||
for i := 0; i < len(runes); {
|
||
stripped := false
|
||
for _, p := range pairs {
|
||
if runes[i] == p[0] {
|
||
end := findCloseParen(runes, i, p[1])
|
||
if end > i && isVersionNote(runes[i+1:end]) {
|
||
i = end + 1
|
||
stripped = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if !stripped {
|
||
out.WriteRune(runes[i])
|
||
i++
|
||
}
|
||
}
|
||
return out.String()
|
||
}
|
||
|
||
func findCloseParen(runes []rune, start int, close rune) int {
|
||
for j := start + 1; j < len(runes); j++ {
|
||
if runes[j] == close {
|
||
return j
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// isVersionNote 括号内容是否版本注记(年/修正/修订/施行 或数字)
|
||
func isVersionNote(content []rune) bool {
|
||
for _, r := range content {
|
||
if unicode.IsDigit(r) {
|
||
return true
|
||
}
|
||
}
|
||
text := string(content)
|
||
for _, kw := range []string{"年", "修正", "修订", "施行"} {
|
||
if strings.Contains(text, kw) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|