做自然语言处理(NLP)这几年,最常被新人问到的就是:“我想入门NLP,应该先学NLTK还是Spacy?”说实话,这个问题没有标准答案,因为这两个库压根不是同一个物种。NLTK是学术界的教学老前辈,Spacy是工业界的工程利器,但新手往往默认它们是“同类工具”,然后选了其中一个就开始啃,结果不是被语料下载搞到崩溃,就是被文档绕晕。
这篇内容我准备换个讲法:用同一个入门者的视角,把NLTK和Spacy放在一起拆开揉碎对比着学。从环境搭建、语料处理、分词、词性标注,到命名实体识别,全程用真实可跑的代码和踩坑记录说话。你不需要任何NLP基础,只要会一点Python,跟着走一遍,就能搞清楚什么时候用NLTK,什么时候用Spacy,以及这两个库各自解决什么问题。这套路线我自己带新人走了很多遍,今天直接分享给你。
1. 为什么NLP入门绕不开NLTK和Spacy
1.1 NLTK:教学科研的常青树
NLTK全称Natural Language Toolkit,诞生于2001年,是宾夕法尼亚大学计算机系推动的项目。它最厉害的地方不是算法多先进,而是把大量经典语料和语言学资源打包在了一起——Brown语料库、Reuters语料库、Gutenberg文学语料、WordNet词汇数据库、停用词库、情感分析词典,全都有。你在NLP论文里看到的经典实验,几乎都能在NLTK里找到对应材料。
因为是教学工具,NLTK的方法设计得很“教科书”。比如分词有word_tokenize,词性标注有pos_tag,命名实体识别有ne_chunk,每一步都是单独的函数调用,结果出来是什么结构也很直观。它适合用来理解NLP的每一步在做什么,也适合快速验证明文论文里的算法思想。
但NLTK的短板同样明显。它的很多模块依赖正则规则和外部语料资源,遇到新词、网络用语、非规范文本时表现会比较差。另外速度偏慢,处理几十万条文本时能明显感觉到卡顿。我早期拿它做过一个新闻语料清洗任务,跑了快二十分钟,后来换Spacy重写,几分钟就完了。所以它有很强的学习价值,但直接上生产环境就要慎重。
1.2 Spacy:工程落地的工业级选择
Spacy是另一条路线。它从一开始就是奔着“产品能用”去的,核心卖点是速度、准确率和统一的管道式设计。你加载一个模型之后,它会自动完成分词、词性标注、依存句法分析、命名实体识别等一系列任务,结果全挂在同一个Doc对象上,不需要像NLTK那样一个函数一个函数地拼流程。
我第一次用Spacy的感受是:这玩意太“省事”了。nlp("苹果公司发布了新款iPhone")这句话跑完,分词、词性、实体、依存关系全部出来,直接取属性就行。它就是为工程场景设计的——大批量文本、多语言支持、模型可扩展,还能和深度学习框架做对接。
不过Spacy也有学习门槛。它的管道(pipeline)概念、Doc/Token/Span对象模型、模型安装方式,对新手来说一开始会觉得“绕”。很多人第一次装模型就卡住了,python -m spacy download那一步不知道是干嘛的,加载模型时报错也不知道怎么排查。这些坑我会在下面逐一拆开讲。
1.3 一张表看懂两者核心差异
| 对比维度 | NLTK | Spacy |
|---|---|---|
| 定位 | 教学与研究、语料探索 | 生产环境、工程落地 |
| 核心优势 | 语料资源丰富、方法透明 | 速度快、管道统一、模型化 |
| 分词方式 | 基于正则与外部资源 | 基于统计模型与上下文 |
| 词形还原 | 需指定词性(WordNetLemmatizer) | 自动根据词性还原(lemma_) |
| 命名实体识别 | 基于分类器,返回树结构 | 基于神经网络模型,返回实体列表 |
| 多语言支持 | 多语料,但工具链偏英文 | 多语言预训练模型 |
| 学习曲线 | 平缓,适合打基础 | 稍陡,需要理解对象模型 |
| 生产可用性 | 低,速度与精度不足 | 高,常被用于真实系统 |
这一张表我建议你收藏。入门阶段不需要懂每个词背后的细节,但心里要有个定位:NLTK是帮你理解NLP的“解剖室”,Spacy是让你把NLP用起来的“加工厂”。两者不冲突,甚至可以配合使用,后面我会用案例演示。
2. 环境准备:装好工具才能动手
2.1 创建虚拟环境并安装Python依赖
不管你是用conda还是原生Python,我都建议先建一个独立环境,别直接把东西装到系统里。尤其是NLTK和Spacy各自的依赖版本有差异,放进同一个环境容易互相干扰。
# 创建虚拟环境(Python 3.9+ 都可以) python -m venv nlp-env # 激活环境 # Windows nlp-env\Scripts\activate # macOS / Linux source nlp-env/bin/activate # 安装依赖 pip install nltk spacy如果你装NLTK和Spacy的时候觉得慢,多半是PyPI官方源在国内访问不理想。解决办法很简单,指定国内镜像源:
pip install nltk spacy -i https://pypi.tuna.tsinghua.edu.cn/simple这里有个细节:Spacy的版本迭代很快,目前3.x和2.x在API上差异挺大。老教程里的一些写法(比如nlp=spacy.load('en'))在新版中已经不推荐了。装完之后可以用pip show spacy看版本,我下面的代码基于3.7+版本,如果你用的是旧版本,建议更新一下。
2.2 NLTK语料数据下载的几种姿势
NLTK本体和语料数据是分开的,装完库之后还要下载数据,这一步也是新手的第一个拦路虎。打开Python交互环境,输入:
import nltk nltk.download()会弹出一个GUI窗口,让你勾选数据包。这种交互式下载看着方便,但实际体验很一般——下载速度慢、动不动就断,尤其是popular那个集合,体积大,全勾的话等半天。
我实际用下来,更推荐直接用命令行指定包名下载,只下自己需要的,省时省力:
import nltk # 只需下载一次 nltk.download('punkt') # 分词模型 nltk.download('stopwords') # 停用词 nltk.download('averaged_perceptron_tagger') # 词性标注 nltk.download('maxent_ne_chunker') # 命名实体识别 nltk.download('words') # 单词列表 nltk.download('wordnet') # WordNet词库关于“nltk下载慢”这个问题,还有一个非常实用的解法。手动从NLTK官方数据仓库下载对应zip包,然后解压到本地nltk_data目录。我一般是这样处理的:
- 在Python里运行
nltk.data.path,会看到一列搜索路径,第一个通常是/home/用户名/nltk_data(Mac为/Users/用户名/nltk_data,Windows为C:\Users\用户名\AppData\Roaming\nltk_data)。 - 手动下载
punkt.zip、stopwords.zip等压缩包。 - 按
nltk_data/tokenizers/punkt.zip、nltk_data/corpora/stopwords.zip这样的目录结构放好,不需要额外解压流程,NLTK自己会识别zip。
这个方法虽然听起来麻烦,但对网络条件不稳定的同学来说,是最靠谱的一条路。实测一次性把常用数据包放好,之后用到什么都不会再卡在下载这一步。
2.3 Spacy模型安装与验证
Spacy的模型同样需要单独安装,但方式和NLTK不一样。在命令行里执行:
python -m spacy download en_core_web_sm其中en_core_web_sm是英文小模型,体积几十MB,适合入门实验。如果你想处理中文,可以装zh_core_web_sm,底层分词依赖jieba,效果也够用。
如果你的网络环境同样不给力,还有一种非常稳的方法:先找到模型对应的下载地址,用浏览器或下载工具拿到.whl文件,再本地安装:
pip install en_core_web_sm-3.7.1-py3-none-any.whl安装完成之后,在Python里验证:
import spacy nlp = spacy.load("en_core_web_sm") doc = nlp("Apple is looking at buying U.K. startup for $1 billion") print(doc)能看到原文输出,说明模型加载成功。接着可以用nlp.pipe_names查看当前模型的管道组件:
print(nlp.pipe_names) # ['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']这六个组件分别负责:词向量映射、词性标注、依存句法解析、属性规则、词形还原、命名实体识别。看到这个列表,你就知道Spacy的“一站式处理”是怎么回事了。
3. 核心功能实操:从分词到NER
3.1 分词与句子切分的实际对比
分词是NLP最基础的一步。同一个句子,NLTK和Spacy的做法和结果差异很明显,我们直接看代码。
import nltk import spacy # NLTK 分词 text = "Mr. Smith bought cheapsite.com and it's not easy for him." tokens_nltk = nltk.word_tokenize(text) print(tokens_nltk) # ['Mr.', 'Smith', 'bought', 'cheapsite.com', 'and', 'it', "'s", 'not', 'easy', 'for', 'him', '.'] # Spacy 分词 nlp = spacy.load("en_core_web_sm") doc = nlp(text) tokens_spacy = [token.text for token in doc] print(tokens_spacy) # ['Mr.', 'Smith', 'bought', 'cheapsite.com', 'and', 'it', "'s", 'not', 'easy', 'for', 'him', '.']单看结果是相近的,但注意细节:it's在NLTK里被拆成了it和's,Spacy也是同样的拆分,这是英文缩写的通用处理方式。真正有差异的地方在于处理边界情况的能力——比如新造词、URL、混合文本。NLTK的分词是规则驱动,出现不了在词典里的词时容易切碎;Spacy的分词由统计模型预测,自带上下文理解,遇到没见过的词表现得稳定得多。
句子切分方面,NLTK用sent_tokenize,Spacy直接用doc.sents:
# NLTK 句子切分 sent_list = nltk.sent_tokenize(text) # Spacy 句子切分 sentences = list(doc.sents)两者对标准文本效果都不错。但如果你要处理“Mr. Smith”这种带缩写结尾的句子,NLTK偶尔会把Mr.后面的句点当成句子边界,需要额外调参;Spacy依靠模型判断,基本不用干预。这就是“手工规则”和“数据驱动”的区别,入门阶段建议都跑一遍感受一下。
3.2 词性标注与词形还原
词性标注(POS tagging)是给每个词打上名词、动词、形容词等标签。NLTK的pos_tag用的是Brown语料训练的最大熵分类器,标注速度还行,但你需要理解Penn Treebank标记集,比如NN是名词、VB是动词、JJ是形容词。
# NLTK 词性标注 tokens = nltk.word_tokenize("The cats were running quickly") tags_nltk = nltk.pos_tag(tokens) print(tags_nltk) # [('The', 'DT'), ('cats', 'NNS'), ('were', 'VBD'), ('running', 'VBG'), ('quickly', 'RB')]Spacy的词性标注是管道自动完成的,每个token对象上有两个属性:token.pos_是粗粒度标记(通用词性),token.tag_是细粒度标记(宾夕法尼亚树库风格):
# Spacy 词性标注 doc = nlp("The cats were running quickly") for token in doc: print(token.text, token.pos_, token.tag_) # The DET DT # cats NOUN NNS # were AUX VBD # running VERB VBG # quickly ADV RB词形还原是另一个高频需求。比如“running”还原成“run”,“cats”还原成“cat”。NLTK的WordNetLemmatizer有一个很容易踩的坑:如果它不知道词性,默认把词当成名词处理,于是“running”会被原封不动地留下来。你必须手动传词性:
from nltk.stem import WordNetLemmatizer lemmatizer = WordNetLemmatizer() # 不传词性,会把running当名词 print(lemmatizer.lemmatize("running")) # running # 传动词词性,才能还原 print(lemmatizer.lemmatize("running", pos="v")) # runSpacy完全没有这个问题,token.lemma_直接就是当前语境下的原形:
doc = nlp("The cats were running quickly") for token in doc: print(token.text, token.lemma_) # The the # cats cat # were be # running run # quickly quickly这里我特别想强调:如果你做文本挖掘、关键词提取、情感分析这类任务,词形还原是必须做的一步。很多人觉得“分词完就够用了”,结果统计词频时“run”和“running”被当成两个词,后面所有分析都会失真。NLTK能实现这个功能,但用起来繁琐;Spacy是开箱即用。这也是我渐渐在工程里更依赖Spacy的原因之一。
3.3 命名实体识别(NER)的两种写法
命名实体识别是NLP里最有实用价值的任务之一,负责把“人名、地名、机构名、时间、金额”等实体从文本中挑出来。比如“Apple is looking at buying U.K. startup for $1 billion”里面,Spark的实体是“Apple(机构)”“U.K.(地区)”“$1 billion(金额)”三个。
NLTK写NER需要先分词、再词性标注、再调用ne_chunk,返回的结果是一个树状结构,遍历起来很绕:
# NLTK 命名实体识别 from nltk import ne_chunk tokens = nltk.word_tokenize("Apple is looking at buying U.K. startup for $1 billion") tags = nltk.pos_tag(tokens) tree = ne_chunk(tags) print(tree) # (S # (ORGANIZATION Apple/NNP) # is/VBZ # looking/VBG # ... # (GPE U.K./NNP) # ...)要提取实体名称,得递归遍历这个树,对新手来说写起来很费劲。Spacy这边直接取doc.ents:
# Spacy 命名实体识别 doc = nlp("Apple is looking at buying U.K. startup for $1 billion") for ent in doc.ents: print(ent.text, ent.label_) # Apple ORG # U.K. GPE # $1 billion MONEY两行代码,输出一个元组列表,text是实体原文,label_是实体类型。这种体验上的差距,用过一次就回不去了。如果你在做新闻舆情分析、合同信息抽取、客服工单分类这类任务,NER几乎是标配,Spacy的写法能极大降低开发成本。
4. 用一次新闻文本处理把两个库串起来
4.1 案例目标与数据准备
理论知识说了一堆,总要落地跑一个完整案例。我这边拿一个很贴近日常的场景——新闻文本分析。假设我们拿到一段英文新闻稿,想快速了解“这条新闻在讲什么、提到了哪些关键实体”,这是典型的NLP入门综合练习。
原始文本我用一段简短的新闻:
"Tesla announced its newest electric vehicle factory in Texas yesterday. Elon Musk said the plant will produce 500,000 cars per year. The company also reported a 20% increase in quarterly revenue. Rivian, a competitor, is planning a similar facility in Georgia."先做基础清洗。新闻文本通常带着标点、换行、多余空格,我们统一处理一下:
import re raw_text = """Tesla announced its newest electric vehicle factory in Texas yesterday. Elon Musk said the plant will produce 500,000 cars per year. The company also reported a 20% increase in quarterly revenue. Rivian, a competitor, is planning a similar facility in Georgia.""" # 统一空格和换行 text = re.sub(r'\s+', ' ', raw_text).strip() print(text)这一步看起来不起眼,但很关键。新闻爬虫抓下来的文本里经常混着大量空白字符,不清理干净会影响后续所有处理的准确性。
4.2 NLTK部分:停用词过滤与高频词统计
新闻文本里有很多“the”“a”“will”“said”这类功能词,它们对主题分析没有贡献,应该过滤掉。NLTK自带的停用词表是干这个活的好手:
import nltk from nltk.corpus import stopwords from nltk.probability import FreqDist from nltk.stem import WordNetLemmatizer # 第一次使用后,停用词数据需要提前下载 nltk.download('stopwords') nltk.download('punkt') nltk.download('wordnet') stop_words = set(stopwords.words('english')) lemmatizer = WordNetLemmatizer() # 分词 + 词形还原 + 过滤停用词和非字母词 words = nltk.word_tokenize(text.lower()) clean_words = [] for w in words: if w.isalpha() and w not in stop_words: clean_words.append(lemmatizer.lemmatize(w, pos='v')) print(clean_words) # ['tesla', 'announce', 'newest', 'electric', 'vehicle', 'factory', # 'texas', 'yesterday', 'elon', 'musk', 'say', 'plant', 'produce', # 'car', 'per', 'year', 'company', 'also', 'report', 'increase', # 'quarterly', 'revenue', 'rivia', 'competitor', 'plan', 'similar', # 'facility', 'georgia']看到没,因为传了pos='v',所以“announced”还原成“announce”,“reported”还原成“report”,这样后续统计词频才能聚得拢。
接着用FreqDist统计高频词:
freq = FreqDist(clean_words) print(freq.most_common(10)) # [('tesla', 1), ('announce', 1), ...]NLTK在词频统计方面做得非常顺手,虽然这里文本太短,高频词区分度不高,但如果你面对的是几十篇新闻,这个方法就能快速看出语料库的主题中心词。这也是我为什么说NLTK在教学和探索阶段依然有价值——它的FreqDist、ConditionalFreqDist等工具做统计和特征工程非常直观。
4.3 Spacy部分:实体抽取与关键短语
同一段文本,再用Spacy做一次实体抽取,相当于自动给新闻“画重点”:
import spacy nlp = spacy.load("en_core_web_sm") doc = nlp(text) # 输出所有实体 for ent in doc.ents: print(ent.text, "->", ent.label_) # Tesla -> ORG # Texas -> GPE # yesterday -> DATE # Elon Musk -> PERSON # 500,000 cars -> CARDINAL # 20% -> PERCENT # Rivian -> ORG # Georgia -> GPE不到十行代码,新闻里出现的关键实体就全被拎出来了。这就是自动化摘要的雏形。你还可以进一步做聚合统计,比如看看整篇新闻里组织类的实体出现了几次,地点类实体有哪些,这些信息对分类、推荐、热点发现都有用。
另外,Spacy的noun_chunks可以抽取名词短语,帮助你看出文本在讲什么主体:
for chunk in doc.noun_chunks: print(chunk.text) # its newest electric vehicle factory # Texas # Elon Musk # the plant # 500,000 cars # the company # a 20% increase # quarterly revenue # a competitor # a similar facility # Georgia把实体抽取和名词短语结合起来,新闻的大意就很清楚了:特斯拉在德州建厂、马斯克宣布产量、营收增长、竞争对手Rivian在乔治亚也要建厂。我经常拿这个套路做新闻数据的快速探查,五分钟就能摸清一个语料库的主题结构。
5. 新手最容易踩的坑:问题排查速查
5.1 数据、模型加载问题
NLTK最常见的问题就是运行时提示找不到语料资源。比如LookupError: Resource punkt not found,基本原因就是没下载对应数据包。解决办法在前面已经说过,用nltk.download('punkt')或者手动放置数据包。
Spacy这边则是模型未安装或版本不匹配。常见报错是OSError: [E050] Can't find model 'en_core_web_sm'。解决办法是执行模型安装命令,或者确认安装的模型名与spacy.load()中的名称完全一致。模型版本与Spacy版本不匹配时还会出现ImportError,这时候要么升级Spacy,要么换一个对应版本的模型,不要硬撑。
另一个容易忽略的问题是内存。NLP模型在第一次加载时会消耗一定内存,特别是用大模型时。如果你在低配机器上跑,可以考虑用en_core_web_sm这类小模型,或者用nlp.disable_pipe()关闭不需要的管道组件,能省不少资源:
nlp = spacy.load("en_core_web_sm", disable=["parser", "ner"])5.2 编码与路径问题
处理中文文本时,最容易遇到UnicodeDecodeError。这通常是因为文件编码不是UTF-8。读取文件时建议显式指定编码:
with open("news.txt", "r", encoding="utf-8") as f: text = f.read()NLTK在Windows上偶尔会出现路径分隔符问题,导致找不到nltk_data目录。解决方案是给nltk.data.path手动添加路径:
import nltk nltk.data.path.append(r"C:\Users\你的用户名\nltk_data")这个技巧在局域网机器、公司电脑等场景里很实用,因为用户目录有时候会被系统重定向,默认路径并不存在。
5.3 性能与批量处理
很多新手处理大批量文本时,喜欢在循环里一次又一次调用nlp(text),一下子就把内存吃满了。像我前面说的,如果你的新闻数据有上万条,一定要用nlp.pipe()做批量处理,它内部会处理批量和流水线,性能提升非常明显:
texts = ["news1 ...", "news2 ...", "news3 ..."] docs = list(nlp.pipe(texts, batch_size=50)) for doc in docs: # 处理每一条文本 pass如果你的任务只需要分词,不需要句法解析,可以只启用底层组件:
nlp = spacy.load("en_core_web_sm", disable=["parser", "ner"])这样跑起来明显更快。NLTK也有类似的性能问题,比如word_tokenize在大文本上耗时较长,可以考虑用nltk.tokenize底层的TreebankWordTokenizer直接替代,能省掉一些句意判断的额外开销。
5.4 其他常见报错速查表
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
LookupError: Resource punkt not found | NLTK语料未下载 | nltk.download('punkt')或手动放置数据包 |
OSError: [E050] Can't find model | Spacy模型不存在 | 执行python -m spacy download en_core_web_sm |
'str' object has no attribute 'text' | 将字符串当作Doc对象使用 | 先执行doc = nlp(text),再遍历token |
ImportError: cannot import name 'lemmatize' | 函数名或模块路径写错 | 使用from nltk.stem import WordNetLemmatizer |
UnicodeDecodeError | 文件编码不是UTF-8 | open(file, encoding='utf-8') |
| 中文结果显示乱码 | 终端或文件编码问题 | 终端用UTF-8,输出到文件时明确指定编码 |
| Spacy模型下载超时 | 网络问题 | 手动下载whl包本地安装 |
最后说两个我反复踩过的小坑。第一,Spacy的模型下载和NLTK的语料下载都不是“装一次就完事”,换一台机器、换一个环境就得重来,所以建议把这些准备步骤写成一个setup.py脚本,新环境一条命令搞定。第二,NLTK和Spacy装在同一环境里没有冲突,但要注意别在同一个脚本里用相同的变量名,我曾经写过import nltk后又把nlp这个变量覆盖了,结果调了半天才发现问题。
我的个人建议:入门阶段先用NLTK做一两个小项目,把分词、词性、词频、词形还原这些概念彻底搞明白,然后再切换到Spacy做真实项目。这样你不仅会“用”工具,还知道工具背后的处理逻辑,出了问题也更容易定位。如果时间紧张,直接学Spacy也完全可行,但遇到奇怪的结果时,记得回头补一补NLTK里的基础概念,会帮你少走很多弯路。