text/scanner 不适合分词,它专为 Go 源码词法分析设计,无法按自定义分隔符或语义切分;应改用 bufio.Scanner 配 SplitFunc,或专用分词库如 tiktoken-go、gojieba。
别用 text/scanner 做分词 —— 它不是为这个设计的,强行用会切错、漏字、还难调试。
text/scanner 的目标是识别 Go 源码风格的 token:比如把 "a, b + 1" 拆成 IDENT、COMMA、ADD 等类型。它按语法边界走,不按空格、标点或自定义分隔符切。
常见误用现象:
Scan() 直接卡住或返回 ILLEGAL
"|||" 切分日志行,结果只扫到第一个 ||| 后就停了TokenText() 拿到的是带引号的字符串字面量(如 "hello"),不是干净的 hello
根本原因:text/scanner 的 Mode 默认跳过空白,且不提供“按任意字节序列切”的能力。它甚至不保证返回的 token 是用户语义上的“词”。
立即学习“go语言免费学习笔记(深入)”;
bufio.Scanner 配 SplitFunc
这才是真正对标 Python str.split("|||") 或 shell cut -d'|' 的做法。关键在自定义 SplitFunc,它必须正确处理两个边界:
atEOF == true:最后一段没分隔符,得整段返回len(data) < len(sep):缓冲区还没读满,不能贸然切,得返回 0, false 让 Scanner 继续读一个最小可用的 "|||" 分隔实现:
func splitOnTriplePipe(data []byte, atEOF bool) (advance int, token []byte, err error) {if atEOF && len(data) == 0 {return 0, nil, nil}if i := bytes.Index(data, []byte("|||")); i >= 0 {return i + 3, data[0:i], nil}if atEOF {return len(data), data, nil}return 0, nil, nil}
然后绑定:
scanner := bufio.NewScanner(r)scanner.Split(splitOnTriplePipe)for scanner.Scan() {fmt.Println(scanner.Text()) // 每次拿到一段,不含 |||}
大模型场景下的分词(比如对接 GPT 的 cl100k_base 编码表)和传统空格分词完全不同:它要 UTF-8 字节对齐、查合并规则表、处理特殊前缀(<|endoftext|>)、支持 byte fallback。
直接后果:
strings.Fields() 处理中文 → 全切成单字,语义全丢bufio.Scanner 配空格 Split → 对 “人工智能”、“iPhone15” 这类词完全无感tiktoken-go
推荐路径:
pkoukk/tiktoken-go,预载 cl100k_base.tiktoken 文件gojieba 或 sego,它们内置词典和 HMMstrings.SplitN(line, "|", 3) 更快更稳bufio.Scanner 默认 64KB 缓冲,遇到 base64 blob 或未换行的 JSON,scanner.Text() 会 panic 报 scanner: token too long。这不是 bug,是设计上的安全限制。
绕过方法只有两个:
bufio.NewReader + ReadString('n'),自己控制每次读多少字节if len(line) > 2*1024*1024 { continue },避免后续 strings.Contains 在超大字符串上反复扫描还有一个隐形坑:多个 goroutine 共享同一个 []byte 底层切片,file.Read(buf) 会覆盖前一次内容。发送前必须 copy(dst, src),别直接塞进 channel。
真正难的不是怎么切,而是切完之后要不要保留原始偏移、是否要支持回溯、是否要兼容多编码——这些细节一旦忽略,线上跑几天后就会发现某类日志永远搜不到。