简介Jiayan甲言是一套专注古代汉语处理的NLP工具包面向古汉语研究者、文史爱好者及NLP开发者用于解决文言文分词、词性标注、断句标点及文言词库自动合成等痛点。相较通用现代汉语NLP工具它针对古籍语料做了专门优化适合开展古籍数字化、文言文本挖掘与教学研究。资源压缩包共28个文件以20个Python源码文件为主体涵盖核心模块、分词器、词性标注器、断句器及示例脚本另含README、LICENSE、依赖清单等说明文档整体仅217KB轻量易部署、导入即可使用。目前已有3318人学习下载社区关注度较高。通过源码可深入理解基于无监督双向语言模型与最大概率路径的动态规划分词算法并借助lexicon目录快速构建自定义文言词典postagger与sentencizer模块分别提供词性标注和断句标点能力data目录内置基础语料配合examples.py可直接运行体验。整体代码注释规范、模块划分清晰便于二次开发与实验复现是一份兼具学习与实用价值的古汉语NLP参考资料。1. 甲言给文言文做 NLP先解决词库、分词、断句三件事做过古典中文 NLP 的人都有体会把白话文那一套分词器直接搬到文言文上结果基本是翻车现场。某高校古籍数字化小组处理一批明清日记时用通用工具跑“子曰学而时习之”输出变成“子/曰/学/而/时/习/之”七个字全切成了单字词更麻烦的是断句一整段没有句读的古文被切成二十多个碎片根本没法做后续标注。后来他们换了甲言Jiayan这套专注古代汉语的 Python 工具包才把词库合成、分词、词性标注、断句和标点整条流程跑通。它解决的正是通用 NLP 工具在古汉语语料上「词典不匹配、切分粒度乱、断句看心情」这三个核心痛点。这篇文章就从这三个能力展开讲清楚它怎么用、参数怎么调、坑在哪里适合正在做古籍语料清洗、文言信息抽取和句读恢复的从业者。2. 安装与文言词库合成把自定义词典变成可用语言模型2.1 安装与基础验证甲言是标准的 Python 包依赖 wheel 和 C 扩展编译环境。我一般建议直接创建独立虚拟环境后再装避免和已有的 NLP 项目出现依赖冲突。python -m venv venv_jiayan source venv_jiayan/bin/activate # Windows 下执行 venv_jiayan\Scripts\activate pip install jiayan安装完成后先做一个最小加载验证确认核心模块能被正常导入import jiayan print(jiayan.__version__)这段验证的目的不是看版本号而是确认 C 扩展编译成功。很多人在第二步就翻车import jiayan报 ImportError九成是编译器环境缺了 MSVC Build Tools 或 GCC。逻辑上你可以把甲言理解为两部分一层是 Python 调用接口另一层是底层用 C 实现的统计模型与解码器import失败通常意味着底层扩展没编译进当前环境。参数层面这里没有额外参数但如果安装时看到pyproject.toml相关报错优先升级pip和setuptools再重装。2.2 词库合成为什么甲言需要离线词典通用分词器的词典以现代汉语为主文言文里的实词、虚词、官职名、地名在词典里要么没有要么词频极低所以“学而时习之”会被拆成碎片。词库合成就是把你自己整理的领域词典合并进模型的词表中让分词、断句优先命中你提供的词条。我一般这样准备词典文件一行一个词词和词频用空格或 Tab 分隔。词频可以填统计次数也可以直接填所在语料的出现频率拿不准时统一填 1让模型靠上下文去决策。合成接口的思路是「原始词表 领域词典 → 新词表 → 新统计模型」三步import jiayan # 预加载的语言模型泛化能力强但领域词不足 base_model jiayan.create_language_model(base_model.klm) # 领域词典格式为“词 频次”每行一条 lexicon_path my_ancient_lexicon.txt # 执行词库合成返回新的模型实例 synthesized_model base_model.synthesize_lexicon(lexicon_path, output_pathmy_synth.klm) # 重新加载合成后的模型用于后续分词 lm jiayan.load_language_model(my_synth.klm)代码里的create_language_model负责把基础统计模型读进内存synthesize_lexicon是核心方法它对词典里的每个词做词频加权把新词插入模型词表的同时重新归一化概率分布load_language_model加载的是合成产物不是原模型。参数上要注意两点output_path一定要显式指定否则结果只存在内存里退出进程后合成就白做了词典编码必须是 UTF-8繁体或异体字保持和语料一致的写法否则合成后词条无法命中原文。2.3 参数与边界词频、词长与词典格式参数影响建议值词频决定词条在解码时的先验权重词频越高越容易被优先切出不确定时填 1避免过度干预词长影响切分粒度过长会把正常词组吞并以 26 字为主人名地名最多 8 字平滑控制未登录词的容忍度越高越容易切出词典外的组合保持默认除非发现分词过碎这里有一个边界很容易被忽略词库合成不是越多越好。某开发者一开始把整本辞书都塞进词典结果分词把所有二字常词都切成成语块反而拉低了准确率。词库合成解决的是「领域词缺失」不是「用词典替代统计模型」。合成后最好用一小段验证语料来回测分词校准词频权重而不是直接上全量数据。3. 分词与词性标注从原始文本到可读标注结果3.1 分词接口与基本用法分词是甲言最基础的功能加载合成后的语言模型直接对句子做切分from jiayan import load_language_model, get_segmenter lm load_language_model(my_synth.klm) segmenter get_segmenter(lm) text 子曰學而時習之不亦說乎 tokens segmenter.segment(text) print(tokens) # [子曰, 學而, 時習之, 不亦, 說乎]load_language_model返回的是解码器依赖的概率模型get_segmenter是外层封装的切分器它内部会做字符级图解码然后按最大联合概率搜索最优切分路径。参数上segment方法只接受字符串不接受列表文本里的换行符和空格会被当成切分边界处理所以如果你的语料带有行号先清洗干净再传入。分词的输出粒度由模型决定而不是由词典决定。这句话的意思是说词典只负责提供候选词到底切不切、切多长取决于整个句子的联合概率。从实操来看文言虚词如“之、乎、者、也”经常被单独切出这是正常的但如果把“天下”切成了“天/下”就要检查词典里有没有这两个字的合成词条以及词频是否太低。3.2 词性标注认识甲言的标签体系词性标注在分词结果上做二次分类。甲言对分词后的每个 token 输出一个标签标签体系接近北大词性标注集但针对文言文做了调整from jiayan import get_postagger tagger get_postagger(lm) tagged tagger.tag(tokens) print(tagged) # [(子曰, v), (學而, c), (時習之, v), (不亦, d), (說乎, v)]get_postagger返回一个标注器实例tag方法接收上文分词得到的词列表按顺序输出“(词, 词性)”元组。注意它不会自己再分词所以喂给它的列表必须是已经切好的如果你把原始句子直接传进去它会按单字逐字标注输出结果基本没法用。甲言的词性集合里v代表动词n名词c连词d副词p介词u助词。这套标签和白话文工具的主要差异在助词和语气词上文言文里大量出现的“也、矣、焉、哉”多数归入助词u或语气词y如果你看到的标注集定义有点不一样以你下载的源码包里的标签文档为准。标注速度在普通笔记本上大约每秒几百到一千词属于可接受范围。3.3 标注后处理把结果转成 JSON 或 DataFrame裸的分词和词性结果很难直接落地我一般会转成结构化的中间格式方便后续做统计、筛选或写回数据库。这里给一个通用的转换函数import pandas as pd def to_dataframe(tokens, tagged): rows [] for word, pos in tagged: rows.append({ token: word, pos: pos, length: len(word) }) return pd.DataFrame(rows) df to_dataframe(tokens, tagged) print(df)这个函数接收分词列表和标注列表逐条组装成字典再转 DataFrame。注意两个输入列表长度必须一致否则会造成行错位你可以把tag(tokens)的结果按词对齐检查一遍len(tokens) len(tagged)是硬性条件。length字段是我额外加的用于过滤超长未登录词如果发现某个 token 长度超过 10基本都是词库合成或分词阶段出了问题这种记录应该单独抽出来排查。4. 断句与标点文言文句读恢复的完整流程4.1 断句模块从连续文本到短句古文没有标点断句是最耗费人工的一步。甲言的断句模块本质上是一个序列标注模型判断每个字后面要不要切分。使用方式如下from jiayan import get_sentences_splitter splitter get_sentences_splitter(lm) raw 大學之道在明明德在親民在止於至善 sentences splitter.split_sentence(raw) print(sentences) # [大學之道在明明德, 在親民, 在止於至善]get_sentences_splitter返回断句器split_sentence按句读切分一整段文本。逻辑上它先对每个字位预测一个切分概率再用阈值选择最终断点较大的阈值会让断句更保守输出长句更多较小的阈值会切得更碎适合处理短句为主的语录体。这里有个经验值处理论说文时阈值调高处理语录、对话时阈值调低。文本长度方面split_sentence接收长段落没有问题但如果传入上千字的整篇文章建议先按段落拆开避免一次解码耗时过长。4.2 标点标注句读、逗号与语气词断句只解决“在哪里断”不解决“断完打什么标点”。甲言的标点功能进一步区分句号、逗号和问叹语气。我习惯的做法是先断句再对每个短句做内部停顿预测from jiayan import get_punct_tokenizer punct get_punct_tokenizer(lm) text_with_punct punct.punctuate(大學之道在明明德在親民在止於至善) print(text_with_punct) # 大學之道在明明德在親民在止於至善。get_punct_tokenizer对标点位置做分类输出带标点符号的完整文本。注意它的输入是原始连续文本不是分好词的列表因为标点和分词共用了内部的上下文特征。参数上标点模型对语气词比较敏感遇到“乎、哉、耶”倾向于输出问号遇到“矣、焉、也”倾向于输出句号如果你处理的文本是奏章或碑文这种语气判断不一定可靠最好在后处理里做规则修正。4.3 常见误用把白话文模型思路套到文言文最容易翻车的一点是有人把白话文 NER 或标点恢复模型的思路直接套过来拿甲言当黑匣子用。文言文的句读高度依赖句式结构和语气词分布用现代汉语标点模型硬跑结果通常是乱打逗号。另一个常见误用是混淆断句和分词断句器输出的每个子句仍然需要再走一遍分词和词性标注不能直接拿子句字符串去匹配关键词因为子句内部还可能存在未登录词。实操上我更推荐「断句 → 分词 → 词性标注」三步走每一步都保留原始位置信息最后再合并结果这样排查错误时能定位到具体环节。5. 避坑指南甲言使用中最容易翻车的五个问题5.1 现象一词典合成了但分词结果没变某个开发者按示例把词典合成后重新加载模型分词输出完全没变化他以为是合成接口失效。原因合成后的模型没有覆盖原模型路径。代码里合成是通过synthesize_lexicon生成新文件但如果后面load_language_model仍然指向基础模型路径等于没加载产物。解决每次加载后先打印模型路径确认落盘位置更稳妥的做法是直接比较base_model和synthesized_model的词表大小合成后词表一定比原来大。从那以后我每次做完词库合成都会先跑一行词表比对再进全量流程。5.2 现象二断句结果把并列句切碎了有一份地方志语料原文本是“山高水长林密路险”断句器输出变成“山高/水长/林密/路险”四个短句结构上确实切开了但人为破坏了并列关系。原因断句阈值设得太低模型在每一个对仗点都判了句读。解决调整断句阈值把每一次的切分概率打印出来结合语料判断最优值。处理对仗文本时我会在断句后加一条保护规则——如果切出的子句长度小于 4 字且前后子句结构相似就合并。这个规则看起来简单实际能救回一半以上被切碎的对仗句。5.3 现象三自定义词一直未被识别词库合成后词典里明明有“巡抚”和“布政使”分词结果还是“巡/抚”“布/政/使”。原因词典词频设置太低或词典文件里的词和语料写法不一致。比如语料用的是繁体“巡撫”词典里写了简体“巡抚”合成后词条永远匹配不上。解决写一个自动检查脚本把词典词逐一在验证语料里做字符串匹配统计命中率命中率低于 80% 就回查写法差异。另一个原因是词频把高频词频调到 50 以上让解码器优先选它。5.4 现象四安装时依赖版本冲突安装完甲言后同一个环境里别的新版工具报错反过来把甲言卸了又没法用。原因甲言的 C 扩展依赖特定版本的编译接口新编译器和旧包之间存在 ABI 不兼容。解决创建独立虚拟环境把安装命令和验证命令直接写进requirements.txt同时锁住 Python 版本建议使用 3.9 或 3.10等底层扩展更新后再尝试更高版本。这条经验是我踩过最久的坑后来所有古文项目组都强制统一环境版本。5.5 现象五处理长文本时速度骤降一段 2000 字的地方志跑了几分钟还没出结果最后内存直接拉满。原因断句和分词解码器的复杂度与输入长度呈非线性关系长文本一次解码的路径搜索空间巨大。解决用 200 字为窗口做切分窗口重叠 20 字先粗断再拼接。这个方法牺牲一点边界准确率但速度能提升几十倍。窗口边界处的切分结果我会单独抽出来人工核对尤其是涉及人名地名的位置。6. 进阶把甲言接进自己的语料处理管线当我需要批量处理一整批古籍时单条调用就不好使了。我通常会把甲言的四个核心能力封装成一个完整管线词库检查、断句、分词、词性标注统一输出结构化记录再配合正则在结果上做修正。def process_text(raw_text, lm, splitter, segmenter, tagger): lines [] for chunk in split_text(raw_text, size200, overlap20): sub_sents splitter.split_sentence(chunk) for sent in sub_sents: tokens segmenter.segment(sent) tagged tagger.tag(tokens) lines.extend(to_dataframe(tokens, tagged).to_dict(records)) return linessplit_text是我的窗口切分函数size200决定了每次送入解码器的文本长度overlap20保留边界上下文确保断句在窗口交界处不会断裂。to_dataframe复用上一章的转换逻辑把元组列表展开成结构化记录。封装完成后整批语料都以统一的 DataFrame 形式写回 CSV方便后续查重和人工校对。如果在管线里再叠加一个小技巧对分词结果里的专有名词单独维护一份规则表匹配到“左迁”“擢”“除”这类官职变动词时把相邻的人名一并抽出就能直接服务于官职任免类信息抽取。这类规则不依赖模型概率是纯字符串逻辑但恰好补上了通用统计模型在历史语境上的短板。从那以后我每次处理古籍语料都会强制走一遍先查词库命中率再调断句阈值最后人工抽查窗口边界。整套流程跑完分词的稳定性和断句的可用度都上了一个台阶。希望帮到你。本文还有配套的精品资源点击获取