3、基于规则的分词:空格分词、标点分词、正则分词

规则分词是令牌化里最“古老”也最直接的方法。你不需要任何统计学模型,也不需要神经网络的权重。说白了,就是告诉程序:遇到某个字符就切一刀。这种方法看起来简单,但我做了这么多年 NLP 项目,发现很多线上 bug 恰恰就出在“简单”二字上。

3.1 空格分词:最原始的做法

空格分词的核心逻辑就一句话:text.split()。Python 里默认的 split 会按空白字符(空格、制表符、换行)切分,然后返回一个列表。

适用场景:英文文本、代码 化、已经做过预处理的结构化文本。

# 一个例子,我做过英语新闻的分词
text = "The quick brown fox jumps over the lazy dog."
tokens = text.split()
print(tokens)
# ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog.']

请注意最后一个 是 "dog.",带了句点。这说明纯空格分词不会处理标点。我个人习惯在空格分词前先对标点做剥离,否则后面处理会很麻烦。

踩坑警告:我曾经在做一个多语言评论分析项目时直接用了空格分词处理中文。结果当然是一团糟——每个汉字变成了一个 ,连词都被拆散了。中文一句话里根本没有单词之间的空格。

3.2 标点分词:把标点当作天然边界

标点分词不是某种独立算法,而是一种思路:在遇到句号、逗号、问号、感叹号、引号、括号等标点符号时强制切分。通常和空格分词搭配使用。

你会想:不就是标点旁加个空格嘛?但实际的坑很多。比如 "U.S.A." 里面的点要不要切?"it's" 里的撇号是单词一部分还是标点?"3.14" 里的点代表小数点还是句号?

规则类型 例子 空格分词结果 标点分词结果(典型) 我推荐的策略
普通句尾 "Hello world." ["Hello", "world."] ["Hello", "world", "."] 分离标点
缩写点 "U.S.A." ["U.S.A."] ["U", ".", "S", ".", "A", "."] 保留缩写
所有格 "John's book" ["John's", "book"] ["John", "'s", "book"] 不拆撒克逊所有格
数字小数点 "3.14" ["3.14"] ["3", ".", "14"] 保留数字格式

嗯,这里你要注意:没有一种标点规则能同时覆盖所有语言和所有业务场景。我见过最离谱的 bug 是有人用标点分词处理化学式 "H₂SO₄",结果下标数字被当作标点切掉了,整个 序列完全没法用。

实用小技巧:在做标点分词之前,先做一层“保护性替换”——把需要保留的带点符号(缩写词、数字、版本号等)临时换成占位符,分完之后再替换回来。

3.3 正则分词:灵活但需要警惕

正则分词就是用正则表达式来定义“什么是一个 ”。你可以指定 的匹配模式,也可以指定分隔符的模式。我个人觉得这是规则分词里最灵活的工具,但也是最容易写出性能炸弹的方式。

举个最常见的例子——用 \w+ 匹配连续的字母数字和下划线:

import re

text = "快速(fast)的棕色 fox 跳过 lazy 狗。"
# 匹配连续的中文或英文单词
pattern = r'[\u4e00-\u9fff\w]+'
tokens = re.findall(pattern, text)
print(tokens)
# ['快速', 'fast', '的棕色', 'fox', '跳过', 'lazy', '狗']

# 注意 "的棕色" 被打包在一起了,因为"的"和"棕色"之间没有空格

为什么会这样?因为中文文本里没有空格,而我用的模式允许连续匹配中文字符和英文单词。如果你希望中文每个字单独成 ,就得换模式:[\u4e00-\u9fff]|[a-zA-Z]+。

正则分词的核心套路:

  • 正面匹配:用 re.findall(pattern, text) 直接提取 。
  • 分割匹配:用 re.split(delimiter_pattern, text) 在分隔符处切分。
  • 混合策略:先匹配需要保留的复杂 (如 URL、邮件),剩下的再按简单规则切。

我在项目中遇到过一件特别典型的事:一个日志解析脚本需要从服务器日志里提取 IP、时间、HTTP 方法、URL、状态码和响应时间。我当时做了一个正则,一次性匹配所有字段。结果上线后每天只跑几千条日志都卡爆了——因为正则引擎的回溯在长 URL 上指数级膨胀。最后我只能拆成多个小正则,分批处理,性能直接提升了 20 倍。

性能雷区:

  • 避免在长文本上使用 .* 或 .+ 贪婪匹配,尤其后面还有嵌套分组时。
  • 非必要不用 re.fullmatch 或 re.search 做全量匹配,能用 finditer 就用。
  • 对于长度未知的输入,考虑限制重复次数:{1,100} 而不是 *。

3.4 三种方法的对比与组合

我把它们列成一个对比表,你写代码时可以直接对照参考:

维度 空格分词 标点分词 正则分词
实现复杂度 极低(一行 split) 低(预处理+split) 中等(正则表达式)
性能 最快 很快 看表达式复杂度
中文支持 不支持 不直接支持 可以支持
特殊 保留 不能 需要预处理 容易配置
维护成本 几乎为零 低 高(新规则要改正则)

你想想看,在实际生产里很少有人只用一种规则。我通常的做法是:

  1. 先用正则提取出需要保护的结构(URL、邮件、金额、日期等)。
  2. 对剩余文本做标点分词(分离标准标点,保留缩写和数字)。
  3. 再按空格拆分,得到 序列。

这种“三层递进”的方法我用了好几年,在英文、法文、德文、日文(假名部分)上都效果不错。不过中文的话,嗯,还是要靠更高级的分词器,后面章节会讲到。

一个真实的避坑案例:我曾经接手一个电子邮件分析系统,之前的同学用 re.split(r'\s+|[.,!?;:()]', text) 来做分词。结果所有邮件的

基于规则的分词流程:三层递进策略 原始文本输入 第一层:正则保护 提取URL、邮件、金额、日期等复杂实体 第二层:标点分词 分离标准标点 + 保护缩写/数字/所有格 第三层:空格分词 text.split() 得到最终 序列 列表 → 后续 NLP 流程 注意顺序不能乱 先保护 → 再标点 → 最后空格 否则保护对象会被提前切碎 性能提示 正则越长越慢 优先用 finditer 而不是 findall

到这里你应该已经清楚了:规则分词的本质是“人写规则,机器执行”。它没有学习过程,所以边界情况全靠你提前想到。我个人建议永远把规则分词作为流水线的第一道工序,只做粗粒度切分。后面的精细处理交给更智能的分词器。别想着一条正则打天下——我试过,改了一个月,最后还是改成多级流水线了。

一个小经验:规则分词的代码一定要写单元测试。把你能想到的所有边界用例(空字符串、连续多个标点、Unicode 特殊字符、数字、缩写、中英文混排)都写成测试用例。每次改规则,跑一遍测试。否则上线后你会被用户反馈折磨到怀疑人生。