粤语NLP实战:pycantonese分词、词性标注与粤拼转换指南
简介pycantonese是一套面向粤语语言学与自然语言处理场景的Python工具库适合从事粤语文本挖掘、语音数据研究或计算语言学的开发者使用。它围绕粤语特性提供Jyutping罗马音转换、分词、停用词过滤和词性标注等核心能力可帮助解决粤语拼音转换困难、词边界模糊、语法结构特殊等常见问题。资源包共294个文件压缩包大小约15.46MB以py源码、rst/txt/html文档、js脚本及woff/woff2字体资源为主既包含库本体完整代码也附有可供参考的构建文档与说明材料。值得一提的是压缩包内还收录了FC系列粤语对话语料.cha样本便于读者结合真实语料快速上手实验。配合NLTK、spaCy等框架pycantonese可进一步支撑情感分析、语义理解乃至机器翻译等高级任务无论用于学术研究还是粤语产品开发都是值得收藏的实用工具包。已有689人浏览学习。1. 「唔該」不是「唔」加「該」粤语 NLP 为什么绕不开 pycantonese「唔該」在粤语使用者的日常对话里是「谢谢」的意思但在通用中文分词器眼里它经常被切成「唔 / 該」两个无意义碎片。粤语书面语与普通话文本在词汇、虚词、句式代号上差异极大通用 NLP 工具链面对「食咗」「我哋」「埋單」这种口语词时准确率会断崖式下跌。pycantonese 正是为这个场景而生的 Python 库它把粤语语言学的语料知识、读音规则和 NLP 工程能力接在一起提供分词、词性标注、粤拼转换、谐音和字词频数据等能力。这篇文章按「装环境 → 分词标注 → 粤拼 → 踩坑 → 进阶」的顺序走一遍可复现路径适合做粤语语料清洗、字幕对齐、语言学习产品的人也适合想理解「一个语言分支的 NLP 落地到底难在哪」的开发者。2. 用 pycantonese 搭建粤语文本处理环境安装、数据与首次加载先明确一件事pycantonese 不是一个大而全的深度学习框架它是一个把「粤语语言学资产」工程化的工具库。理解这一点能帮你省下不少瞎调参数的时间。它的主要价值在数据与规则层比如内置口语语料、读音对应表、字词频统计以及基于这些数据训练或推导出来的分词与标注能力。接下来的三节我会从安装开始一步步走到第一次调用分词函数。2.1 pip 安装与依赖检查最小环境长什么样常见做法是用虚拟环境隔离项目依赖避免把系统 Python 环境搞乱。安装命令很简单python -m venv .venv source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate pip install pycantonese安装完成后先验证包是否能正常导入再检查版本与依赖状态pip list | grep -i cantonese python -c import pycantonese; print(pycantonese.__version__)这里有两个参数值得说明。第一Python 版本建议用 3.10 或更新的稳定版过旧的解释器在字符串处理、类型标注上会暴露一系列兼容问题尤其是后面涉及粤拼字符规范化时旧版本的标准库行为差异很容易变成隐形坑。第二如果 pip 安装时出现依赖下载超时不要盲目加--no-cache-dir先确认网络源或换镜像源再重试安装。提示pycantonese 的公开接口在不同版本里可能有微调装完先跑一次dir(pycantonese)把当前版本暴露的函数名过目一遍再进下一步。这个习惯能避免照着旧教程写代码时遇到AttributeError。2.2 内置语料与读音数据首次加载时发生了什么pycantonese 之所以比通用分词器更适合粤语核心在于它内置了面向粤语口语的语料资产。我第一次用的时候以为它只是一组分词函数后来才发现每当调用涉及分词、读音、词频的接口时库里会加载对应的语料数据与读音对照表。这个过程不是初始化时一次性发生的而是在首次调用时按需载入因此冷启动会有一次可感知的延迟。理解这个机制对性能调优很重要。如果你是在 Web 服务里使用它最好在服务启动阶段主动触发一次数据加载而不是让第一个请求去承担冷启动耗时。可以通过查看模块级弹出的公开入口来确认当前版本的数据加载方式import pycantonese as pc # 列出当前版本暴露的全部公开入口确认数据模块与函数名 entry_names [n for n in dir(pc) if not n.startswith(_)] print(entry_names)这段代码的逻辑很简单dir(pc)返回模块的全部属性名过滤掉下划线开头的内部属性剩下的就是你可以直接调用的接口或数据对象。运行后你会看到类似segment、pos_tag、语料相关对象等一系列名字。这一步相当于先摸清工具台上有哪些扳手再动手拆机器。参数上唯一要注意的是「按需加载」与「常驻内存」的取舍。如果程序反复调用分词接口数据一旦加载就会留在进程内存里后续调用会明显变快但如果你的程序是短生命周期的一次性脚本那就没必要做启动预热直接调用即可。2.3 第一次调用分词先看输出再看报错跑通一个最小的分词脚本是确认环境与接口是否匹配的最快方式。我通常用一句包含典型粤语口语词的文本做冒烟测试比如「尋日」昨天、「我哋」我们、「餐廳」、「食飯」、「埋單」结账、「銀包」钱包import pycantonese as pc text 尋日我哋成班人去咗餐廳食飯埋單嗰陣先知唔記得帶銀包。 tokens pc.segment(text) print(tokens)如果当前版本把分词接口放在了子模块里直接调用pc.segment可能报AttributeError这时不要慌回到 2.2 节列出的入口清单里找到对应函数名即可。分词输出的通常是一个列表每个元素是一个切分后的词项比如「成班人」「食飯」「埋單」会被尽量保留为整体这正是粤语词典与统计模型在起作用。第一次运行如果报错多半不是 pycantonese 自身的问题而是输入文本的编码。命令行直接粘贴中文一般没有问题但如果从文件读取请务必使用open(..., encodingutf-8)这一点在 Windows 环境下尤其重要否则你会看到各种「粤拼乱码」和字符乱码后文第 5 章会专门展开。3. 粤语分词与词性标注把「食咗飯」切对不是玄学分词和词性标注是粤语 NLP 的基本盘。通用中文分词器在普通话新闻语料上表现不错但一旦碰上粤语口语切分结果就会变得很难看。这章的思路是先看通用工具怎么翻车再用 pycantonese 的正确姿势做分词与标注最后讲清楚分词前的预处理参数应该怎么控制。3.1 通用中文分词器在粤语口语上的翻车现场「我尋日食咗飯而家好飽。」这句话如果用通用分词器处理经常会被切成「我 / 尋 / 日 / 食 / 咗 / 飯 / 而家 / 好 / 飽」。「尋日」「咗」「而家」这些词在普通话词典里根本不存在通用分词器只能把它们按单字拆开。更麻烦的是语气词和虚词比如「啦」「㗎」「咋」「啫」它们没有实在意义但在粤语里承担重要的语气功能通用工具会直接把它们当作标点或噪声丢弃。对比一下两类工具的目标差异通用分词器追求的是在混合文本上的平均表现词表以普通话常用词为主pycantonese 针对的是粤语口语与书面语词表里收录了大量粤语特有词汇和语料统计结果。这不是谁比谁强的简单比拼而是语域匹配的问题。拿粤语文本去用普通话词典就像拿着普通话菜单到粤菜馆点菜不是菜不好是工具不匹配。所以我的建议是如果你的数据里夹杂普通话和粤语先用语言识别或关键字集合判断文本语域再分流处理。纯粤语或粤语为主的语料直接用 pycantonese 处理普通话语料多的才考虑混用通用工具。判断的方法很简单统计「嘅、咗、哋、唔、係」这些高频粤语字的出现密度大于阈值就走粤语管道。3.2 分词结果与词性标注一个段落两种输出词性标注是分词的下一级操作。pycantonese 的做法是先切词再对每个词项给出词性标签。下面这段代码把分词和标注放在一起跑import pycantonese as pc text 佢尋日唔舒服所以請咗一日假。 tokens pc.segment(text) annotated pc.pos_tag(tokens) for token, pos in zip(tokens, annotated): print(f{token}\t{pos})这段代码的逻辑是segment先返回词项列表pos_tag接收词项列表后返回等长的词性标签列表zip把词项与标签一一配对打印。这里有一个容易误解的点pos_tag的输入不是原始句子而是已经切好的词项列表。如果你直接把字符串塞进去接口会因类型不匹配报错。关于词性标签要注意它使用的标注集与普通话工具并不完全相同具体标签缩写需要以当前版本输出的实际值为准。我的建议是先用一小段文本打印所有出现的标签统计成一个标签表再决定是否要映射到你业务系统内部的词性体系。这一步看起来多余但在做后续特征工程时能省很多时间尤其是当你需要把词性特征喂给下游模型时标签集不一致会导致灾难性的特征引用错误。3.3 分词参数怎么调词典、缓存与最小词长很多从通用分词器转过来的用户会习惯性地找「自定义词典」「最小词长」之类的配置项。pycantonese 的分词逻辑更多依赖于内置语料与统计模型自定义词典并不像通用工具那样即插即用。我一般做的事情是控制「输入侧」而不是「词表侧」。最有效的输入侧控制是预处理。粤语文本常带有全角标点、零宽字符、特殊空格等噪声这些字符会严重影响切分。下面是我常用的规范化函数import re import unicodedata import pycantonese as pc def normalize_for_cantonese(text: str) - str: # 全角空格统一为半角空格 text text.replace(\u3000, ) # 将全角标点统一为半角表单方便后续处理 text unicodedata.normalize(NFKC, text) # 在主要标点两侧补空格避免标点粘在词项上 text re.sub(r([。、]), r \1 , text) return text.strip() raw_text 而家三點半我哋去食糖水好唔好 for token in pc.segment(normalize_for_cantonese(raw_text)): print(token)这段代码包含三个关键处理点NFKC归一化会把全角逗号、括号、数字统一成半角减少字符变体带来的干扰正则替换规则在标点前后补空格是为了防止标点与相邻词项粘在一起造成切分边界错误最后strip()去掉首尾空白。参数上真正值得调节的不是分词器内部而是输入文本的清洗强度。如果语料是从网页抓取的保留 HTML 实体转义符如果是字幕文件还夹杂时间轴标记。我强烈建议在进入分词之前把跟正文无关的标记全部剥离否则分词器会把「wolf-720p」「00:01:23,400」这样的噪声也当成处理对象输出结果会显得非常碎。4. 粤拼转换与语言学前置数据从汉字到读音这一章进入标题里「语言学」的部分。粤拼Jyutping是粤语的罗马化注音方案pycantonese 把汉字到粤拼的转换做成了可直接调用的能力。此外同音字查询和字词频数据也是语言学资产在工程里的典型应用。理解了这些你才能把 pycantonese 从「分词工具」升级成「粤语文本分析工具箱」。4.1 用 pycantonese 把整句文本转成粤拼串把汉字转成粤拼最常见的场景包括语音合成前端、字幕注音、错别字校对。逐字转换的代码很直观import pycantonese as pc text 唔該幫我落單 for ch in text: jp pc.jyutping(ch) print(ch, jp)这里pc.jyutping(ch)接收单个汉字返回对应的粤拼字符串。粤拼的特色是自带声调数字比如「唔」是m4「該」是goi1数字 1 到 6 分别代表六个声调。这个数字设计对工程非常友好你可以直接通过去除末尾数字来获得不带声调的读音也可以把数字当作声调特征输入模型。实际使用中要注意多音字会按照语料统计下的最常见读音返回而不是根据上下文动态选择读音。例如「行」在「銀行」和「行路」里读音不同如果你需要按词定音最好先分词再逐词查询或者拿到所有候选读音后自行根据上下文选择。这一点是粤拼转换最大的边界不要指望它像语音合成前端那样做完整的多音字消歧。提示如果你需要的是「词级」粤拼而不是「字级」粤拼做法是先分词再对每个词项取第一个字的读音或者按词项拼接字符读音。实际工程项目里词级读音对下游任务更有意义。4.2 谐音与同音字组语言学功能在工程里的真实用途pycantonese 的数据里包含读音到汉字的映射关系利用它可以实现「给定一个字的读音找到所有同音字」的功能。这个能力在错别字纠错、昵称生成、歌词近似匹配里非常实用。不需要专门找接口用字符到粤拼的转换就能自己构建一个同音字查询import pycantonese as pc chars 唔毋吾梧 target pc.jyutping(唔) same_sound [ch for ch in chars if pc.jyutping(ch) target] print(target, same_sound)这段代码的逻辑很简单对候选字集合里的每个字转粤拼与目标字的粤拼做精确匹配收集所有同音字。要注意的是粤拼字符串包含声调数字所以「唔」(m4) 和「毋」如果是同音它们的粤拼完全相同才能匹配上。如果你想把声调不同的字也视为近似音就先把末尾的数字去掉再比较。这个功能在实际产品里可以做成「读音容错映射表」。比如用户输入歌词时写成了「唔該」但正确文本是「毋該」虽然字形差异大读音却高度一致通过读音匹配就能召回。不过要提醒一句粤语存在文白异读现象同一个字在读书音和白话音里可能读音不同pycantonese 返回的通常是常见层级的读音遇到生僻语境时要人工复核。4.3 词频表与字频数据做文本分析的第二个入口「语言学」在工程里的另一层落地是统计资产。pycantonese 内置的语料可以用来统计字频、词频这对教学大纲设计、字幕重点词筛选、语料质量评估都有直接帮助。即使你不直接使用它内置的统计接口也可以把语料导出成纯文本后用标准库做计数from collections import Counter def top_chars(corpus_texts: list[str], n: int 30) - list[tuple[str, int]]: counter Counter() for text in corpus_texts: # 跳过空白字符统计每个汉字出现次数 counter.update(ch for ch in text if not ch.isspace()) return counter.most_common(n)逻辑是把语料列表逐句展开成字符流用Counter累加频率最后取前 n 个高频字。如果你需要的是词频则先对每句分词再对词项列表做同样的统计。这里有一个容易忽略的细节字频和词频的用途完全不同。字频适合做「常用字表」和输入法词库排序词频适合做「教学词汇表」和「文本主题判断」。做之前先想清楚下游要什么不要拿词频数据硬套字频分析场景。语料本身的语域也会直接影响统计结果口语会话语料统计出来的高频词和新闻语料的高频词差很远。5. 粤语 NLP 的坑位清单编码、标注集与语料差异用 pycantonese 做过一轮真实项目后你会发现坑往往不在库的功能缺失而在数据与环境的细节上。这一章整理 4 个我必须反复提醒的踩坑点每条按「现象 → 原因 → 解决」展开。5.1 Python 字符串编码导致粤拼乱码现象在 Windows 终端里运行粤拼转换脚本输出的m4 goi1变成乱码或者csv文件里出现大量锟斤拷。原因终端默认编码与 Python 标准输出的 UTF-8 不一致写入文件时没有显式指定编码导致系统按默认编码Windows 下常为 GBK写入粤拼字符串。解决运行脚本前设置环境变量set PYTHONIOENCODINGutf-8读写文件时显式指定编码。写入文件时建议用utf-8-sig因为某些 Windows 工具比如 Excel打开无 BOM 的 UTF-8 文件会乱码with open(jyutping_out.csv, w, encodingutf-8-sig) as f: f.write(字,粵拼\n) for ch in text: f.write(f{ch},{pc.jyutping(ch)}\n)顺带提醒从网页复制文本时很容易带入零宽空格和零宽连字符这些不可见字符会混进文本里看似正常的字符串在分词和转换时产生诡异结果。遇到输出正常但匹配异常的情况优先检查字符串编码与不可见字符。5.2 词性标注集与普通话标注集不一致的乌龙现象拿着普通话词性标注规范的文档去解释 pycantonese 的输出看到某个标签后开始猜含义结果语法特征映射全错。原因pycantonese 的词性标注基于粤语语料自身的标注体系与普通话工具使用的标注集来源不同缩写也不通用。解决不要猜直接跑一段覆盖各类词性的文本把输出标签全部打印出来建立一份「当前版本标签实际含义」的本地注释。如果业务系统内部有自己定义的词性体系写一个映射函数把 pycantonese 的标签映射成内部标签而不要把词性字符串直接当特征丢给下游模型。标签集不一致引发的问题在特征工程阶段很难排查因为错误隐藏在字符串匹配里。5.3 口语文本与书面语文本在分词上的明显偏差现象用 pycantonese 处理新闻稿或正式公告分词结果显得碎不少应该成词的搭配被切开了。原因内置语料的语域偏向粤语口语会话词频统计和词典结构都更贴合「傾偈」聊天场景对正式书面语的覆盖不足。解决先用关键词集合判断输入文本的语域。如果文本偏书面语就要做好心理预期切分质量可能不如口语场景。更稳妥的方案是在书面粤语场景下用通用中文分词器作为基线再用 pycantonese 的分词结果与基线做对比在差异处人工抽查确定最终以哪个结果为准。实际项目里我会把这个判断逻辑封装成一个函数根据「嘅、咗、哋」等口语词密度自动选择处理管道。5.4 标点与全角字符预处理不到位导致切分边界错乱现象「食咗飯。」被切出「飯。」这样的词项或「而家三點半」里的全角逗号把整句切割成奇怪片段。原因没有对全角标点做归一化标点直接粘在相邻词项上分词器只能把「标点 词」当成一个整体。解决在进入分词之前把全角标点统一为半角并在标点两侧补空格。我在 3.3 节给的normalize_for_cantonese函数可以复用。如果语料里有特殊符号比如「/」「」「#」按业务需求决定是否在预处理时替换成空格。有一个原则可以记一下宁可让切分结果多出几个孤立标点也不要让标点污染真正的词项边界。孤立标点在后续过滤阶段很容易处理污染后的词项才是真正的麻烦。6. 进阶技巧用粤拼做发音相似度匹配最后一个技巧是我在字幕校对与用户输入纠错项目里反复用到的把文本转成粤拼再做读音层面的相似度匹配。这个方法特别适合处理「白字」同音错别字问题因为很多粤语口语词的写法不唯一用户输入时经常写成另一个同音字。做法分成三步。第一步把字符转成去掉声调后的粤拼第二步用字符串相似度算法做比较第三步设定阈值判断是否算发音相近import re import unicodedata from difflib import SequenceMatcher import pycantonese as pc def norm_jyutping(ch: str) - str: s pc.jyutping(ch) # 去掉末尾声调数字只保留声母韵母信息 return re.sub(r[0-9]$, , unicodedata.normalize(NFC, s)) def sound_similar(ch_a: str, ch_b: str) - float: if ch_a ch_b: return 1.0 pa, pb norm_jyutping(ch_a), norm_jyutping(ch_b) if not pa or not pb: return 0.0 return SequenceMatcher(None, pa, pb).ratio() for pair in [(唔, 吾), (咪, 米), (係, 系)]: print(pair, round(sound_similar(*pair), 2))这段代码里norm_jyutping负责把「唔」的m4转成m把「該」的goi1转成goiSequenceMatcher计算两个拼写串的相似度。我自己在项目里常用的参考阈值是 0.7大于 0.7 视为发音接近进入候选替换0.5 到 0.7 之间需要人工复核低于 0.5 直接放弃。这个阈值需要根据你的具体字符集微调比如带声调比较时阈值通常要提高到 0.8 以上。要注意的是粤拼的声母韵母结构比普通话拼音更复杂韵母里有长元音和短元音对比如aa和a相似度计算时会被视为不同字符。这是符合预期的因为粤语里这两个音确实有辨义作用。如果你做的是宽松匹配可以在比较前把aa归一化成a但代价是召回率上升的同时误判也变多。我自己现在处理粤语文本的习惯是先把语域判断、字符规范化和分词封装成一个流水线函数再在流水线之后接业务逻辑而不是每次临时写一遍预处理。这样既能保证不同脚本之间的结果一致也方便在语料版本或接口变化时只改一处。希望帮到你。本文还有配套的精品资源点击获取