第3章:基于正则表达式的基础分词器实现

3.1 说到正则,别把它想得太玄乎

我记得刚入行那会儿,一看到“正则表达式”这几个字就头大。说白了,它就是一套描述字符串模式的规则。你用它对文本做匹配、切分、替换。想想看,我们在Word里用Ctrl+F搜索一个词,那就是最简单的匹配。正则就是把这个能力放大、加上了通配和逻辑。

我举个实际例子吧。假设你要把一句英文按空格和标点切开:

Input: "I love NLP; it's awesome!"
Output: ["I", "love", "NLP", "it's", "awesome"]

人眼一看就明白了,但机器不知道“;”和空格是分界,“'”不是。我们需要给机器一套规则——这就是正则分词器的本质。

3.2 分词器的核心思维:先匹配,再切分

基础分词器就两步:

  • 定义切分规则——用正则把“单词”和“分隔符”区分开
  • 按规则切分——把文本切成列表

我最早写分词器的时候犯过一个错误:直接按空格和标点一刀切,结果把“don't”切成了“don”和“t”。这显然不是我们想要的。所以,你设计的正则必须保留某些特殊字符(如撇号、连字符),同时切掉真正的分隔符。

核心原则:分词正则 = 保留规则 + 分隔规则。先定义什么是一个合法,再说怎么拆。

3.3 正则引擎的三种“暴力”基础

不管你在Python还是其他语言里,正则引擎都基于三种基础操作。我把它们整理成了一张表,你自己看:

操作 语法示例 含义
连接 ab 匹配a后紧跟b
选择 a|b 匹配a或b
重复 a* 匹配0个或多个a

听起来简单吧?但组合起来威力巨大。比如\b\w+\b就能匹配一个完整的单词。我建议你先把这三条背下来,不用背上百个元字符,核心就这三个。

3.4 评测指标:好不好的标准是什么?

分词器做完了,怎么知道好不好用?得看三个指标:

  • 准确率 (Precision):切出来的里,多少个是对的。
  • 召回率 (Recall):本该切出来的里,多少个被找到了。
  • F1值:上两个的调和平均。说白了就是综合得分。

我在项目中遇到过一种情况:分词器准确率挺高,但召回率低得可怜。为什么?因为正则写得太严了,少部分生僻词全漏了。这是正则分词器的天生短板——遇到词典外的新词就抓瞎。但别急,后面章节会讲统计和BPE来解决这个问题。

小窍门:开发时可以用#或|||作为特殊分隔符来对齐原文和分词结果,方便肉眼对比。我个人习惯在调试时先跑一个小样本,盯着看五条结果,比跑全量数据有效十倍。

3.5 手写一个正则分词器

咱们直接看代码。下面是我用Python实现的,核心就30行。你找个空文件敲一遍,跑通再说。

import re

class RegTokenizer:
    def __init__(self):
        # 匹配规则:保留字母、数字、撇号、连字符、下划线
        self._pattern = re.compile(r'\b\w[\w\'\-]*\w\b|\w')
    
    def tokenize(self, text):
        return self._pattern.findall(text.lower())

# 试试看
tokenizer = RegTokenizer()
sample = "I can't wait for the party — it's gonna be fun!"
tokens = tokenizer.tokenize(sample)
print(tokens)
# 输出: ['i', "can't", 'wait', 'for', 'the', 'party', "it's", 'gonna', 'be', 'fun']

我解释一下正则\b\w[\w'\-]*\w\b|\w:

  • \b是单词边界,帮我们定位一个单词的开始和结束
  • \w匹配字母、数字、下划线
  • [\w'\-]*匹配零个或多个字母、撇号、连字符
  • 最后的|\w是兜底——如果上面的规则匹配不上(比如单个字符),至少匹配一个字母

注意:你们看到的\w默认不包含中文字符。如果你要处理中文,得改成[\u4e00-\u9fff\w]。我曾经为了这个“不兼容”排查了半天,最后发现是Unicode字符类的问题。嗯,这确实是个常踩的坑。

3.6 可视化:正则分词流程

下面这张SVG图直观展示了整个流程。从原始文本输入开始,经过正则匹配引擎和模式库,最后生成序列。你仔细看看箭头之间的依赖关系,就明白为什么匹配是切分的前提了。

正则分词流程 原始文本 正则匹配引擎 序列 模式库 定界符/保留规则 输入: "I can't wait" 输出: ["i", "can't", "wait"] 核心:正则模式定了怎么匹配,就定了怎么切分 匹配引擎根据模式库中的规则,逐一匹配并提取

3.7 正则分词器的三大硬伤

我用了正则分词器一年,总结了三个逃不过的缺点:

  • 无法解决歧义:“这个局长”和“这个局/长”,正则永远选前者,它不理解上下文
  • 词典外词处理差:看到新造词“emoji高手”,正则可能会切错
  • 语言依赖强:英文分词规则和中文差异巨大,一套正则打不了全场

你可能会问:既然这么多问题,为什么还要学?我的回答是:正则分词是“起点中的起点”。它让你

  • 理解什么是分词空间
  • 掌握评测指标体系
  • 为后面学统计分词和BPE打下最朴素的基础

一句话总结:正则分词器是最好的“入门练手”项目,50行代码让你亲自感受什么是分词。但生产环境里,它只能做Simple Tokenization的前置清洗。

3.8 实战:写一个评测脚本来验证分词质量

光说不练假把式。我通常的做法是准备一份小样本,人工标注正确答案,然后用脚本计算Precision、Recall、F1。下面是一个简洁的实现:

def evaluate_tokenizer(tokenizer, texts, gold_tokens_lists):
    total_pred = 0
    total_gold = 0
    total_correct = 0
    for text, gold in zip(texts, gold_tokens_lists):
        pred = tokenizer.tokenize(text)
        total_pred += len(pred)
        total_gold += len(gold)
        total_correct += len(set(pred) & set(gold))
    precision = total_correct / total_pred if total_pred else 0
    recall = total_correct / total_gold if total_gold else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0
    return precision, recall, f1

# 举个例子
texts = ["I'll go; you stay.", "can't wait for the party"]
gold = [["i'll", "go", "you", "stay"], ["can't", "wait", "for", "the", "party"]]
prec, rec, f1 = evaluate_tokenizer(RegTokenizer(), texts, gold)
print(f"Precision: {prec:.2f}, Recall: {rec:.2f}, F1: {f1:.2f}")

你运行一下,就知道你的分词器到底什么水平了。我建议你把样本量增加到50条以上,包含标点、缩写、大写、数字、连字符等情况。这样测出来的分数才有意义。

好,这一章就到这里。你只要把正则核心三操作记牢,写一个评测脚本,剩下的就是调参数坑了。下次见。