简介:zh_core_web_lg-3.8.0.tar 是面向中文自然语言处理学习者与 Python 开发者的 spaCy 中文大模型离线包,适合用于分词、词性标注、命名实体识别与依存句法分析等任务的复现和项目原型搭建。压缩包共 45 个文件、约 575.1MB,主体由 5 个 model 模型权重、9 个 cfg 配置、5 个 json 元数据、5 个 txt 词表或说明,以及少量 py 脚本、npz 词向量与许可证文件组成,兼顾直接加载与二次配置。已有约 450 人学习下载,说明其在中文 NLP 入门与模型部署场景中有一定参考价值。使用者可据此了解 spaCy 大模型的目录组织、元数据与安装入口,快速完成环境准备、模型加载和基础管线验证,并为实体识别、文本分析等实验提供可复用的中文预训练资源;对希望避开在线下载、在本地离线复现中文管线的读者较友好,也可作为课程设计、毕业项目与小型应用的模型底包,适合具备基础 Python 语法、正在接触 NLP 管线的读者按需取用。
1. 一个 tar 包扔进离线机:zh_core_web_lg 到底解决了什么问题
上周某同事把一个几百 MB 的 tar 包丢进内网跳板机,说生产环境没有外网,让我把中文实体识别跑起来。包里就是zh_core_web_lg-3.8.0.tar,spaCy 官方中文大模型,3.8.0 这条版本线对应 spaCy 3.8.x。它不是算法库,也不是脚手架,而是一份训练好的权重加一张静态词向量表:装完spacy.load("zh_core_web_lg")一行,分词、词性、依存句法、人名地名机构名、词向量全部就位,python侧不用再拉任何在线资源。合适的人有两类:一是要在离线或内网环境做中文信息抽取、必须自带模型的;二是想拿现成中文向量做召回粗排、聚类、去重的。想从零训练自己模型的,这个包不合适,它是成品不是起点。
2. 拆包看结构:zh_core_web_lg-3.8.0.tar 里到底装了什么
拿到一个几百 MB 的 tar,最忌讳的就是直接pip install然后等人报错。我一般先解开看结构,五分钟能省掉后面半小时的排查。
2.1 三层结构:包元信息、配置、组件权重
模型包是spacy package打出来的源码分发包,目录形状大致如下:
tar -tf zh_core_web_lg-3.8.0.tar | sed -n '1,25p' # zh_core_web_lg-3.8.0/ # zh_core_web_lg-3.8.0/PKG-INFO # zh_core_web_lg-3.8.0/setup.py # zh_core_web_lg-3.8.0/MANIFEST.in # zh_core_web_lg-3.8.0/zh_core_web_lg/__init__.py # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/config.cfg # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/meta.json # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/vocab/ # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/tagger/ # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/parser/ # zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/ner/最外层是给 pip 看的:setup.py里写死了依赖区间,PKG-INFO是包描述。中间那层config.cfg加meta.json是模型的身份证,meta.json里的spacy_version字段就是它能接受的 spaCy 版本范围,pipeline字段列出这条管道装了哪些组件。最里面vocab/存词表和静态词向量,tagger/、parser/、ner/各存一份权重加组件配置。看懂这层,后面所有报错你都能定位到是「包的问题」还是「环境的问题」。
解压之后读一眼 meta,比装完再猜快得多:
mkdir -p /opt/models && tar -xf zh_core_web_lg-3.8.0.tar -C /opt/models python - <<'PY' import json, pathlib p = pathlib.Path("/opt/models/zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0/meta.json") meta = json.loads(p.read_text("utf-8")) print("版本:", meta["version"]) print("要求 spaCy:", meta["spacy_version"]) print("管道:", meta["pipeline"]) print("向量:", meta.get("vectors")) PY输出的spacy_version就是硬约束,比如>=3.8.0,<3.9.0,意思是你环境里的 spaCy 必须落在 3.8 这条线上,3.7 装不上,3.9 也不保证兼容。vectors字典里的keys是词向量表覆盖的词条数量,vectors是实际向量条数,两个数字不一样很正常——有些词共用同一行。这段检查不写任何文件、不改环境,跑完心里就有底了。
2.2 sm / md / lg 怎么选:别为了省磁盘选错档
同一语言下 spaCy 一般发三档,中文也是一样。选错档位的代价不是慢一点,而是某些能力直接没有。
| 档位 | 静态词向量 | 典型用途 | 代价 |
|---|---|---|---|
| sm | 通常没有,靠上下文张量表示 | 只要分词、词性、依存,做流水线前置 | 体积最小,similarity不可信 |
| md | 有,词表较小 | 需要粗粒度语义相似、聚类 | 中等,OOV 命中率一般 |
| lg | 有,词表最大 | 词级相似度、去重、召回粗排、离线交付 | 体积最大,加载最慢 |
判断标准很简单:你的下游要不要算「两个词/两句话像不像」。要算,就上 lg;只做结构化标注,sm 足够,disable=["parser", "ner"]之后速度能差出好几倍。我见过有人拿 sm 去做语义去重,结果doc.similarity()恒等于 0,查了一下午以为是编码问题——其实是模型根本没带向量表,玄学在这儿。拿到包之后用nlp.vocab.vectors.shape一看便知,第一个维度是 0 就是没向量。
2.3 版本号是绑死的,不是随便升
spaCy 从 v3 开始,模型版本和 spaCy 版本严格对齐:模型3.x.y只能配spacy>=3.x.0,<3.(x+1).0。所以zh_core_web_lg-3.8.0的正确搭档是 3.8.x,和 3.7 或 3.9 混搭都可能出问题。
| 模型版本 | 配套 spaCy | 说明 |
|---|---|---|
| 3.7.x | >=3.7.0,<3.8.0 | 老项目锁死 3.7 时用这一版 |
| 3.8.0 | >=3.8.0,<3.9.0 | 当前包对应的线 |
| 3.9.x | >=3.9.0,<3.10.0 | 上游升级后需要同步换模型 |
这张表不是背下来的,是每个包的meta.json里写着的。工程上的做法是:项目requirements.txt里把spacy==3.8.*和模型包一起锁死,别让某次pip install -U把其中一半升上去。血泪经验是——只升 spaCy 不升模型,或者反过来,都会在spacy.load那一行炸掉,而且报错信息指向的是配置文件里的某个字段,跟版本号八竿子打不着。
3. 装进环境:从 tar 到 spacy.load 的四步验证链
装模型这件事本身不复杂,复杂的是顺序和离线。下面这套流程我在三台不同机器上跑过,包括一台完全断网的内网机。
3.1 先定环境,再动模型
模型包声明了对 spaCy 的依赖,所以顺序必须是先 spaCy 后模型。反过来装,pip 会尝试去源上找 spaCy,断网环境直接卡死。
python -m venv .venv-zh source .venv-zh/bin/activate # Windows: .venv-zh\Scripts\activate python -m pip install -U pip setuptools wheel python -c "import spacy; print(spacy.__version__)" # 必须落在 3.8.x三行命令里最容易被跳过的是setuptools和wheel。tar 是源码包,安装时需要本地构建,缺这两个会在后面报Could not build wheels之类的错,看着像模型坏了,其实是构建工具没装。装 spaCy 本身如果也没网,就在有网的机器上pip download spacy==3.8.* -d wheelhouse把整包依赖拖下来,拷过去pip install --no-index --find-links=wheelhouse spacy。这套离线搬运法对 thinc、numpy、blis 这些底层依赖同样适用。
3.2 让 pip 直接吃下这个 tar
正常有网或者配了内网源的情况,一条命令就够:
pip install ./zh_core_web_lg-3.8.0.tar但它会先建一个隔离的构建环境去装 setuptools,断网时这一步必然失败,日志最后一般是Network is unreachable或者Could not find a version that satisfies the requirement setuptools。内网机的正确姿势是关掉构建隔离、同时断掉回源:
pip install --no-build-isolation --no-index ./zh_core_web_lg-3.8.0.tar # 或者先解开再装目录,行为和装 tar 完全一致 tar -xf zh_core_web_lg-3.8.0.tar -C /opt/pkg pip install --no-build-isolation --no-index /opt/pkg/zh_core_web_lg-3.8.0--no-build-isolation让它用当前虚拟环境里已有的 setuptools 来构建,不再新建;--no-index是后悔药,防止 pip 在断网边缘反复重试拖时间。还有一种更干脆的玩法:压根不装,直接spacy.load("/opt/models/zh_core_web_lg-3.8.0/zh_core_web_lg/zh_core_web_lg-3.8.0")指向解压后的目录。好处是不污染 site-packages,多个项目可以共用同一份权重;坏处是路径写死在代码里,得用配置项兜住。
3.3 四条验证,一条不过就是没装好
装完别急着写业务代码,先跑这段。四条输出对得上,才算真的通了:
import spacy nlp = spacy.load("zh_core_web_lg") print("版本:", nlp.meta["version"]) # 期望 3.8.0 print("管道:", nlp.pipe_names) # 期望看到 tagger/parser/ner 等 print("向量形状:", nlp.vocab.vectors.shape) # 期望 (N, 300),N 明显大于 0 doc = nlp("张三昨天在北京签了合同") print("分词:", [t.text for t in doc]) print("词性:", [(t.text, t.pos_) for t in doc]) print("依存:", [(t.text, t.dep_, t.head.text) for t in doc]) print("实体:", [(e.text, e.label_) for e in doc.ents])pipe_names是第一个关键信号。不同小版本的组件集合可能略有差异,但至少应该看到tagger、parser、ner、attribute_ruler这几类;如果只有tok2vec,说明加载到了别的东西。vectors.shape第二个维度一般是 300,第一个维度是个几十万量级的数字——太小说明加载的是 md 或 sm。最后那行实体输出,正常应该能识别出人名、地名、机构名,识别不出来但分词正常,那就不是安装问题,是文本域和训练域差太远。
3.4 中文分词器:默认可能退化成按字切
这一点最容易翻车,而且不报错。spaCy 的中文分词器会优先调用 jieba 做词级切分,环境里没装 jieba 就退化成按字切分——自然语言处理会变成五个单字 token,后面词性、依存、NER 全部受影响,但程序照常跑,日志干干净净。
import spacy nlp = spacy.load("zh_core_web_lg") toks = [t.text for t in nlp("自然语言处理很有趣")] print(toks) # 正常: ['自然语言', '处理', '很', '有趣'] # 退化: ['自', '然', '语', '言', '处', '理', '很', '有', '趣']发现是退化形态,补一句pip install jieba,然后务必重启 Python 进程——分词器在spacy.load那一刻就初始化完了,热加载不生效。仓库里如果有多环境,记得确认 jieba 装进了同一个解释器,pip -V和python -c "import sys; print(sys.executable)"两条命令的输出要对得上。模型目录里的config.cfg有一个[nlp.tokenizer]段,可以显式写死分词策略,团队协作时我一般会把它锁上,免得有人在生产环境漏装依赖导致分词静默降级。
4. 跑通五类标注:分词、词性、依存、NER 与词向量的落地写法
模型装好只是开始,真正决定项目成败的是怎么把输出用起来。这一章给的是可以直接抄进项目骨架的写法。
4.1 一次遍历拿到五类标注
Doc对象是个容器,遍历一次就能把 token 级和实体级信息全部取出,不要为了拿词性再跑一遍模型。下面这段是我项目里annotate()函数的精简版:
import spacy nlp = spacy.load("zh_core_web_lg") def annotate(text: str) -> dict: doc = nlp(text) tokens = [] for t in doc: tokens.append({ "text": t.text, "pos": t.pos_, # 粗粒度词性,英文标签名 "tag": t.tag_, # 细粒度词性,依赖具体语言包 "dep": t.dep_, # 依存关系 "head": t.head.i, # 依存父节点在 doc 中的下标 "is_stop": t.is_stop, }) ents = [{ "text": e.text, "label": e.label_, # PERSON / GPE / ORG 等 "start": e.start_char, "end": e.end_char, } for e in doc.ents] return {"tokens": tokens, "ents": ents, "vector_dim": doc.vector.shape[0]} print(annotate("李四在上海的一家科技公司做算法工程师"))pos_是通用词性集(名词、动词、形容词这些),跨语言一致,适合做粗粒度过滤;tag_是这套中文模型自己的细分标签,换模型就变,别把它写进硬编码的业务规则里。head.i存下标而不是对象,是为了后面能直接序列化,存对象在落盘时会炸。start_char和end_char是字符级偏移,前端做高亮全靠它,千万别用 token 下标代替——中文没有空格,两个下标体系完全不是一回事。
4.2 批量处理:n_process 和 batch_size 怎么调
线上是几万条文本,一条条nlp(text)会慢到怀疑人生。正确入口是nlp.pipe:
import spacy nlp = spacy.load("zh_core_web_lg") def run(texts, batch_size=32, n_process=2): # batch_size 控制一次进模型的文档数,内存紧就往下调 # n_process > 1 走多进程,每个进程一份模型,注意内存乘数 # disable 掉当前任务用不到的组件,速度提升立竿见影 for doc in nlp.pipe(texts, batch_size=batch_size, n_process=n_process, disable=["parser"]): yield [(t.text, t.pos_) for t in doc] if __name__ == "__main__": # Windows 及 spawn 启动方式下必须有这层保护 texts = [f"第{i}条待处理文本" for i in range(1000)] for row in run(texts): passbatch_size是内存和吞吐的平衡杆,默认值偏保守,文本长的时候往上调到 64、128 通常还能吃到收益,但要注意单条超长文本会让这一批整体变慢。n_process开多进程并不是线性加速,模型权重要在每个子进程里各持一份,内存直接乘以进程数,四个进程跑 lg 模型在小内存机器上很容易被 OOM Killer 干掉。disable是最划算的优化:只做实体识别就关掉parser,只分词就再关tagger,中文长文本上经常能省一半时间。if __name__ == "__main__"那层保护在 Windows 和 macOS 的 spawn 模式下是硬要求,缺了会看到进程反复重启直到崩溃,这个坑我踩过不止一次。
4.3 词向量相似度:先搞清楚它算的是什么
doc.similarity()算的是文档内所有 token 向量的平均值之间的余弦相似度。这个定义决定了它对短文本、对虚词、对分词结果都异常敏感:
import spacy nlp = spacy.load("zh_core_web_lg") a, b = nlp("笔记本电脑"), nlp("便携式计算机") print("doc 相似度:", round(a.similarity(b), 4)) tok = nlp("量子退火")[0] print("是否在词表:", tok.has_vector, "向量模长:", round(tok.vector_norm, 4)) # 查近邻比算相似度更实用 for key, score in nlp.vocab.vectors.most_similar(tok.vector.reshape(1, -1), n=5)[0][0]: print(nlp.vocab.strings[key], round(float(score), 4))has_vector为 False、vector_norm为 0,说明这个词不在静态词表里,OOV 词的向量是全零,参与平均会把整句向量往原点拽,相似度自然失真。中文的复合词、新词、专有名词大量落在 OOV 区间,所以拿它做语义相似要谨慎,做「同一个词的两种写法」这种近义判断反而还行。真要上语义检索,向量模型比这个静态表靠谱得多,这份词向量的合适定位是低成本粗排、聚类预分组、拼写变体归并。most_similar那条路径值得多用,它直接在向量矩阵里做近邻搜索,比两两算similarity更贴近实际需求,而且不会因为句子里混了虚词而失真。
4.4 把结果落成 jsonl,方便下游接
中间产物我倾向用 jsonl,一行一条,边跑边写,中断了也不用从头来:
import json import spacy nlp = spacy.load("zh_core_web_lg") texts = ["第一段要处理的文本", "第二段要处理的文本"] with open("out.jsonl", "w", encoding="utf-8") as f: for doc in nlp.pipe(texts, batch_size=32, disable=["parser"]): rec = { "text": doc.text, "tokens": [t.text for t in doc], "ents": [{"t": e.text, "l": e.label_, "s": e.start_char, "e": e.end_char} for e in doc.ents], } f.write(json.dumps(rec, ensure_ascii=False) + "\n")ensure_ascii=False必须加,否则中文全变成\uXXXX,文件小了一半但人没法看,排查问题时痛苦翻倍。字段名我习惯用短名,几十万行的时候体积差别很直观。disable按需开,只做分词和实体就不需要依存句法,速度差别在长文档上非常明显。
5. 避坑与排查:五个让我重装过环境的真实故障
下面这五条,每一条我都至少在现场遇到过一次,按「现象 → 原因 → 解决」记下来。
5.1 报 E050:Can't find model 'zh_core_web_lg'
现象:spacy.load("zh_core_web_lg")抛OSError: [E050] Can't find model 'zh_core_web_lg',但pip list里明明有。
原因:装到了另一个解释器。最常见的组合是 pip 指向系统 Python、脚本跑在虚拟环境里,或者反过来——sudo pip install装进 root 的 site-packages,普通用户跑脚本自然看不见。
解决:先对齐三件事,which python、python -m pip -V、python -c "import zh_core_web_lg; print(zh_core_web_lg.__file__)"。第三条报 ImportError 就说明确实没装进这个解释器,用python -m pip install重装一遍,永远不要裸调pip。如果模型是解压目录形式,直接把spacy.load()的参数换成绝对路径,绕开包发现机制。
5.2 pip 卡在 Building wheel,最后超时
现象:pip install ./zh_core_web_lg-3.8.0.tar输出停在Building wheel for zh-core-web-lg,几分钟后报网络错误或者Could not build wheels。
原因:tar 是源码包,pip 默认新建隔离构建环境去装 setuptools、wheel 这些构建依赖,内网或断网环境下必然失败。日志里通常有一行不容易注意到的Installing build dependencies。
解决:加--no-build-isolation,让它复用当前环境已有的构建工具;再加--no-index彻底断开回源。前提是当前环境里 setuptools 和 wheel 已经装好,python -m pip install -U setuptools wheel一行确认。离线机器上也可以先在联网机器pip wheel出 whl 再拷,比反复折腾 tar 省事。
5.3 分词变成单个汉字,但程序不报错
现象:整条管道跑得很顺,日志没异常,但下游规则全失效,实体识别结果稀烂。
原因:jieba 没装,中文分词器静默退化成按字切分。这个降级不抛异常、不打警告,是典型的黑匣子行为。
解决:pip install jieba后重启进程。上线前在 CI 里加一条断言,跑一句固定文本,比对分词结果长度,长度异常就直接让流水线红掉。这条断言我现在的每个项目都会加,成本五秒钟,能挡掉一整类静默故障。
5.4 similarity 恒为 0,或者出现 W007 警告
现象:两句话明明意思接近,doc.similarity()返回 0.0,日志里有UserWarning: [W007] The model you're using has no word vectors loaded。
原因:加载的模型没有静态词向量,或者词表里的向量全是零。多半是拿 sm 档当 lg 用,或者装错了包。
解决:nlp.vocab.vectors.shape一看,第一个维度是 0 就没救,换 lg 模型重装。如果维度正常但某些词仍然算不准,那是 OOV 问题,改用nlp.vocab.vectors.most_similar做近邻查询,或者对域内术语用nlp.vocab.set_vector手工补向量。
5.5 服务内存随请求数缓慢上涨
现象:Flask/FastAPI 服务跑一晚上内存翻倍,重启就好,第二天照旧。
原因:在请求处理函数里调用了spacy.load。每次加载都要重新构建 Vocab 和读入权重,几百 MB 的模型一天下来内存碎片堆满。
解决:进程启动时加载一次,做成模块级单例或依赖注入的全局对象;用 gunicorn 的话配合--preload让父进程加载、子进程共享。同时注意n_process别在已经多 worker 的服务里再开,两层多进程叠起来内存是乘法关系。改完之后用tracemalloc或者简单的psutil打点确认曲线平了再上线。
6. 进阶用法:用 EntityRuler 和 to_disk 把它改成能交付的东西
原始模型只认通用实体,业务里的产品名、内部系统名、行业术语它一概不认识。硬编码后处理是下策,正确做法是挂一个 EntityRuler 在 NER 前面:
import spacy nlp = spacy.load("zh_core_web_lg") ruler = nlp.add_pipe("entity_ruler", before="ner") # 必须在 ner 之前 ruler.add_patterns([ {"label": "PRODUCT", "pattern": "云原生方案"}, {"label": "ORG", "pattern": "某某科技有限公司"}, {"label": "PRODUCT", "pattern": [{"LOWER": "智能"}, {"LOWER": "网关"}]}, ]) print([(e.text, e.label_) for e in nlp("某某科技有限公司发布了云原生方案").ents])before="ner"是关键:先匹配的实体不会被后面的统计模型覆盖,规则优先级高于模型。pattern既可以是字符串(内部会走分词),也可以是 token 属性列表,后者对变体更稳。
改完管道之后,用to_disk落成一份可交付的目录,对方环境只需要 spaCy 本体,不需要重装模型:
nlp.to_disk("/opt/models/zh_core_web_lg_custom") # 交付侧 nlp2 = spacy.load("/opt/models/zh_core_web_lg_custom")这里有个后悔药必须提前说:disable只是运行时跳过组件,to_disk依然会把全部权重写进去,体积一点没省;真要瘦身得用remove_pipe,但删掉的组件拿不回来。所以我每次交付前都留一份原始模型目录,改过的另存一份,两条路径分开写。
从那以后我每次拿到一个模型 tar 包,都先解压看一眼meta.json里的spacy_version再动手装,第二步跑一遍分词断言确认没退化,第三步才写业务代码。三步走完,后面基本不会返工。希望帮到你。
本文还有配套的精品资源,点击获取