广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Python NLTK RegexpTokenizer如何同时提取所有单词和特定短语?

NLTK 的 RegexpTokenizer 和普通的空格切分最大的区别在于:它把分词逻辑交给正则表达式,而不是固定分隔符。也就是说,你想保留哪些 token、丢弃哪些符号,都可以在一条 pattern 里说清楚。这个特性在需要同时提取普通单词和特定多词短语时尤其有用,因为像 New York 或 machine learning 这样的短语如果先按空格切分,后续再想还原会非常麻烦。接下来从基本用法开始,逐步完成同时提取单词和特定短语的任务。

Python NLTK RegexpTokenizer如何同时提取所有单词和特定短语?

一、RegexpTokenizer 的基础机制

RegexpTokenizer 位于 nltk.tokenize 模块中,初始化时可以接收一个正则表达式,并通过 gaps 参数决定匹配内容是 token 还是分隔符。默认情况下 gaps=False,表示正则表达式匹配到的内容就是要保留的词;如果把 gaps 设为 True,那么正则匹配到的内容会被当作需要丢弃的空隙,token 则从这些空隙之间截取。

先看一个最简单的单词提取示例。假设文本是 Hello, world! New York is a city.,如果使用 r'\w+' 作为匹配模式,\w 会匹配字母、数字和下划线,连续匹配到的部分就成为一个 token。此时逗号、感叹号、空格都不会出现在结果里,但 New York 会被拆成 New 和 York 两个词。这个行为符合常规分词预期,却无法保留多词短语。

from nltk.tokenize import RegexpTokenizer
text = 'Hello, world! New York is a city. Machine learning helps.'
tokenizer = RegexpTokenizer(r'\w+')
print(tokenizer.tokenize(text))
# ['Hello', 'world', 'New', 'York', 'is', 'a', 'city', 'Machine', 'learning', 'helps']

如果改用 gaps 模式,把空格和标点当成空隙,同样可以得到普通单词。下面的写法使用 r'\s+' 作为分隔模式,正则匹配到的空白会被移除,剩下的非空白片段成为 token。但这种方式对短语依然无能为力,因为 New York 中间的空格本身就会被当成空隙切掉。理解这两种模式之后,就能更清楚地知道:要保留内部带空格的短语,必须回到默认的 match 模式,通过正则把它们整体匹配出来。

gaps_tokenizer = RegexpTokenizer(r'\s+', gaps=True)
print(gaps_tokenizer.tokenize(text))
# ['Hello,', 'world!', 'New', 'York', 'is', 'a', 'city.', 'Machine', 'learning', 'helps.']

二、用正则模式提取所有单词

提取所有单词并不是简单使用 \w+ 就一定能满足所有场景。英文文本里经常出现带连字符的复合词,例如 state-of-the-art、high-quality、e-commerce。如果只写 r'\w+',这些词会被拆成三到四段,因为连字符不属于 \w。要保留这类词,需要把连字符和其后的字母数字纳入同一个 token。

可以使用 r'\w+(?:-\w+)*' 来匹配普通单词以及带一个或多个连字符的复合词。这个正则先匹配一串 \w+,然后通过非捕获分组 (?:-\w+)* 允许出现零次或多次连字符加单词片段。这样 state-of-the-art 会作为一个整体出现,而不是 state、of、the、art 四段。对于带撇号的缩写,比如 don't 和 O’Reilly,需要根据语料情况决定是否保留撇号。如果希望保留 don't,可以把模式写成 r"\w+(?:[-']\w+)*",注意撇号用单引号表示即可。

text2 = 'This state-of-the-art model handles e-commerce and don\'t fail.'
word_tokenizer = RegexpTokenizer(r"\w+(?:[-']\w+)*")
print(word_tokenizer.tokenize(text2))
# ['This', 'state-of-the-art', 'model', 'handles', 'e-commerce', 'and', "don't", 'fail']

这里还涉及一个常见误区:很多人以为 NLTK 默认的分词器能自动保留所有英语缩写和复合词,实际上默认的 word_tokenize 会把 don't 拆成 do 和 n't,而 RegexpTokenizer 的行为完全取决于你提供的正则。因此在使用前需要先观察语料中的标点类型,再决定正则里要不要包含连字符、点号、撇号或数字范围。对于普通新闻文本,r'\w+(?:-\w+)*' 通常已经足够;对于医学或技术文本,可能还要允许斜杠、加号等符号出现在词内。

三、同时匹配特定短语的两种实现方式

要在提取所有单词的同时保留特定短语,核心思路是把短语的匹配优先级放到普通单词之前。正则引擎在遇到择一匹配时通常从左到右尝试,因此只要把 New York、machine learning 这类短语写在前面,再把 \w+ 或 r'\w+(?:-\w+)*' 作为兜底分支,就能优先得到完整短语。

下面是一个简单实现。假设要保留的短语是 New York 和 machine learning,可以把它们放进一个非捕获分组,并用 \b 限定边界,避免匹配到 Newark 或 learning 的片段。整个 pattern 为 r'\b(?:New York|machine learning)\b|\w+(?:-\w+)*'。这样文本中的 New York 会作为一个 token 输出,而不会被拆成 New 和 York。

phrase_pattern = r'\b(?:New York|machine learning)\b|\w+(?:-\w+)*'
phrase_tokenizer = RegexpTokenizer(phrase_pattern)
print(phrase_tokenizer.tokenize(text))
# ['Hello', 'world', 'New York', 'is', 'a', 'city', 'Machine learning', 'helps']

当短语列表较长时,手动拼接正则会变得难以维护。更推荐的做法是先把所有短语用 re.escape 转义,再按长度倒序排列,最后用竖线连接。按长度倒序很重要,因为如果有 New York 和 New York City 两个短语,正则引擎遇到 New York 时可能先匹配短的,导致 City 被单独留下。长短语写在前面可以避免这种截断。下面的示例展示了动态构造 pattern 的过程。

import re
from nltk.tokenize import RegexpTokenizer
phrases = ['New York', 'machine learning', 'New York City']
escaped = [re.escape(p) for p in sorted(phrases, key=len, reverse=True)]
phrase_part = '|'.join(escaped)
pattern = rf'\b(?:{phrase_part})\b|\w+(?:-\w+)*'
tokenizer = RegexpTokenizer(pattern)
text3 = 'He lives in New York City and studies machine learning.'
print(tokenizer.tokenize(text3))
# ['He', 'lives', 'in', 'New York City', 'and', 'studies', 'machine learning']

另一种实现方式是为短语建立单独的合并逻辑:先用普通分词器得到所有单词,再扫描 token 序列,把相邻且能组成短语的词合并。这种方式的优点是灵活性更高,可以在合并前加入词性标注或大小写判断;缺点是代码量更大,而且需要额外处理重叠短语。如果只是从文本中提取固定表达,优先使用单条正则更加直观,也更容易调试。

四、边界、大小写与性能注意事项

\b 是单词边界断言,它本身不消耗字符,只检查当前位置一侧是单词字符而另一侧不是。对于 New York 这种带空格短语,\b 能防止匹配到 Newark 或 new yorks 的片段。但 \b 对中文等无空格语言并不总是有效,因为中文字符是否算单词字符取决于 Unicode 设置。在处理中英混合文本时,如果发现短语边界异常,可以改用明确的上下文判断,例如要求短语前后必须是空白或标点。

大小写问题也经常影响短语提取。默认情况下正则区分大小写,所以 new york 不会匹配 New York。如果业务上希望忽略大小写,可以在初始化 RegexpTokenizer 时传入 flags=re.IGNORECASE。但要注意,忽略大小写后普通单词的输出也会变成小写吗?不会,匹配结果仍保留原文形式,只是匹配过程不再区分大小写。比如文本中的 Machine Learning 会被完整捕获,输出仍是 Machine Learning。示例写法如下:

tokenizer = RegexpTokenizer(pattern, flags=re.IGNORECASE)
print(tokenizer.tokenize('He is learning Machine Learning in new york.'))
# ['He', 'is', 'learning', 'Machine Learning', 'in', 'new york']

性能方面,只要短语数量不是特别大,单条正则通常不会有明显瓶颈。但如果短语列表达到几千甚至上万条,正则引擎在每次匹配时都要尝试大量分支,速度会下降。此时可以考虑对文本先做普通分词,再用哈希集合判断相邻 token 是否构成短语。对于大多数自然语言处理任务,短语数量在几十到几百之间时,RegexpTokenizer 的简洁性远远比微小的性能差异更重要。

最后还要注意,不要把 RegexpTokenizer 当成万能分词器。它在规律清晰、模式可控的文本上非常好用,但面对高度不规则的社交媒体文本、包含大量表情符号或错别字的内容,仍然需要结合预处理或更复杂的 NLP 管道。理解了正则匹配的优先级、边界和大小写策略,你就能根据实际文本灵活构造 pattern,在一次遍历中同时拿到普通单词和特定短语。

广告位 · 文章底部(后台-模块管理替换此区域)