第3章 基于规则的分词:正则表达式与自定义规则实践
规则分词是 Tokenization 里最“笨”但也最可控的方法。说白了,就是你自己写正则规则,告诉程序“看到这种模式就切一刀”。我刚入行时觉得这东西太 low,直到在一个电商搜索项目中,碰到一堆品牌名和型号组合——什么 iPhone14ProMax、AK-47、SU-30MKK……模型分词全乱套,最后还是靠几行正则稳住了局面。
3.1 正则表达式分词基础
正则分词的核心理念很简单:定义一个或多个模式,匹配文本中的“词”或“子词”。Python 的 re.findall 是最常用的工具,它会返回所有匹配项,自动按匹配位置切分。
先从最基础的开始。假如你只想要连续的字母和数字,可以用:
import re
text = "Hello, world! I'm tokenizing 3 strings."
tokens = re.findall(r'\w+', text)
print(tokens)
# ['Hello', 'world', 'I', 'm', 'tokenizing', '3', 'strings']
你看,\w+ 把标点、空格都跳过了。但有个问题——I'm 被切成了 I 和 m。这在中文语境下可能无所谓,但英文里有些缩写需要保留原样。
我个人习惯从 \w+ 起步,然后逐步加规则兜底。比如匹配带撇号的单词:
tokens = re.findall(r"\w+(?:'\w+)?", text)
# ['Hello', 'world', "I'm", 'tokenizing', '3', 'strings']
(?:'\w+)? 表示可选的后缀,这样 I'm 就保留了。这个技巧我在处理英文文档时经常用。
3.2 自定义规则:处理边缘情况
真实文本永远比教科书复杂。数字、百分比、日期、URL、邮箱……每条规则都可能被你写进正则。我建议你准备一个规则优先级表,按“先特殊后一般”的顺序匹配。
下面这张表是我项目里常用的规则清单:
| 场景 | 正则模式 | 说明 |
|---|---|---|
| 整数/浮点数 | \d+(?:\.\d+)? |
匹配 42 或 3.14,不会吃掉小数点后的数字 |
| 带连字符的复合词 | [A-Za-z]+(?:-[A-Za-z]+)+ |
匹配 well-known、state-of-the-art |
| 电子邮件 | \S+@\S+\.\S+ |
简单版,实际项目需要更严谨 |
| URL | https?://\S+ |
先匹配 URL,防止被后续规则切碎 |
| 中文连续汉字 | [\u4e00-\u9fff]+ |
匹配纯中文词语 |
| 非空格连续字符 | \S+ |
最后兜底,捕获所有未匹配的非空白字符 |
规则顺序很重要。我曾经踩过一个坑:先写了 \S+ 兜底,结果邮件地址没被单独识别,直接被整体切成一坨。后来我把 URL、邮箱、数字这些特殊模式前置,剩下再给 \S+ 处理,才对了。
3.3 实战:构建一个中英文混合分词器
假设你要处理一段中英文混排的文本,比如 “iPhone14 在2023年卖了 10.5 亿美元,同比+12%”。需求是把中文词语、英文单词、数字、百分比都独立切出来,但不要拆散 “iPhone14” 里的内部数字。
我是这样干的:
- 定义规则列表(按优先级顺序):
URL→Email→带可选后辍的英文单词→浮点数/百分比→中文连续汉字→单个非空白字符 - 合并所有规则:用
|连接,并用非捕获分组包裹。 - 用
re.finditer获取每个匹配的位置,避免遗漏。
代码示例:
import re
def regex_tokenize(text):
# 规则按优先级从高到底排列
patterns = [
r'https?://\S+', # URL
r'\S+@\S+\.\S+', # 邮箱
r"\w+(?:'\w+)?", # 英文单词(含缩写)
r'\d+(?:\.\d+)?%?', # 数字/浮点/百分比
r'[\u4e00-\u9fff]+', # 中文
r'\S', # 兜底:单个非空白字符
]
# 用 | 连接
combined = '|'.join(f'({p})' for p in patterns)
tokens = [m.group() for m in re.finditer(combined, text)]
return tokens
text = "iPhone14 在2023年卖了 10.5 亿美元,同比+12%。"
print(regex_tokenize(text))
# ['iPhone14', '在', '2023', '年', '卖', '了', '10.5', '亿', '美元', ',', '同比', '+', '12', '%', '。']
你看,iPhone14 整体保留了,因为 \w+ 规则匹配了字母+数字;10.5 也是整体;+12% 被切成了 + 和 12%——这符合预期吗?如果你希望“+12%”整体保留,可以再加一条 [+\-]?\d+(?:\.\d+)?%? 放在数字规则前面。
规则分词的灵活就在这里:想怎么切,全凭你的规则。但代价是——你永远在打补丁。
3.4 规则分词的局限性
⚠ 上下文相关的问题,规则很难处理。
比如 “Apple 发布了 new MacBook” 和 “The apple is red”。规则无法区分 “Apple” 是品牌名还是水果,它只能按同样的模式切出来。我曾经在新闻舆情分析中,把 “特朗普” 切成了 “特” “朗” “普” 三块,因为我的中文规则只匹配单个汉字……后来才知道 [\u4e00-\u9fff]+ 可以匹配连续汉字,根本不用写 [\u4e00-\u9fff]。
💡 什么时候该放弃规则?
当你的规则列表超过 20 条,而且还在不断新增“特例”时,就该考虑用统计分词(比如 BPE、WordPiece 等)了。规则分词适合:文本格式固定、领域词汇明确、对切分结果有精确控制需求的场景。
还有一点:规则分词对中文的“歧义切分”无能为力。比如 “下雨天留客天天留我不留” 这种句子,不同断句意思完全不同——规则只能按你写死的模式切,不可能做语义理解。
总结一下:规则分词是我们工具箱里最廉价、最可控的工具。用它处理格式规范的文本(比如日志、英文文档、化学公式)效率极高;但面对自然语言的歧义,它只能保证你“不错过大错”,做不到“每次都完美”。做好心理准备——规则永远会有漏洞,这就是为什么后面我们还要学 BPE 和 SentencePiece。