// normalize.go:归一化与正则封装(safetyscan 内部实现细节)。 package safetyscan import ( "regexp" "strings" "unicode" "golang.org/x/text/unicode/norm" ) // regexWrap 正则包装(编译后只读)。 type regexWrap struct { re *regexp.Regexp pattern string } // compile 编译正则。 func compile(pattern string) (*regexWrap, error) { re, err := regexp.Compile(pattern) if err != nil { return nil, err } return ®exWrap{re: re, pattern: pattern}, nil } func (w *regexWrap) matchString(s string) bool { return w.re.MatchString(s) } func (w *regexWrap) replaceAll(s, repl string) string { return w.re.ReplaceAllString(s, repl) } // replaceGroup 捕获组替换:保留捕获组前后边界字符,仅把组内容替换为 repl。 func (w *regexWrap) replaceGroup(s string, _groupExpr, repl string) string { var b strings.Builder last := 0 for _, loc := range w.re.FindAllStringSubmatchIndex(s, -1) { // loc = [整起,整止, 组1起,组1止, 组2起,组2止, ...];尾边界组(如 [^0-9]|$) // 是第二个捕获组,可能匹配空串(索引为 -1),需逐项判空。 if len(loc) < 4 || loc[0] < 0 || loc[2] < 0 || loc[3] < 0 { continue } b.WriteString(s[last:loc[0]]) // 上一匹配结束到本匹配开始 b.WriteString(s[loc[0]:loc[2]]) // 组 1 前的边界字符 b.WriteString(repl) // 组 1 内容替换 if len(loc) >= 6 && loc[4] >= 0 && loc[5] >= 0 && loc[4] < loc[5] { b.WriteString(s[loc[4]:loc[5]]) // 组 2(尾边界字符) } last = loc[1] } if last == 0 { return s } b.WriteString(s[last:]) return b.String() } func (w *regexWrap) count(s string) int { return len(w.re.FindAllString(s, -1)) } // normalize NFKC 归一 + 小写 + 全角折叠(design §5.4:NFKC+同形字归一)。 func normalize(s string) string { s = norm.NFKC.String(s) // 全角→半角、兼容分解 s = strings.ToLower(s) // 常见混淆字符折叠(样例集,真实同形字表由合规方扩充) const from = ",。:;!?()「」" const to = ",.:;!()?()\"\"" runes := []rune(s) for i, r := range runes { if idx := strings.IndexRune(from, r); idx >= 0 && idx < len(to) { runes[i] = rune(to[idx]) } } _ = unicode.ToLower // 保留导入位(小写已统一走 strings.ToLower) return string(runes) }