基于规则的Tokenization:正则表达式、字典匹配、最大匹配法

Tokenization 的第一步,往往不是上模型,而是先拿规则顶上。说白了,规则就是人工写死的模式。你告诉程序:“看到这个就切一刀”。

我最早做 NLU 项目的时候,就是靠正则和字典撑起来的。数据量不大,规则够用,跑得又快。今天我们就聊聊这三板斧:正则表达式、字典匹配、最大匹配法。

正则表达式直接分词

用正则做 tokenization,最简单,也最容易翻车。我的习惯是——先确定你的 长什么样。

比如英文,基本就是 \w+ 或者更细致的 [A-Za-z]+。中文呢?单字、双字、人名、地名,正则覆盖面有限。

import re

text = "I love Python, and it's great!"
tokens = re.findall(r"\w+", text)
print(tokens)
# ['I', 'love', 'Python', 'and', 'it', 's', 'great']

注意到 it's 被拆成了 it 和 s。这就是正则的局限——它不懂语法,只认模式。

你想想看,如果要处理「don't」「can't」这类缩写,正则就得写 \w+(?:'\w+)?。规则越堆越多,维护成本就上去了。

我的建议是:正则适合做粗切或预处理。比如去掉标点、过滤数字。别指望它搞定一切。

字典匹配:查表分词

字典匹配比正则聪明一点。你把所有可能的词装进一个集合(set),然后扫描文本,看哪个连续片段在字典里。

但这里有个工程问题——怎么确定长度?你从头扫,每次取多长去查?这就是最大匹配法要解决的。

  • 正向最大匹配:从左向右,先取最长可能词,匹配不到就缩短。
  • 逆向最大匹配:从右向左,同理。
  • 双向最大匹配:两个方向都跑,选结果更优的那个。

我记得有一次做中文地址解析,用正向最大匹配总把「北京市朝阳区」切成「北京/市朝/阳区」。换了逆向匹配才正确分出「北京市/朝阳区」。所以双向匹配并不是摆设。

class DictTokenizer:
    def __init__(self, words, max_len=5):
        self.dict = set(words)
        self.max_len = max_len

    def forward_max_match(self, text):
        tokens = []
        i = 0
        while i < len(text):
            matched = text[i]
            for j in range(min(self.max_len, len(text) - i), 0, -1):
                word = text[i:i+j]
                if word in self.dict:
                    matched = word
                    break
            tokens.append(matched)
            i += len(matched)
        return tokens

最大匹配法的核心假设是:字典覆盖了所有合理词。但新词、未登录词会直接拆成单字。我曾经被一个产品名「阿帕奇」坑过,字典里只有「阿帕」「奇」,结果分词全乱。

三种方法对比

下面这个表格,帮你一眼看清差异:

方法优点缺点适用场景
正则表达式简单、快、无外挂规则难维护、对新词不友好预清洗、简单英文
字典匹配(正向/逆向)基建要求低、可控对字典质量极度依赖、长度偏好封闭领域的专业分词
双向最大匹配精度相对高、消歧能力强速度翻倍、实现稍复杂通用场景、快速基线

说白了,规则型词法分析器的天花板就是你的字典。你投入多少人力维护字典,它就回报多少准确率。

基于规则的分词流程

下面这张图,是我自己做项目时常用的决策流程。先试正则过滤噪声,再用字典匹配做核心切分,最后用后处理修正明显错误。

基于规则的 Tokenization 流程 原始文本 正则预处理(去噪、标准化) 选择匹配策略 正向最大匹配 逆向最大匹配 双向融合 序列 注意:双向匹配会对比正向/逆向结果,选择切分数量少、单字少的那一版。 如果两个方向结果一致,误差最低。

经验之谈:我在线上部署过一个 OCR 后处理管线,纯规则分词,字典只有 2 万条,速度跑到了 1ms/句。但一旦遇到未登录词就崩。后来我加了个“最小单字长度”的后处理——连续单字超过 3 个就触发特殊规则。嗯,勉强能用。

实战中的小心思

  • 字典加载:用 set 或 frozenset,普通 list 会导致 in 操作变成 O(n)。
  • 最大长度设置:通常取字典中最长词的长度,或者硬编码 5~10。太长会引入大量无意义匹配。
  • 中文编码:如果你处理中文,Python 3 的 str 是 Unicode,可以直接索引字符,但要注意中文字符宽度不等于1个字节。
  • 性能优化:字典匹配可用前缀树(Trie)来加速,但如果你字典不大(<10万),set+遍历也够。

我个人习惯是:先用正则把 URL、邮箱、数字这些“强模式”提走,剩下部分交给字典匹配。这样字典匹配的压力小很多,准确率也稳。

基于规则的 Tokenization 虽然古老,但在工业界的冷启动阶段、或者领域极度垂直的场景下,依然是最可靠的方案。记住一句:规则定了,天花板就定了。想突破,就得往统计和模型走。但那是后面的话题了——我们先把规则玩透。