我一直觉得,做中文 NLP 很容易走两条极端,要么写一堆零散的正则和脚本,勉强把需求拼起来, 要么脑子一热,直接把问题全丢给大模型。但真到落地一个能长期跑在生产上的小系统, 比如从风电 / 光伏新闻里稳定抽出装机容量、上网电量这些指标——你会发现,光靠“大力出奇迹”其实挺难维护的。

spaCy 对我来说,就是在这两端之间找到的那条“中间道路”,它足够工程化,能舒服地接进现有代码和服务。 同时又保留了规则、传统机器学习、甚至挂上 LLM 的空间。

这篇文章想做的事很简单:从零开始,把我自己摸索 spaCy 中文工作流的过程梳理一遍, 从安装、分词、NER,到规则匹配、模型训练,再到和 RAG / 问答系统打配合。 如果你也在为“如何把中文文本处理做得既靠谱又不折腾人”发愁,希望这篇小指南能帮你少踩几些坑。

spaCy 是什么?能帮我做什么?

spaCy 是一个开源的 Python 自然语言处理(NLP)库,定位很明确: 给工程师用的工业级 NLP 框架

它的特点大致可以概括为几句:

  • 内置成熟的 分词、词性标注、依存句法分析、命名实体识别 等能力;
  • 跑得不慢,工程化程度比较高,方便接入现有系统;
  • 支持 75+ 语言,包括中文;
  • 可以和 传统机器学习深度学习,甚至 大语言模型(LLM) 配合使用。

如果你需要: 从文本中抽取结构化指标(比如,我需要从新能源新闻里抽项目规模、装机容量、上网电量等指标), 做文本相似度计算、文本分类、打标签, 为 RAG 或问答系统做更干净的结构化预处理。 那 spaCy 是一个很合适的基础工具。

准备工作

安装 spaCy

pip install spacy

❗ 建议使用虚拟环境(如 venvconda)来管理依赖。

安装中文模型

spaCy 官方提供了一系列中文模型,例如:

  • zh_core_web_sm
  • zh_core_web_md
  • zh_core_web_lg
  • zh_core_web_trf(基于 Transformer 的大模型)

一般来说:

  • 快速尝鲜zh_core_web_sm
  • 精度更高 / 生产环境:优先考虑 md / lg / trf 系列

安装示例(以 zh_core_web_sm 为例):

python -m spacy download zh_core_web_sm

使用时加载模型:

import spacy

nlp = spacy.load("zh_core_web_sm")
doc = nlp("12月29日上午,华能贵港七星岭风电项目开工仪式在覃塘区东龙镇举行。")

spaCy 的基础概念

💡 接下来所有例子,统一用下面这段风电新闻作为示例文本,方便你对比感受不同功能的差异。

12月29日上午,华能贵港七星岭风电项目开工仪式在覃塘区东龙镇举行,标志着贵港市第一个风电项目正式开工建设。
▲现场奠基图▲项目开工仪式现场图据悉,华能贵港七星岭风电项目总投资5.9亿元,本期工程核准容量 60MW,
采用 27台 单机容量 2兆瓦 及以上的风机发电机组,预计年上网电量为 15668万千瓦时,年平均利用小时为 2611小时,
建成投产后每年将上缴地方税收 1200万元。▲效果图▲效果图华能贵港七星岭风电项目的开工建设,
将对我市壮大绿色环保经济起到积极的作用。

Tokenization

Tokenization(分词 / 切词) 是 spaCy 的入口: 把一整段文本拆成「词、标点符号、数字」等基本单元,后续的词性标注、实体识别都基于它。

doc = nlp(article["body"])

for token in doc:
    print(token.text)

示例输出(部分):

12月
29日
上午
,
华能
贵港
七星
岭风电
项目
开工
仪式
在
覃塘区
东龙镇
举行
,
...

这里能看到:

  • spaCy 已经把 日期、地名、项目名称 这些元素拆成了一个个 token;
  • 后面我们可以基于这些 token 去做规则匹配和实体识别。

词性标注(POS Tagging)

POS Tagging(词性标注) 会给每个 token 打上词性和句法信息,比如:

这个词是名词、动词、数词、专有名词…… 这个词和其他词之间的语法依赖关系是什么(主语、宾语等)。

简化示例(非真实输出,仅示意):

12月        NOUN    compound:nn
29日        NOUN    compound:nn
上午        NOUN    nmod:tmod
华能        PROPN   compound:nn
贵港        NOUN    compound:nn
七星        PROPN   compound:nn
岭风电      PROPN   compound:nn
项目        NOUN    compound:nn
...
举行        VERB    ROOT

这层信息在后面两类场景特别有用:

  1. 规则匹配时,做更“聪明”的筛选(比如只匹配动词后面的数值)
  2. 做复杂语法规则的抽取(用 Dependency Matcher)

命名实体识别(NER)

Named Entity Recognition(命名实体识别) 的目标是: 从文本中自动识别出 人名、地名、组织、时间、金额等像名词又比普通名词重要的东西

在风电项目的场景里,我们更关心的可能是:

  • 项目名称、项目类型;
  • 总投资、装机容量;
  • 年上网电量、平均利用小时数;
  • 风机数量、单机容量等。

这种行业特化的实体往往需要你自己通过 规则 + 训练 来定制,后面会专门讲。

向量和相似度

spaCy 也支持 词向量 / 文档向量相似度计算。 你可以用它来:

  • 粗略比较两段文本“是不是一个意思”;
  • 在索引前做预筛选,或者给搜索结果排序。

示例代码(使用英文 en_core_web_md 模型):

import spacy

nlp = spacy.load("en_core_web_md")
doc1 = nlp("I like salty fries and hamburgers.")
doc2 = nlp("Fast food tastes very good.")

print(doc1, "<->", doc2, doc1.similarity(doc2))

中文场景下,你可以换成支持向量的中文模型,或自己训练 / 接入外部向量。

句子边界识别

很多时候,我们更希望“按句子”为单位来分析,比如:

  • 找到包含特定实体的句子;
  • 把长文本切成一段段“比较完整”的句子,方便后续分类或 QA。
for sent in nlp(article["body"]).sents:
    print(sent.text)

配合 NER 使用,你可以做到: 先找到所有装机容量实体,再把它们所在的句子取出来,结合上下文进行二次判断。

基于规则的匹配

spaCy 的一个工程向非常强的功能就是 Rule-based Matching(基于规则的匹配)。 当你大致知道文本长什么样时,规则往往比纯模型更简单更稳定。

Token-based Matcher

from spacy.matcher import Matcher

matcher = Matcher(nlp.vocab)

matcher.add("项目规模/装机容量", [
    [{"TEXT": {"REGEX": r"(\d+\.?\d*\s?(MW|兆瓦))"}}],
])
matcher.add("上网电量/发电量", [
    [{"TEXT": {"REGEX": r"(\d+\.?\d*\s?(GWh|千瓦时|万kW·h))"}}],
])

doc = nlp(article["body"])
matches = matcher(doc)

for match_id, start, end in matches:
    string_id = nlp.vocab.strings[match_id]
    span = doc[start:end]
    print(match_id, string_id, start, end, span.text)

这里我们用简单的正则匹配了:

  • 60MW / 2兆瓦 之类的 装机容量
  • 15668万千瓦时上网电量 / 发电量

对于“指标提取”来说,这已经是一个不错的起点。

EntityRuler

如果你希望这些匹配结果直接变成 doc.ents 里的实体,可以用 EntityRuler

from spacy.lang.zh import Chinese

nlp = Chinese()
cfg = {"segmenter": "pkuseg"}
nlp = Chinese.from_config({"nlp": {"tokenizer": cfg}})
nlp.tokenizer.initialize(pkuseg_model="news")

text = "...上面那段风电项目新闻..."
doc = nlp(text)

ruler = nlp.add_pipe("entity_ruler")

patterns = [
    {"label": "单机容量", "pattern": [{"TEXT": "单机"}, {"TEXT": "容量"}, {"TEXT": {"REGEX": r"(\d+\.?\d*\s?(MW|兆瓦))"}}]},
    {"label": "项目规模/装机容量", "pattern": [{"TEXT": {"REGEX": r"(\d+\.?\d*\s?(MW|兆瓦))"}}]},
    {"label": "利用小时数", "pattern": [{"TEXT": "利用小时"}, {"ORTH": "为"}, {"TEXT": {"REGEX": r"(\d+\.?\d*\s?(小时))"}}]},
    {"label": "风机数量", "pattern": [{"TEXT": {"REGEX": r"(\d+\s?(?:台|套))"}}]},
]

ruler.add_patterns(patterns)
doc = nlp(text)

# 例如用 displacy 在 Notebook 中高亮实体
from spacy import displacy
displacy.render(doc, style="ent", jupyter=True)

这一步可以看成是让规则匹配结果,进入 NER 的生态圈

SpanRuler

有时候你不想把所有匹配都变成 doc.ents,而是希望单独存放,比如 doc.spans["ruler"]SpanRuler 就是为这种场景设计的。

ruler = nlp.add_pipe("span_ruler")
patterns = [
    {"label": "项目规模/装机容量", "pattern": [{"TEXT": {"REGEX": r"(\d+\.?\d*\s?(MW|兆瓦))"}}]},
    {"label": "上网电量/发电量", "pattern": [{"TEXT": {"REGEX": r"(\d+\.?\d*\s?(GWh|千瓦时|万kW·h))"}}]},
]
ruler.add_patterns(patterns)

doc = nlp(article["body"])
print([(span.text, span.label_) for span in doc.spans["ruler"]])

简单理解:

  • Entity 更偏语义上的命名实体
  • Span 更像你想额外标记出来的一段文本

其它规则工具一览

你完全可以根据需求自由组合:

  • PhraseMatcher:对一堆固定短语做高速匹配,适合关键词列表;
  • DependencyMatcher:基于依存句法树,描述更复杂的语法模式;
  • Combining models and rules:模型负责“泛化能力”,规则负责“刚性约束”,两者相得益彰。

传统机器学习

当规则已经有点吃力,希望模型具备更多“举一反三”的能力时,就需要走上 模型训练 这条路。

典型流程大概如下:

准备训练数据

TRAIN_DATA = [
    ("The F15 aircraft uses a lot of fuel", {"entities": [(4, 7, "aircraft")]}),
    ("did you see the F16 landing?", {"entities": [(16, 19, "aircraft")]}),
    # ...
    ("is the F35 a waste of money", {"entities": [(7, 10, "aircraft")]}),
]

中文场景下,把 "aircraft" 替换成你关心的标签,比如 "项目规模/装机容量", "风机数量" 等。

转换为 .spacy 格式

通过官方工具把原始标注数据转换成 spaCy 自己的二进制格式。

创建并填充配置文件

先初始化一个基础配置:

python -m spacy init fill-config base_config.cfg config.cfg

你可以在 config.cfg 里指定:

  • 使用哪个语言;
  • pipeline 里有哪些组件(tok2vec, ner 等);
  • 训练超参数等。

启动训练

python -m spacy train config.cfg \
    --output ./output \
    --paths.train ./train.spacy \
    --paths.dev ./dev.spacy

训练完成后,会在 ./output 目录下生成一个可加载的模型,你可以像加载官方模型那样用它:

nlp = spacy.load("./output/model-best")
doc = nlp(article["body"])

结合大语言模型:spacy-llm 的玩法

如果你已经在用 ChatGPT / Llama 等 LLM,spaCy 提供了 spacy-llm 组件, 可以把 LLM 当成 spaCy pipeline 中的一个“黑盒组件”,做 NER、分类等任务。

基本配置示例

[nlp]
lang = "en"
pipeline = ["llm"]

[components]

[components.llm]
factory = "llm"

[components.llm.task]
@llm_tasks = "spacy.NER.v3"
labels = ["单机容量", "风机数量", "装机容量", "年上网电量"]

[components.llm.model]
@llm_models = "spacy.GPT-3-5.v1"
config = {"temperature": 0.3}

使用方式示例

from spacy_llm.util import assemble
from dotenv import load_dotenv

load_dotenv()  # 加载 API Key 等配置
nlp = assemble("config.cfg")

doc = nlp(
    "12月29日上午,华能贵港七星岭风电项目开工仪式在覃塘区东龙镇举行..."
)

for ent in doc.ents:
    print(ent.text, ent.label_)

输出示例(理想情况):

27台       风机数量
单机容量 2兆瓦  单机容量
60MW      装机容量
15668万千瓦时 年上网电量

这类方案的好处是: 用 llm 来干理解复杂语义的重活, 用 spaCy 做数据流转、结构化、与已有 pipeline 的集成。

Workflow 与工程实践:跑通一个从“文本到指标”的流程

官方也提供了一些项目模板和工具(如 explosion/projectsweasel),方便你定义端到端的 NLP 流程。

在“风电 / 光伏指标提取”的典型场景中,可以设计这样一条链路:

  1. 原始文本清洗:去掉 HTML、特殊符号等;
  2. spaCy 预处理:分词、句子切分、基本 NER;
  3. 规则匹配 + EntityRuler / SpanRuler:抽取装机容量、上网电量、风机数量、利用小时数等;
  4. LLM / 自训练 NER 校正:对规则结果进行语境修正(例如“这里的 60MW 是项目规模还是风机单机容量?”);
  5. 指标结构化输出:生成统一的 JSON / 表格,进入后续数据存储或 RAG 过程。

spaCy 的几个典型用法

指标提取与数据清洗

  • NER:识别项目名、公司名、金额、装机容量等;
  • Similarity:项目名称与项目位置的模糊匹配;
  • Text Classification:为文档打“项目阶段”“能源类型”等标签;
  • Document ranking / classification

    • 给新闻质量打分,过滤掉噪声;
    • 按“是否为正式公告”之类的维度分类。

与规则 & QA 结合

  • 先通过规则 / NER 找到候选实体;
  • 再看实体所在句子,用关键字 + 语境做进一步判断;
  • 或者把相关句子送入 QA 模型,做类似 RAG 的问答抽取:
QA_input = {
    "question": "Who controls Deir Hafer and Al-Bab?",
    "context": sent.text,
}
res = hugg(QA_input)

(示例是英文战地新闻,同样的套路可以用在光伏 / 风电项目文本里。)

服务于 RAG 与 Prompt 处理

在 RAG 系统中,spaCy 可以用来:

  • 对长文本做更智能的切分(按句子 / 段落);
  • 抽取关键词,用于构建索引或 Prompt;
  • 配合 LlamaIndex 等框架优化 Node Parser / Text Splitter 的效果。

结语

如果把整条 NLP 工程链路比作一条“生产线”, spaCy 更像是那套 可靠、标准化、可组合的流水线设备

一端接入原始新闻、公告、报告, 中间完成分词、标注、实体识别、规则抽取、模型推理, 另一端吐出干净、结构化的指标、标签, 供风电 / 光伏评估系统、RAG 系统或 Dashboard 使用。

在入门阶段,可以先把这几块弄熟:

  1. 分词、POS、NER、句子分割
  2. Matcher / EntityRuler / SpanRuler 的规则抽取
  3. 简单的 模型训练spacy-llm 集成

等到这些基础打稳,再去扩展 Workflow、和现有业务系统深度整合,就会顺畅很多。