☰
韩语词库构建实战:数据清洗、词条建模与Anki导出
2026/10/11 2:38:38 网站建设 项目流程

简介:一份面向韩语学习与词典开发的词汇表资源,适合韩语学习者、词典应用开发者和语料分析人员使用。压缩包内共有五个文件,包含两个词典数据文件、两个词表文本文件和一个说明文档,压缩包大小约28.37MB。词表文件分别提供包含重复词的完整列表和去重后的独立列表,便于批量记忆或构建词库;两个词典数据文件存储了整理前后的词条与释义,可配合韩国国立国语院标准词典的检索链接查询对应词汇。资源已有三百九十一人学习下载,适合需要系统梳理韩语单词、开发检索工具或进行自然语言处理的学习者。通过对比整理前后的词典数据,可以更直观地掌握每一条词汇的用法变化,同时借助说明文档中的查询示例,能够快速验证单词的准确含义,从而高效搭建个人韩语学习语料库。

1. korean_wordlist到底是什么:不是词表,是词库工程

korean_wordlist这个标题听起来像是“把韩语单词整理进一张表”的轻量任务,但真下手做一遍,你会发现它是文本清洗、数据建模、检索设计三件事的合体。我最早为某个模拟备考工具做底层词库时,第一版方案朴素得可笑:抓一份公开词表、转CSV、直接入库。结果同一单词以不同词性重复出现、动词全是-다结尾导致没法背、CSV一用Excel打开就乱码,前后返工三次才把数据理干净。这个项目要做的事,是用一套可复现的流水线,把散落各处的韩语词汇整理成字段统一、分级明确、能直接对接Anki、App或NLP任务的结构化词表。适合三类人:做韩语学习工具的开发者、要干净词表做NLP实验的入门者、想自建背诵数据的韩语学习者。

2. 词条建模先于写代码:韩语词汇表该有的字段与分级

词库最贵的决定是字段设计。字段定死,清洗、去重、导出都在轨道上跑;字段没定死,每加一个功能都意味着全量返工。我见过不止一个项目把“单词-释义”两列当作词库全部家当,后来要加发音时发现没罗马音字段、要按TOPIK等级推送时发现没等级字段,只能重新处理一遍整个数据。所以这一章先把词条结构讲透,再谈实现。

2.1 最少字段集:谚文、罗马音、词性、释义缺一不可

一份面向韩语学习者的词表,最少得四个字段:谚文原词(word)、罗马音(romanized)、词性(pos)、中文释义(meaning)。这四个分别回答“怎么写”“怎么读”“什么词类”“什么意思”,哪一个缺失,下游都会有问题。释义缺了在流水线里立刻能发现,罗马音缺了往往要等做发音功能才暴露,两次返工的成本远高于一开始就留好位置。

{ "word": "학교", "romanized": "hakgyo", "pos": "noun", "meaning": "学校", "level": "topik1", "frequency_rank": 87 }

我一般还会追加三个可选字段:level(TOPIK等级)、frequency_rank(频次排名)、examples(例句数组)。等级和频次是学习路径排序的依据;例句是语境理解的载体,对黏着语尤其重要,因为单词在句子里的形态常常和词典形长得不一样。还有一个细节容易忽略:word字段必须是词典形,动词形容词用-다结尾。比如“가다”是合法词条,“갑니다”就不该出现,它是“가다”的敬语活用形,放进词表会污染去重和搜索逻辑。

词性的粒度也要提前谈好。韩语词性粗分有名词、代词、数词、动词、形容词、冠形词、副词、感叹词,但学习工具最常用的粒度就是四类:noun、verb、adj、adv。初版词库不必陷入他动词/自动词的细分——那些留给NLP场景后续再加,学习场景里动词就是动词,欠一个级别的精确度换到的是一大幅度降低的维护成本。

2.2 分级与来源:TOPIK等级和高频词表怎么对齐

韩语学习领域公认的难度标尺是TOPIK考试体系:TOPIK I对应1-2级,TOPIK II对应3-6级。市场上流传的“TOPIK必备词表”大多按这个体系做Tag,一份完整的korean_wordlist应当给每个词打上level标签,这也是后续所有按难度切分功能的前提。

做分级时要特别核对来源的原始名称。有些表叫“TOPIK初级词表”,内部却混入大量中高级词;有些叫“高频6000词”,但高频词和考试词的重合率并没有想象中高——前者按语料库词频排序,后者按考点重要性排序,两套逻辑不能混着用。

我实践下来可行的对齐路径是:拿一份公开的TOPIK分级词表做主集,另一份开放语料的高频词表做排序辅助,遇到两表等级冲突(某词在A表是2级、在B表是3级),就人工去查考试真题里该词出现的篇数来裁定。这个过程自动化能做一半:先把两表都转成统一词条结构,再按word做join,冲突记录落盘,最后人工抽检被标记的冲突词。不要迷信任何一份来源的完整性,多源交叉才能过滤出可信的等级数据。

初版词库选多大容量也是个决定项。常见做法是直接做全量考试词表(约6000词),但如果目标是快速上线一个可用版本,优先收录高频1000词加初级词表,保证前两个月的内容质量。容量小一点没关系,字段设计正确后后续扩充只是往流水线里喂数据的问题。

2.3 活用形不搞清,词库就是个空壳

韩语是黏着语,动词和形容词的活用体系是词库建模里最绕不开的结构性问题。一个韩语动词如果只存词典形,不标记词干,后续做例句对齐或搜索映射都会很吃力。以“가다”(去)为例:词干是“가”,加连接词尾-ㅂ니다得“갑니다”(去,敬语);加过去时词尾-았어요得“갔어요”(去了)。同一个词目下挂着一整族不同结尾的词形。

通常做法是在词条结构里增加一个可选的"stem"字段,存放去除-다之后的结果。这样词库既保留了学习者需要的词典形作为主入口,又给检索层留了一条从其他形态映射回词典形的路径。

{ "word": "가다", "stem": "가", "romanized": "gada", "pos": "verb", "meaning": "去", "level": "topik1", "examples": [ { "sentence": "학교에 갑니다", "translation": "去学校" } ] }

实际搜索场景里,用户在App里输入“갑니다”,预处理阶段如果能先用一组词尾规则把它裁剪成“가”,再拿“가”去词库匹配,命中率会显著提高。不做这个设计,用户一点击活用形就搜不到词,这在韩语学习工具里是体验灾难。具体词尾规则的完整列表很长,初版词库不需要全部实现,先覆盖使用频率最高的-습니다/-ㅂ니다、-았/었어요、-는/은三类就够了,剩余规则等用户反馈出来再补,避免一开始就被词尾表淹没。

3. 从原始词表到korean_wordlist:构建清洗流水线

词条结构定好后,做的事情是把来自不同源头、格式混乱的原始词表清洗成符合模型的结构化数据。清洗流水线我拆成三步:Unicode归一化、词典形去重、多格式导出。每一层只解决一类问题,顺序不能颠倒,否则后面做完了再回头补归一化,去重和排序全要重做。

3.1 韩文Unicode归一化:NFC和全角符号的细节

韩文在Unicode里的编码方式有两个层次,这是所有韩文处理项目的第一个坑。现代韩文音节(如“학교”)在Unicode里是预先组合好的单一码点,两个音节恰好是两个码点。但不少网页和文档工具会把音节拆分成初声、中声、终声的序列存放——同一面目全非的“학교”在底层是两个不同的字符串。如果不做归一化,它们在字典里会各自占据不同的key,去重失效、排序错乱、搜索失配全都会找上门。

import unicodedata def normalize_hangul_line(text: str) -> str: # NFC 把拆开的初/中/终声重新组合成完整音节 text = unicodedata.normalize("NFC", text) # 清理零宽空格与 BOM text = text.replace("\u200b", "").replace("\ufeff", "") # 全角符号统一为半角,韩文词表常混入全角括号/句点/逗号 table = {ord(full): ord(half) for full, half in zip( "():;,.", "():;.,")} return text.translate(table) assert normalize_hangul_line("학교") == "학교"

代码里值得解释的参数有三个。一是unicodedata.normalize("NFC"):NFC形式把拆开的子序列重新组合成预组合音节,这是韩文处理的标准起点;二是零宽空格(U+200B)和BOM(U+FEFF)——从网页复制的词表几乎一定会带进来,肉眼不可见,却足以让字符串相等判断静默失败,必须显式清除;三是全角符号映射表:有些来源把括号、逗号写成全角形式,中文环境下导出的词表尤为常见,统一成半角后CSV和搜索才不会出幺蛾子。

韩文音节块在Unicode里的排列规律在这里顺带说明,因为后面章节反复用它。音节起始于U+AC00,每个音节的码点值等于“初声索引×21×28 + 中声索引×28 + 终声索引”。初声19种、中声21种、终声28种(含无终声)。这意味着从一个音节的code point就能反推它由哪三个音素构成,这个逆运算在排序和首字母搜索里是实现的关键。清洗阶段我还会用这个范围做一次全量校验,把不在合法音节区间内的字符全部打印出来人工确认,能拦下不少混进词表的乱码。

3.2 词典形去重:为什么字符串集合去重会误伤

初学者最自然的去重动作是set(word),这在韩语词库上是危险的。同形异义词在韩语里不少,最典型的例子是“배”:它可以是“梨”,也可以是“船”,还可以是“肚子”。三个词条字符串一样,词性一样(都是名词),释义不同。字符串去重会把它们合并成一条,数据量一大,这种静默吞词的问题会成片出现。

def dedupe_entries(entries): seen = set() result = [] for entry in entries: # 去重键:(word, pos),同形异义词靠 sense_id 再拆 key = (entry["word"], entry.get("pos", "")) if key in seen: continue seen.add(key) result.append(entry) return result

去重键用(word, pos)二元组,比单纯用word安全一层,但仍然罩不住“배”这种同形同词性的多义词。进一步的办法是给条目加sense_id字段:两个“배”分别标"fruit"和"ship",去重键升级为(word, pos, sense_id)。sense_id的取名不能依赖自动演化,必须人工介入,因为自动判义的工具在韩语上并不成熟。人工量不会太大,全量词表里真正的多义词大约只有几十个,逐个处理是值得的。另一个隐蔽问题是合并重复来源:多张原始词表拼进来时,同一词条往往出现多次,但各表字段完整度不同——A表有罗马音没有频次,B表有频次没有罗马音。合并时应以字段质量最高的那张表作底,其余表只做补全,而不是后出现的完整条目直接覆盖先出现的。

3.3 多格式导出:JSON、CSV、Markdown一套搞定

清洗完的词条要有一层稳定的导出,不同下游消费的格式不一样。App和API用JSON,数据交换与表格处理用CSV,人工审阅与git diff用Markdown。这一层看似是体力活,但字段顺序若不固定,每次导出都可能列漂移,下游解析就会在版本升级后崩掉。

import json, csv def export_wordlist(entries, json_path="words.json", csv_path="words.csv", md_path="words.md"): with open(json_path, "w", encoding="utf-8") as f: json.dump(entries, f, ensure_ascii=False, indent=2) fieldnames = ["word", "stem", "romanized", "pos", "meaning", "level"] with open(csv_path, "w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for e in entries: writer.writerow({k: e.get(k, "") for k in fieldnames}) with open(md_path, "w", encoding="utf-8") as f: for e in entries: f.write(f"- **{e['word']}** ({e['romanized']}) " f"[{e['pos']}] {e['meaning']}\n")

三个细节直接决定导出好不好用。第一个是json.dump的ensure_ascii=False:不设它,谚文全部变成\uXXXX转义序列,JSON文件人能看懂的部分就只剩键名,不利于调试和审查。第二个是CSV用utf-8-sig编码:它会在文件头写入BOM,Excel和WPS才会正确识别UTF-8,这个参数和newline=""缺一不可,否则要么乱码要么每行后多出空行。第三个是Markdown的模板化输出——每个词条固定为“-单词(罗马音) [词性] 释义”的一行,排进diff工具里能清晰看到增删改,人工校对几百条词条时,这个可读性救过我很多次。

4. 排序、查询与性能:词库用起来的四个关键点

词库清洗完成只是中间态。真正把它做成可用的工具,排序规则、查询参数、存储选型和性能边界这四个点会依次出现。这几条线和韩文字形结构的关联很强,通用文本处理的直觉在这里经常失准。

4.1 谚文字母序与码点序的差异

韩文排序第一直觉是直接用sorted()按字符串排序,这也是多数人卡住的地方。由于预组合音节在Unicode里按音节全表顺序排列,“가나다”整体的码点序大体等于谚文字典序,但以下几个例外会让你在细节处翻车:词条里混入英文标签时,英文小写字母、大写字母、韩文音节在码点空间里并非按你想要的顺序排队;如果词库中残留NFD分解形态,排序结果会和组合音节完全错开。

HANGUL_BASE = 0xAC00 N_CHOSEONG = 19 N_JUNGSEONG = 21 N_JONGSEONG = 28 def hangul_sort_key(word: str): key = [] for ch in word: code = ord(ch) - HANGUL_BASE if 0 <= code < N_CHOSEONG * N_JUNGSEONG * N_JONGSEONG: ch_idx = code // (N_JUNGSEONG * N_JONGSEONG) jung_idx = (code // N_JONGSEONG) % N_JUNGSEONG jong_idx = code % N_JONGSEONG key.extend([ch_idx, jung_idx, jong_idx]) else: key.append(ord(ch) + 1000) return tuple(key)

这个key函数做的是把每一个韩文音节拆成(初声序、中声序、终声序)三个数值,再按字符顺序展平。它等价于把每个音节还原到“ㄱ”到“ㅎ”的空间里逐层比较,结果是严格的谚文字典序。非韩文字符统一加1000偏移量排到所有韩文音节之后,这样词表里即便混入英文或数字标注,也不会把韩文的排序整体打乱。这个key可以直接传给sorted(entries, key=lambda e: hangul_sort_key(e["word"])),性能在万级词条上完全可接受。

4.2 查询过滤参数:等级、词性、首字母

词库超过两三千条就一定要有过滤能力。学习者最常见的诉求不是“找某个单词”,而是“找ㄱ开头的动词”或“找3级名词”。korean_wordlist里的查询接口至少支持level、pos、first_letter、keyword四个过滤维度。

def filter_words(entries, level=None, pos=None, first_letter=None, keyword=None): def first_hangul_letter(word): code = ord(word[0]) - HANGUL_BASE if 0 <= code < N_CHOSEONG * N_JUNGSEONG * N_JONGSEONG: return code // (N_JUNGSEONG * N_JONGSEONG) return None result = [] for e in entries: if level and e.get("level") != level: continue if pos and e.get("pos") != pos: continue if first_letter is not None and first_hangul_letter(e["word"]) != first_letter: continue if keyword and keyword not in e["word"] and keyword not in e["meaning"]: continue result.append(e) return result

三个参数值得强调。first_letter传入的是初声索引而不是谚文字符本身,这样调用方可以传0代表ㄱ、传1代表ㄲ、传14代表ㅎ,与键码或按钮的映射清晰对应。keyword子串匹配同时打word和meaning两个字段,允许用户用中文释义搜词——这个功能学习者很依赖,例如搜“学校”应返回“학교、초등학교、고등학교”一组词。但keyword的实现是线性扫描,等词库涨到十万级,这段循环就必须交给专门的索引,阈值细节见4.4。

4.3 存储选型:SQLite vs JSON vs 内存dict

词库的存储选型是一个纯粹按规模做判断的问题。一个模拟项目X里词条约9000条,配齐例句和翻译后JSON文件体量在3MB上下,启动时整个读进内存、当列表跑过滤循环毫无压力。但当词库扩充到带多种示例、词源、关联词,JSON能膨胀到几十甚至上百MB,每次都全文加载就不行了。此时SQLite是性价比最高的替代方案。

CREATE TABLE words ( id INTEGER PRIMARY KEY, word TEXT NOT NULL, stem TEXT DEFAULT '', romanized TEXT DEFAULT '', pos TEXT DEFAULT '', meaning TEXT DEFAULT '', level TEXT DEFAULT '', frequency_rank INTEGER DEFAULT 0 ); CREATE INDEX idx_word ON words(word); CREATE INDEX idx_level_pos ON words(level, pos);

SQLite单文件、零部署、标准库自带驱动,查询从Python循环换成一两句SQL,在万级到十万级数据上能把耗时有数量级的压缩。词库的查询模式高度固定,过滤字段就是level和pos,索引按这两个字段建就够了。这个场景不需要一个独立的数据库进程,SQLite一个文件跟着代码仓库走,版本管理、测试、分发都方便。

我一般这样判断:万级以下无脑用JSON;万级到十万级切SQLite;再往上已经不是普通学习词库的范畴,需要专门的搜索服务,那时候再来谈全文索引也不迟。别在词库还只有三五千条时就引入重型组件,复杂度从哪里来,维护成本就从哪里涨。

4.4 万级词条的性能边界

最后给一组可验证的预期。万级词条、带例句与翻译的JSON,体积约5到10MB,内存dict加载后单次线性过滤循环耗时在几十毫秒量级,人几乎无感知。如果把这套逻辑做成Web API,瓶颈常在JSON序列化和网络传输上,而不在数据查询本身。

三个优化实践足够覆盖绝大多数场景。一是首字母过滤可以用二分法加速:在已经按hangul_sort_key排序的列表上,先二分出该初声字母的起始与结束下标,只遍历这个子区间做后续过滤,而不是扫全表。二是例句字段序列化开销大,列表接口不要返回examples字段,详情接口才返回,可以省掉一大半传输时间。三是把多级过滤重构成SQL条件拼接下推给SQLite执行,让数据库吃索引,Python只做结果包装。做完这三条,万级词库的查询响应普遍在几十毫秒内,不需要额外组件。

5. 韩语词汇表落地避坑:五条值得记住的踩坑记录

这章记录的是我在实际把词库交到真实场景里之后遇到的五类问题。每条按现象、原因、解决拆开写,都是确定发生过的状况,不是理论推演。

5.1 罗马音标注差一点就差很远

现象:词表里“학교”的罗马音被我手写完变成了“hagyo”,发给朋友试读,对方读成了“哈乔”。原因:韩语罗马字表记法有细密的规则,塞音、紧音、送气音各有约定,ㅓ和ㅗ是一对极易混淆的元音,非专业人工标注在词条达到数千条时一定会出现前后不一致。解决:不要手写罗马音,程序化转写并按文化观光部2000年罗马字表记法执行一遍全量转换;转换完成后只针对高频词做人工抽检,重点看ㅓ/ㅗ、ㅡ/ㅜ两对元音以及终声ㄱ/ㄷ/ㅂ的表现。即便这样,也要理解罗马音只是发音的近似映射,连音、紧音化等真实语流音变它并承载不了,这一点要在文档里对使用者说明白。

5.2 Excel打开CSV乱码:BOM的三字节教训

现象:words.csv导出后在Windows上双击打开,中文释义全是乱码,韩文直接变成问号。原因:CSV以UTF-8无BOM写入,Windows默认按本地代码页解读,两者不匹配导致解码错误。这个问题的表现很稳定,谁碰谁乱。解决:写CSV时固定用encoding="utf-8-sig",让写出的文件以EF BB BF这三个字节开头,Excel识别到UTF-8的BOM就会自动用正确编码打开。再加一个newline="",防止写出多余空行。这两处参数已固化在第3.3的导出函数里,之后我再也没有在词库项目里见过一次Excel乱码回归。

5.3 汉字词占比六成,不能按外来词处理

现象:整理词性分布时发现名词占比异常高,仔细查下来大量词目都对应汉字词(学校、学生、图书馆、食堂这类),我一度准备把它们单拎出来按外来词建表,结果把结构折腾得很乱。原因:韩语词汇中汉字词占比超过一半,它们本来就是韩语正常词汇的一部分,与从英语借入的词不是一回事。汉字词是韩语词表的顶梁柱,不是旁支。解决:汉字词和固有词同表共存,不需要单建表;但可以在词条上加一个可选的origin字段,值为"hanja"或"native",供做溯源学习和联想记忆的进阶场景使用。默认查询不区分,只在需要时按这个字段filter。加了origin字段之后,词库的前向兼容能力明显增强,后续有人想按词源组织课程时直接就有数据可用。

5.4 -하다派生动词的分合

现象:词库里“공부”(名词,学习/功课)和“공부하다”(动词,学习)并存,做关联推荐时发现两个词在例句中分布高度重叠,去重与排序逻辑一度混乱。原因:名词+하다是韩语最高频的动词构造方式,语义等价但语法词性不同。分则重复,合则词性缺失,怎么都不是。解决:最终方案是两个词条都保留,词性分别标noun和verb,verb词条增加一个related字段链接到对应的名词词元。搜索“공부”时借助related链连带展示“공부하다”,两个词条在UI里是一组,在数据结构里仍是两条。这种“数据严格分开、展示层聚合”的做法延续到了其他几十个-하다词上,结构清爽,没有再反复。

{ "word": "공부하다", "stem": "공부하", "romanized": "gongbuhada", "pos": "verb", "meaning": "学习", "related": ["공부"] }

5.5 官方词表的授权边界

现象:某次为模拟项目X组装词库时,我直接整合了一份某官方机构发布的词表做成随附数据包,还没到发布环节就被合作方提醒可能涉及再分发限制。原因:单纯看,单词和释义属于事实信息,但词表经过选择、排序、分级、附注的整理之后,整体可以构成有独创性的汇编作品,法定授权边界并不宽松。原因背后还有一层:词表里隐含了大量人工劳动,把它当公共领域数据处理既不符合规则也不公平。解决:个人学习自用没有任何问题;一旦做成工具分发或商业化,改用开放授权的语料与开源词表作为数据来源,自行统计高频词并人工分级,让原始来源干干净净。开源生态里确实有可用的韩语语料,只是要按授权条款逐条核实再使用。

6. 导出Anki牌组:词库真正进到学习闭环的最后一公里

词库整理得再干净,不流到学习流程里就是死数据。Anki是自建词库最通用的出口,而导出Anki牌组的核心是理解导入格式的约定:制表符分隔、UTF-8编码、第一行可以写字段名。常见的做法是生成一个“单词、发音、释义、等级”四字段的TSV文件。

def export_anki_tsv(entries, path="korean_deck.txt", with_example=False): lines = [] for e in entries: if with_example and e.get("examples"): example = e["examples"][0]["sentence"] fields = [e["word"], e.get("romanized", ""), e["meaning"], e.get("level", ""), example] else: fields = [e["word"], e.get("romanized", ""), e["meaning"], e.get("level", "")] lines.append("\t".join(fields)) with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines))

导入Anki后,用{{单词}}和{{发音}}
{{释义}}
{{等级}}做卡片模板,五字段版本还能多一个{{例句}}位置。这里有一点值得专门说:对韩语这种黏着语,带例句的输入型卡片显著优于纯单词卡。学习者看到整句后才点击翻面看到释义,能同时吸收单词在真实句子里的接续形态,而这一点恰恰是韩语学习中最容易卡住的地方。

验证Anki导出是否成功有一个基本功——打开牌组浏览界面,按等级排序,确认“topik1”字段没有变成乱码、例句里的谚文完整显示,就说明TSV编码和字段映射都对了。如果导出后卡面空白,多半是字段名与模板变量名没对上,检查{{单词}}是否拼写一致即可。

我现在的习惯是,每次构建完词库先在临时目录里跑一个冒烟测试:随机取50条词,检查谚文是否都被组合成完整音节、词性字段分布是否合理、CSV用表格软件打开是否正常、例句里有没有未翻译的中文残留。这一套十分钟可以完成,却能在发布前挡住绝大多数低级回归。这个词库方案值得做,也适合从你手头现有的词表开始逐步迁移过去,它真正值钱的部分不在词条本身,而在于把词库变成一套可重复构建、可持续维护的流程。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询