AB
AiBoss
Tutorials

NLTK

Tutorials

用 NLTK 完成英文文本预处理与词性标注

面向刚接触自然语言处理的 Python 开发者,讲解如何安装 NLTK、下载语料与模型数据,并完成分词、去停用词、去标点、词干提取、词形还原、词性标注、命名实体识别与关键词提取等一整套英文文本处理流程,附一个可从头跑通的完整示例。

NLTK(Natural Language Toolkit)是一个 Python 自然语言处理库,提供分词、停用词过滤、词干提取与词形还原、词性标注、命名实体识别、语料库与词典资源访问、n-gram 建模、文本相似度计算等一组工具。它适合两类人:一类是刚开始学 NLP、想先把「一段英文文本怎么变成结构化数据」这条链路走通的人;另一类是做教学、做原型验证、需要现成语料和词典资源的人。它的定位偏向教学与实验,不是为高吞吐生产管线设计的,这一点在选型时要清楚。如果你只是想快速试一下分词和词性标注的效果,可以直接打开 NLTK 对应的工具页对照着看输出。

下面按「装好环境 → 下载数据 → 逐步处理文本 → 跑一个完整例子」的顺序展开。所有代码都在 Python 交互环境或脚本里直接执行,不需要额外的服务端组件。

准备工作

Python 环境

NLTK 是纯 Python 包,通过 pip 安装即可。建议在虚拟环境里操作,避免和系统里其他项目的依赖互相干扰:

python -m venv nltk-env
source nltk-env/bin/activate
pip install nltk

Windows 下激活虚拟环境的命令是 nltk-env\Scripts\activate。安装完成后可以在解释器里确认版本:

import nltk
print(nltk.__version__)

具体版本号会随发布变化,以安装时 PyPI 上的当前版本为准。

下载语料与模型数据

这是 NLTK 新手最容易卡住的一步。库本身装好之后,分词器、停用词表、词性标注模型、WordNet 词典这些数据并不在包里,需要单独下载。最直接的方式是打开下载器:

import nltk
nltk.download()

会弹出一个图形界面,在列表里勾选需要的包再点下载。如果是在没有图形界面的服务器上,用命令行方式按名字下载更稳妥:

import nltk

for pkg in ["punkt", "punkt_tab", "stopwords", "wordnet", "omw-1.4", "averaged_perceptron_tagger", "averaged_perceptron_tagger_eng", "maxent_ne_chunker", "maxent_ne_chunker_tab", "words"]:
    nltk.download(pkg)

这几个包分别对应:punkt 与 punkt_tab 是句子和词的分词模型,stopwords 是停用词表,wordnet 与 omw-1.4 是词形还原要用的词典数据,averaged_perceptron_tagger 系列是词性标注模型,maxent_ne_chunker 系列是命名实体识别模型,words 是词表。不同版本的 NLTK 对模型包命名有过调整,如果运行时报「Resource not found」,按报错信息里给出的资源名补下载即可。

下载的数据默认落在用户目录下的 nltk_data 文件夹里。如果机器上有多个 Python 环境,或者想把数据放到共享位置,可以用 nltk.data.path 查看当前的搜索路径,再用 nltk.download(..., download_dir="/your/path") 指定目录,并在代码里把该目录追加进 nltk.data.path。

准备一段测试文本

后面的所有步骤都基于一段英文文本。为了便于对照结果,建议用一段包含专有名词、缩写和标点的短文本,例如:

text = "Apple Inc. is looking at buying a U.K. startup for $1 billion. Tim Cook said the deal would close in early 2024."

操作步骤

第一步:分句与分词

分词是把整段文本切成句子、再把句子切成词。NLTK 的 sent_tokenize 会处理句号、问号、感叹号等边界,并且对「Inc.」「U.K.」这类缩写有专门处理,不会在缩写点处错误断句:

from nltk.tokenize import sent_tokenize, word_tokenize

sentences = sent_tokenize(text)
print(sentences)

words = word_tokenize(text)
print(words)

word_tokenize 会把标点单独切成 token,例如 $、.、, 都会成为独立元素。这一点在后续做词频统计时要注意,标点需要单独过滤。

如果默认分词规则不满足需求,NLTK 还提供几种可替换的分词器:

  • WordPunctTokenizer:按「字母序列」和「非字母序列」切分,规则简单,对缩写不友好。
  • RegexpTokenizer:用正则表达式定义 token 的形态,适合只保留特定字符集的场景,例如 RegexpTokenizer(r"\w+") 只保留字母数字。
  • LineTokenizer:按行切分,适合日志、逐行语料。
  • SpaceTokenizer:只按空白切分,适合已经做过预处理的文本。
from nltk.tokenize import RegexpTokenizer

tokenizer = RegexpTokenizer(r"\w+")
print(tokenizer.tokenize(text))

第二步:去除停用词与标点

停用词是 the、is、at、for 这类高频但语义贡献低的词。NLTK 自带一份英文停用词表:

from nltk.corpus import stopwords

stop_words = set(stopwords.words("english"))
print(len(stop_words))

过滤时通常和标点过滤一起做。一个常见写法是先统一小写,再判断是否属于停用词、是否为纯字母:

filtered = [w.lower() for w in words if w.lower() not in stop_words and w.isalpha()]
print(filtered)

需要注意,isalpha() 会把 $1、2024 这类数字 token 一并去掉。如果业务上需要保留数字(比如金额、年份),就换成更细的过滤条件,例如只排除纯标点 token。

默认停用词表不一定覆盖你的领域。比如做医疗文本时,「patient」「study」可能也需要当作停用词。这时可以自己扩充:

custom_stop = stop_words.union({"said", "would", "inc"})
filtered = [w.lower() for w in words if w.lower() not in custom_stop and w.isalpha()]

反过来,如果某些词被默认表误伤,用集合差集把它们移出去即可。

第三步:词干提取与词形还原

这两个操作目的相似——把词的屈折形式归并到同一个代表形式——但机制不同。词干提取是机械地砍掉词缀,速度快,结果可能不是真实单词;词形还原会查词典,结果一定是合法词形,但更慢,且依赖词性信息。

词干提取有几种算法可选,最常用的是 Porter 和 Snowball:

from nltk.stem import PorterStemmer, SnowballStemmer

porter = PorterStemmer()
snowball = SnowballStemmer("english")

for w in ["running", "runs", "runner", "studies", "studied"]:
    print(w, porter.stem(w), snowball.stem(w))

Snowball 是 Porter 的改进版,支持多种语言,通过构造参数指定语言即可。

词形还原用 WordNet 数据,默认按名词处理,所以最好显式传入词性:

from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet

lemmatizer = WordNetLemmatizer()
print(lemmatizer.lemmatize("running", pos=wordnet.VERB))
print(lemmatizer.lemmatize("better", pos=wordnet.ADJ))
print(lemmatizer.lemmatize("mice", pos=wordnet.NOUN))

如果只写 lemmatizer.lemmatize("running"),得到的是 running 本身,因为默认按名词查。这是很常见的困惑点。

第四步:词性标注

词性标注给每个 token 打上名词、动词、形容词等标签。NLTK 默认使用感知机标注器:

import nltk

tagged = nltk.pos_tag(words)
print(tagged)

输出是 (词, 标签) 的列表,标签采用 Penn Treebank 体系,例如 NN 单数名词、NNS 复数名词、VB 动词原形、VBG 动名词、JJ 形容词、IN 介词、CD 基数词。完整标签集可以查 Penn Treebank 的标签说明。

标注器可以组合使用。NLTK 支持 backoff 链:先用一个标注器,遇到它不认识的词再交给下一个。例如把 UnigramTagger 和默认标注器串起来:

from nltk.tag import UnigramTagger
from nltk.corpus import treebank

train_sents = treebank.tagged_sents()[:3000]
unigram = UnigramTagger(train_sents, backoff=nltk.DefaultTagger("NN"))
print(unigram.tag(word_tokenize("The quick brown fox jumps")))

这里用 Treebank 语料的前 3000 句做训练,剩下的可以留作评估。训练数据量、切分比例都会影响效果,实际项目里要按自己的语料重新训练。

第五步:命名实体识别

在词性标注结果之上,NLTK 的 ne_chunk 可以识别人名、组织名、地名等实体:

tagged = nltk.pos_tag(word_tokenize(text))
entities = nltk.ne_chunk(tagged)
print(entities)

输出是一棵语法树,实体以 Tree 节点形式出现,节点标签是 PERSON、ORGANIZATION、GPE 等。要提取实体名,遍历树即可:

from nltk import Tree

for subtree in entities:
    if isinstance(subtree, Tree):
        name = " ".join(w for w, t in subtree.leaves())
        print(subtree.label(), name)

也可以传入 binary=True,让输出只区分「是实体 / 不是实体」,不细分类型。

第六步:n-gram 与词频统计

n-gram 是把连续 n 个 token 当作一个单元,常用于语言建模和特征构造:

from nltk import bigrams, trigrams
from nltk.util import ngrams

print(list(bigrams(filtered)))
print(list(ngrams(filtered, 3)))

词频统计用 FreqDist:

from nltk import FreqDist

fdist = FreqDist(filtered)
print(fdist.most_common(10))
print(fdist["apple"])

FreqDist 还提供 hapaxes() 返回只出现一次的词,plot() 画频率分布图(需要 matplotlib)。

第七步:关键词提取

NLTK 没有内置的关键词提取器,但可以基于词频和词性组合出一个简单方案:只保留名词和形容词,按词频排序。更完整的做法是计算 TF-IDF,NLTK 提供 TextCollection 来算:

from nltk.text import TextCollection

docs = [filtered, ["another", "document", "about", "apple", "startup"]]
corpus = TextCollection(docs)
for word in set(filtered):
    print(word, corpus.tf_idf(word, filtered))

TF-IDF 值高的词在单篇文档里出现多、在整个语料里出现少,通常就是这篇文档的关键词。

第八步:文本相似度与语义分析

比较两段文本的相似度,可以用 WordNet 的路径相似度,也可以用 Jaccard 之类的集合指标。WordNet 方式:

from nltk.corpus import wordnet as wn

dog = wn.synset("dog.n.01")
cat = wn.synset("cat.n.01")
print(dog.path_similarity(cat))
print(dog.wup_similarity(cat))

path_similarity 基于两个同义词集在 WordNet 层级中的最短路径,wup_similarity 基于最近公共祖先的深度。两者返回 0 到 1 之间的值,越大越相似。要查一个词的所有同义词集,用 wn.synsets("dog");查同义词和反义词用 lemma.antonyms() 和 synset.lemmas()。

一个完整示例

下面把前面的步骤串成一条完整流水线,输入一段英文文本,输出清洗后的词表、词性标注、命名实体和关键词。可以直接保存为 pipeline.py 运行:

import nltk
from nltk.tokenize import sent_tokenize, word_tokenize
from nltk.corpus import stopwords, wordnet
from nltk.stem import WordNetLemmatizer
from nltk import FreqDist, Tree

# 1. 确保数据就绪
for pkg in ["punkt", "punkt_tab", "stopwords", "wordnet", "omw-1.4",
            "averaged_perceptron_tagger", "averaged_perceptron_tagger_eng",
            "maxent_ne_chunker", "maxent_ne_chunker_tab", "words"]:
    nltk.download(pkg, quiet=True)

text = ("Apple Inc. is looking at buying a U.K. startup for $1 billion. "
        "Tim Cook said the deal would close in early 2024.")

# 2. 分句与分词
sentences = sent_tokenize(text)
words = word_tokenize(text)
print("句子数:", len(sentences))
print("token 数:", len(words))

# 3. 去停用词与标点
stop_words = set(stopwords.words("english"))
filtered = [w.lower() for w in words if w.lower() not in stop_words and w.isalpha()]
print("过滤后:", filtered)

# 4. 词形还原(先标注词性,再把标签映射到 WordNet 词性)
def to_wordnet_pos(tag):
    if tag.startswith("J"):
        return wordnet.ADJ
    if tag.startswith("V"):
        return wordnet.VERB
    if tag.startswith("N"):
        return wordnet.NOUN
    if tag.startswith("R"):
        return wordnet.ADV
    return wordnet.NOUN

tagged = nltk.pos_tag(words)
lemmatizer = WordNetLemmatizer()
lemmas = [lemmatizer.lemmatize(w.lower(), pos=to_wordnet_pos(t))
          for w, t in tagged if w.isalpha()]
print("词形还原:", lemmas)

# 5. 词频
fdist = FreqDist(lemmas)
print("高频词:", fdist.most_common(5))

# 6. 命名实体
entities = nltk.ne_chunk(tagged)
for subtree in entities:
    if isinstance(subtree, Tree):
        name = " ".join(w for w, t in subtree.leaves())
        print("实体:", subtree.label(), name)

# 7. 二元组
from nltk import bigrams
print("二元组:", list(bigrams(filtered)))

运行后应当能看到句子被正确切成两句(「Inc.」和「U.K.」没有造成误断),实体部分识别出组织名和地名,高频词里出现 apple、startup、deal 之类的词。如果某一步报资源缺失,回到准备工作一节按报错里的资源名补下载。

注意事项

数据包必须单独下载

NLTK 的模型和语料不在 pip 包里,这是最常见的报错来源。报错信息通常是 LookupError: Resource ... not found,里面会直接给出资源名,照着 nltk.download("资源名") 即可。部署到新机器或容器时,要把下载步骤写进构建流程,否则运行时才失败。

词形还原要传词性

不传 pos 参数时默认按名词处理,动词和形容词的还原结果会不正确。正确做法是先做词性标注,再把 Penn Treebank 标签映射到 WordNet 的 ADJ、VERB、NOUN、ADV 四类,如上面示例中的 to_wordnet_pos 函数。

停用词表与标点过滤会误伤

isalpha() 会去掉所有数字 token,金额、年份、编号都会丢失。停用词表是通用英文表,领域文本里可能需要增删。过滤前统一小写会丢失大小写信息,如果后续要做命名实体识别,注意不要在小写化之后再做,否则专有名词线索就没了。

分词器对缩写的处理

word_tokenize 会把 U.K. 切成 U.K. 一个 token,但把 Inc. 的句点保留在 token 内。不同分词器行为不同,RegexpTokenizer(r"\w+") 会把 U.K. 拆成 U 和 K。选分词器时要先看目标文本的特点。

模型包命名随版本变化

NLTK 在版本演进中对部分模型包做过重命名和拆分(例如标注器与分块器出现了带 _tab 后缀的版本)。写死的下载列表在升级后可能失效,稳妥的做法是捕获 LookupError 后按报错里的资源名动态下载,或者把下载步骤放在部署脚本里单独维护。具体包名与版本对应关系以 NLTK 官方当前发布信息为准。

性能与适用范围

NLTK 的标注器和分块器是为教学与实验设计的,在长文本、大批量数据上的速度不如专门优化的库。做原型、做课程作业、做小规模文本分析没问题;如果要处理百万级文档或要求低延迟,需要评估是否换用其他方案。另外,NLTK 自带的分词、标注、实体模型主要面向英文,其他语言的支持程度不一,使用前要确认目标语言有没有对应模型。

语料库的许可与用途

NLTK 附带的语料(如 Treebank、WordNet)各有自己的许可条款,用于商业项目前要逐一确认。WordNet 本身可以免费使用,但其他语料的限制不同。具体条款以各语料随附的说明为准。

下载源与网络环境

nltk.download() 默认从 NLTK 的数据服务器拉取。在网络受限的环境里可能失败,此时可以手动下载数据包放到 nltk_data 目录下对应的子目录中,再用 nltk.data.path 确认路径被识别。目录结构按资源类型分(tokenizers、corpora、taggers、chunkers 等),放错子目录同样会报找不到资源。