简介:兰卡斯特汉语语料库(LCMC)是一份约100万词次的现代汉语书面语通用型平衡语料库资源,面向语言学研究者、语料库方法学习者以及自然语言处理从业者,可用于英汉时体对比研究、汉语词类标注观察、语体分布分析与语料检索实践,对需要免费公开语料库进行课题设计或论文写作的读者尤其适用。压缩包共71个文件,主体为31个xml语料文本和39个htm说明/索引页面,另含1份doc格式的LCMC数据手册,便于查看语料结构、标注规范与使用说明;资源包仅6.34MB,轻量易用。现有3466人下载学习,适合以此为起点开展汉语平衡语料库相关训练。文件按语料类别组织,xml中保留原始文本与标记信息,htm页面可快速浏览语料分布,doc手册则为使用者提供了字段与格式指引,能明显降低上手成本。
1. 百万字级现代汉语平衡语料库:LCMC 能做什么
做中文词频统计或文体对比时,最尴尬的不是算法写不出来,而是拿不出让人信服的语料。临时从网上抓几百篇文章,来源杂、篇幅不均、文体混在一起,结论很难站得住脚。LCMC 正是一份能直接拿来用的现代汉语平衡语料库,总量约一百万汉字,覆盖新闻、社科、公文、小说等十五类文体,按固定配额取样,多数发行版还自带分词与词性标注。对语言研究者、对外汉语教师、NLP 初学者来说,拿到它等于把最耗时的建库环节直接跳过。它解决的核心问题是:给结论配数据、给课程配例句、给模型配干净的训练文本。
2. 库的底细:从解压到摸清文件构成
2.1 解压之后先认目录:文件命名里藏着文体分类
我拿到这类语料库时有个习惯,先不急着写检索脚本,而是把压缩包解压后完整看一遍目录。LCMC 的文件结构接近经典语料库的惯例:文体用大写字母表达,A 到 P 按顺序排(中间跳过 O),每类下放几十个纯文本文件,文件名像 A01.txt、B05.txt 这样,第一个字母就是文体代码。很多二次发布的压缩包还会套一层子目录,不先认目录,后面统计文体分布时容易漏掉一半文件。
第一步不是打开文本,而是用命令行盘一下整体:
unzip LCMC_release.zip -d lcmc find lcmc -type f -name "*.txt" | wc -l find lcmc -maxdepth 2 -type d -print第一行把下载的压缩包解压到 lcmc 目录,建议用纯英文路径,省得后续 Python 脚本在 Windows 上遇到中文路径编码问题。第二行统计解压后所有 txt 文件数量,如果结果跟发布说明里的篇目数对不上,多半是压缩包没解完整,或者里面还藏着二次压缩的子包。第三行只列两层目录,确认每个文体目录存在,目录名是字母还是中文不重要,文件名规则才是关键。
为什么文件名这么重要?因为后续按文体做对比统计时,程序都靠文件名首字母判断类型,而不是读目录名。我一般还会顺手查一下文件大小的分布,把异常小的文件挑出来:
find lcmc -name "*.txt" -exec ls -l {} \; | awk '{print $5, $NF}' | sort -n | head -20这一行把每个文件按字节数从小到大排序,只显示最小的 20 个。正常一篇 LCMC 样例文本大约两千汉字左右,对应文件体积应该在几 KB 以上。如果看到 0 字节或几十字节的文件,那基本都是空文件或下载损坏,要尽早从分析清单里剔除,不然统计词频时会多出一堆 NaN 或空行问题。
2.2 十五类文体配比:先看清样本是否均衡
LCMC 的设计参考了经典语料库的取样框架,十五类文体各有配额。整理一份对应关系很有必要,后面做文体对比时,直接按这个映射给代码加注释。
| 文体代码 | 类型 | 文体代码 | 类型 |
|---|---|---|---|
| A | 新闻报道 | J | 报告与公文 |
| B | 新闻评论 | K | 一般小说 |
| C | 新闻特写 | L | 武侠小说 |
| D | 宗教文献 | M | 侦探小说 |
| E | 商业与贸易 | N | 科幻小说 |
| F | 社科学术文章 | P | 爱情小说 |
| G | 自然科学文章 | ||
| H | 通俗读物 | ||
| I | 传记与杂文 |
为什么我要先列这张表?因为很多初学者想当然地认为语料库只是“一堆中文文本”,直接对着全库统计词频,完全不区分文体。可新闻和小说的高频词完全不同,混在一起统计会掩盖掉最有价值的文体差异。而 LCMC 这类平衡语料库的价值,恰恰在于它的配比经过设计,可以直接支撑文体间的横向比较。
2.3 Python 全库体检:文件数、总字数与分布统计
目录确认无误后,我习惯跑一个 Python 脚本,把整库的数据量摸一遍。这个脚本输出的结果,就是后续所有分析的底表。
# -*- coding: utf-8 -*- import os import glob corpus_dir = "lcmc" category_map = { "A": "新闻报道", "B": "新闻评论", "C": "新闻特写", "D": "宗教文献", "E": "商业贸易", "F": "社科论文", "G": "自然科学", "H": "通俗读物", "I": "传记杂文", "J": "报告公文", "K": "一般小说", "L": "武侠小说", "M": "侦探小说", "N": "科幻小说", "P": "爱情小说" } summary = {} total_chars = 0 for path in glob.glob(os.path.join(corpus_dir, "**", "*.txt"), recursive=True): fname = os.path.basename(path).upper() cat = fname[0] with open(path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() char_count = len(content) summary.setdefault(cat, {"files": 0, "chars": 0}) summary[cat]["files"] += 1 summary[cat]["chars"] += char_count total_chars += char_count for cat, stat in sorted(summary.items()): print(cat, category_map.get(cat, "未知"), stat["files"], stat["chars"]) print("TOTAL", total_chars)glob.glob(..., recursive=True)会递归捕获所有子目录下的 txt 文件,避免二层目录丢失;os.path.basename取出文件名后,取第一个字符作文体代码;setdefault则为每个文体初始化计数器,省去手动判断字典键是否存在的麻烦。errors="ignore"是先忽略个别解码失败的字符,保证统计口径统一,编码问题后面专门处理。
跑完这个脚本,如果发现某个文体只有二十几篇,而其他文体四十篇,说明压缩包不完整或下载过程丢文件,就要回源头重新获取。我见过有人拿着缺失版的语料库做了半个月词频分析,最后发现少了一整个文体,所有对比结论都作废,那感觉确实不好受。
提示:统计结果别只看终端输出,建议重定向保存一份,例如在命令尾部加
> corpus_report.tsv,方便后面核对或写进论文附录。
3. 编码与标注格式:动手分析前先清洗
3.1 编码识别:为什么文件打开全是乱码
这类学术语料库发布时对编码的态度比较随意。有的压缩包按 UTF-8 存储,有的在 Windows 里被重新保存过,变成 GB18030;还有少数文件头部残留 UTF-8 BOM。在编辑器里打开一切正常,换到 Python 的open()就抛UnicodeDecodeError,这就是编码不统一最直观的信号。
我拿到文本后第一步永远是全库编码检测,而不是直接按 UTF-8 硬读。
# -*- coding: utf-8 -*- import glob def detect_encoding(path): with open(path, "rb") as f: raw = f.read(4096) if raw.startswith(b"\xef\xbb\xbf"): return "utf-8-sig" if raw.startswith(b"\xff\xfe"): return "utf-16" try: raw.decode("utf-8") return "utf-8" except UnicodeDecodeError: try: raw.decode("gb18030") return "gb18030" except UnicodeDecodeError: return "unknown" files = glob.glob("lcmc/**/*.txt", recursive=True) enc_counts = {} for path in files: enc = detect_encoding(path) enc_counts[enc] = enc_counts.get(enc, 0) + 1 print(enc_counts)函数先读文件前 4096 字节,按 BOM 头快速判断 UTF-8-SIG 或 UTF-16,再依次尝试 UTF-8 和 GB18030。GB18030 是中文 Windows 上最常见的本地编码,兼容 GBK,连生僻字都能覆盖,作为兜底方案非常可靠。返回unknown的文本基本可以认定损坏,直接加入黑名单。
为什么只读前 4096 字节?因为编码检测不需要读完整文件,这一段足以区分绝大多数情况。有一种例外:文件开头是大段英文说明,中间才出现中文,此时前半段 ASCII 能用 UTF-8 解、后半段却需要 GB18030,这种情况不常见,真遇到了不要自己修,重新找完整版本拷贝更省事。
3.2 标注格式识别:分词标注版和纯文本版用法完全不同
LCMC 的流通版本大致有两种形态。一种是纯汉语文本,适合检索和阅读;另一种是带词性标注的版本,适合语法研究。带标注的行看起来像“我们/r 的/u 语料库/n”,也就是词、分隔符、标注码连在一起。但不同整理者的分隔符不一样,有人用斜杠,有人用 Tab 键,还有人用双斜杠。
写解析脚本前,先抽几个文件看格式,比直接套正则靠谱:
import glob samples = glob.glob("lcmc/**/*.txt", recursive=True)[:5] for p in samples: with open(p, "r", encoding="utf-8", errors="ignore") as f: lines = [l for l in f.read().splitlines() if l.strip()][:3] print("---", p) for ln in lines: print(ln[:120])这段代码只读取前五个文件,每个文件显示前三行非空内容,每行前 120 个字符。输出里如果频繁出现“/v”“/n”这类后缀,说明文件是词性标注版;如果句子干净通顺,则是纯文本版。还有一种较老的格式用 Tab 分隔词和标注,看起来是“今天\t时间词”,这种后续解析要单独兼容。
为什么先区分版本?因为词频统计的技术路线完全不同。标注版可以直接按词和标注切开统计,得到的是基于原分词结果的词频;纯文本版则需要引入切词工具重新切分,两条路线清洗逻辑不一样,混在一起处理,等统计词性时才发现格式不统一,返工成本非常高。
提示:压缩包里通常有 readme 或说明文件,里面会写明标注体系,先读 readme 永远比靠自己猜格式更快。
3.3 清洗规则:去掉页眉、页码和标点噪音
语料库数字化时通常保留原始出版物的页眉页脚、页码和版权行。这些内容出现在检索结果里非常碍眼,比如查“的”字句时,会带出“第 3 页”“资料来源:某某报”这类不完整片段。清洗阶段要把它们过滤掉,之后再进入检索或统计流程。
# -*- coding: utf-8 -*- import re def clean_corpus_text(text): # 去除页眉页脚常见模式 lines = text.splitlines() lines = [ln.strip() for ln in lines if ln.strip()] lines = [ln for ln in lines if not re.search(r"第\s*\d+\s*页", ln)] lines = [ln for ln in lines if not re.match(r"^[A-Za-z\s]{20,}$", ln)] # 压缩空白与统一换行 body = "\n".join(lines) body = re.sub(r"[ \t]+", "", body) return body.strip()re.search(r"第\s*\d+\s*页", ln)用于匹配“第 3 页”“第十二页”这类页码行;re.match(r"^[A-Za-z\s]{20,}$", ln)过滤掉整行超过 20 个英文字母的纯英文行,因为 LCMC 的样例文本头尾常混入来源英文说明。最后把行内所有空格和 Tab 压掉,统一成紧凑文本。
这一步的参数需要按实际语料微调。比如“第X页”的匹配规则对“第 12 页/共 40 页”这种格式有效,遇到“- 12 -”这种页码格式就得改用r"[-—\s]*\d+\s*$"去匹配行尾数字。所以我一般会把清洗函数写成可以传入正则的版本,而不是写死规则,方便不同语料版本共用。
4. 检索与词频统计:把语料库变成分析工具
4.1 关键词检索与例句导出:按文体提取语料证据
语料库最常用的功能是提取例句。比如想研究“把”字句在新闻和小说中的分布差异,需要把所有包含“把”的句子按文体归类。直接翻文本文件很低效,我一般写一个检索函数,把结果输出成 CSV,方便在表格软件里筛选。
# -*- coding: utf-8 -*- import os, glob, re, csv corpus_dir = "lcmc" target = "把" def clean_sentence(text): text = re.sub(r"\s+", "", text) return text def search_corpus(corpus_dir, target, limit_per_cat=50): out = [] for path in glob.glob(os.path.join(corpus_dir, "**", "*.txt"), recursive=True): fname = os.path.basename(path).upper() cat = fname[0] with open(path, "r", encoding="utf-8", errors="ignore") as f: content = clean_sentence(f.read()) for sent in re.split(r"[。!?!?]", content): if target in sent: out.append([cat, fname, sent]) return out rows = search_corpus(corpus_dir, target) with open("search_ba.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["文体", "文件名", "例句"]) writer.writerows(rows) print("total:", len(rows))clean_sentence先把所有空白字符压掉,避免句子因为换行被拦腰截断。re.split(r"[。!?!?]", content)按中文句末标点切句,英文问号感叹号也一并处理,兼容混排文本。输出用utf-8-sig编码,是为了让 CSV 在 Windows 的表格软件里直接打开不乱码,这个小细节很多人容易忽略。
limit_per_cat参数控制每类最多收集多少条,防止输出文件过大。检索词“把”只是示例,实际使用时可以换成任意目标词或短语。如果想做更精确的结构检索,比如“把+N+V”句式,就需要在切句后加一层正则匹配,那已经属于另一套语法匹配方案。
4.2 高频词统计:纯文本版的分词与停用词设置
词频统计是语料库的看家本领。拿到标注版时,可以直接利用现成的分词结果,把词和标注切开后统计;拿到纯文本版时,则需要用jieba这类工具补做切分。下面的示例按纯文本版处理,这样代码换到其他中文语料也能复用一个流程。
# -*- coding: utf-8 -*- import glob, os, collections import jieba # 最小停用词集,实际使用时建议用独立停用词表文件 stopwords = set("的了是在有和就不人都一这上中很也我你他她它们说为与及被中".strip()) def tokenize_files(corpus_dir): counter = collections.Counter() for path in glob.glob(os.path.join(corpus_dir, "**", "*.txt"), recursive=True): with open(path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() words = jieba.cut(content) for w in words: w = w.strip() if len(w) == 1: continue if w in stopwords: continue counter[w] += 1 return counter counter = tokenize_files("lcmc") for w, c in counter.most_common(30): print(w, c)代码的关键点是stopwords。中文高频词一大半是“的、了、是、在”这类功能词,不提前过滤,“的”会毫无悬念地排在第一位,分析价值几乎为零。len(w) == 1筛掉单字词,理由很多单字在汉语中要么是虚词素、要么语义不独立,先拿掉能减少低频噪声。实际项目中停用词量至少几百个,建议单独存成文本文件,不要每次在代码里维护。
jieba.cut(content)默认使用精确模式,适合词频统计场景。如果语料库本身已经分词,就不要再用jieba二次切分,否则分词结果可能与原始标注不一致,引入额外误差。
4.3 文体对比:用词频差异识别新闻和小说的用语特征
词频统计跑完,下一步通常是比较文体差异。最直接的做法是分别统计两个文体的高频词,看哪些词的频次差异最大。
# -*- coding: utf-8 -*- import os, glob, re, collections def freq_for_category(root, cat): counter = collections.Counter() for path in glob.glob(os.path.join(root, "**", "*.txt"), recursive=True): fname = os.path.basename(path).upper() if fname[0] != cat: continue with open(path, "r", encoding="utf-8", errors="ignore") as f: text = f.read() words = re.split(r"[\s/]+", text) for w in words: if len(w) <= 1 or w in stopwords: continue counter[w] += 1 return counter A = freq_for_category("lcmc", "A") K = freq_for_category("lcmc", "K") common = set([w for w, _ in A.most_common(100)]) & set([w for w, _ in K.most_common(100)]) for w in list(common)[:20]: print(w, A[w], K[w])这个示例假设文本已是分词状态,所以用re.split(r"[\s/]+", text)按空白或斜杠切词。A对应新闻报道,K对应一般小说,两边各取前 100 高频词后求交集,再逐个打印在两个类别里的原始频次。如果某个词在 A 类里出现 300 次、在 K 类里只有 20 次,就是明显的文体标记词。
更严格的对比应该换算成标准频次,比如每十万词出现多少次,以消除两边语料量差异的影响。原始频次只能看相对关系,要写进论文或报告,建议加一步归一化处理。这一步也可以换成计算对数似然比或互信息,但原理都是从相对频率差异里找出特征词。
5. 避坑:LCMC 使用中绕不开的五个典型坑
下面五条都是实际使用这类语料库时容易翻车的地方,按现象、原因、解决三步记录。
5.1 文件打开全是乱码
现象:用 Python 读某个文本,打印出来全是“锟斤拷”或者一大堆替换字符,但用系统记事本打开却显示正常。
原因:编码不统一。老版本在大陆 Windows 环境下保存成了 GB18030,而脚本默认按 UTF-8 解码,自然出错;也有少数文件头部被加了 UTF-8 BOM,干扰解码判断。
解决:按 3.1 节的检测流程先跑一遍全库,按检测结果分类。对已经乱码的文件,写一个自动转换脚本统一转成 UTF-8 另存到新目录,原文件保留不动,等文件数量核对一致后再清理旧包。不要手工逐个文件修复,既慢又容易改坏。
5.2 不同文件的标注格式不一致
现象:同一个压缩包里,A 类文本的标注符号是“词/标注”,K 类文本却是“词\t标注”,少数文件甚至完全没有标注。
原因:语料库经过多轮整理和网络转载,不同时期不同来源的版本格式发生漂移。发布者往往只保证内容完整,不保证格式统一。
解决:先按 3.2 节的抽样式检查做格式普查,统计出全库到底有几种格式,再写一个归一化函数。常见做法是把斜杠分隔和 Tab 分隔统一成同一种分隔符,我再把没有标注的文件单独拎出来,要么重新补标注,要么在统计时排除并写明数量。千万别默认全库格式一致。
5.3 检索结果混入页眉页脚和来源说明
现象:检索“把”字句时,导出的例句里频繁出现“第 1 页”“资料来源:某某编辑部”这类不完整片段。
原因:语料数字化时保留了原始出版物的页眉页脚、页码和版权行,切句逻辑无法区分正文和附属信息,把它们当成普通句子切开了。
解决:在检索前先用 3.3 节的清洗函数剥掉文件头尾与页码行。我习惯把清洗函数接到search_corpus的读取环节里,保证后续所有查询拿到的都是干净文本。如果清洗后仍有个别噪声,就再加一条正则把“资料来源”“责任编辑”这类特征词过滤掉。
5.4 繁简混杂影响词频统计
现象:统计“计算机”时,词频表里同时出现“计算机”和“計算機”,两个词都排在中等频次,合并前看不出是同一个词。
原因:语料来源横跨不同年代的出版物,早期文本可能是繁体转简时不彻底,个别字符保留繁体,发行方一般不做全局繁简归一。
解决:统计前做一次繁简转换。我一般用 opencc 这类库的t2s配置把繁体转简体,不要用简单字符串替换,因为“發”“髮”“发”这类字有上下文歧义,工具库会按语境处理。转换后重新跑词频,高频词表中的重复条目会明显减少。
提示:LCMC 主要使用简体中文,繁简混杂通常只出现在个别文本里,按文件处理即可,不用全局重转。
5.5 Windows 下二次保存导致文件损坏
现象:在 Windows 系统里用记事本打开某个语料文本,顺手按了快捷键保存。之后脚本读取该文件时固定在某一行中断,或者输出一堆替换字符。
原因:记事本默认保存编码可能与原文件不一致,加了 BOM 或直接改了编码。更隐蔽的是,原文件是 LF 换行符,保存后变成 CRLF,切句正则里的换行假设失效。
解决:处理语料期间,把语料目录设为只读,所有中间结果输出到另一个目录。必须编辑原文件时,用能显示编码的编辑器打开,保存时显式选择 UTF-8 无 BOM,并保持原有换行符风格不变。这条坑很隐蔽,一旦中招往往要重下原始文件。
6. 进阶:把常用检索固化成一个 SQLite 小工具
如果只是临时查一两个词,每次写脚本检索没问题。但真正做研究或备课,检索是高频动作,今天查“把”,明天查“被”,后天统计“了”和“着”的分布,频繁重读几百个文本文件效率太低。我习惯把 LCMC 预处理成一份 SQLite 数据库,以后查例句、统计词频都不再碰原始文件,随取随用。
# -*- coding: utf-8 -*- import sqlite3, glob, os db = sqlite3.connect("lcmc.db") db.execute("DROP TABLE IF EXISTS docs") db.execute("CREATE TABLE docs (id TEXT PRIMARY KEY, cat TEXT, content TEXT)") for path in glob.glob("lcmc/**/*.txt", recursive=True): fname = os.path.basename(path).upper() cat = fname[0] with open(path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() db.execute("INSERT OR REPLACE INTO docs VALUES (?,?,?)", (fname, cat, content)) db.commit() print("done")docs表只有三个字段:文件名、文体代码、全文。INSERT OR REPLACE的作用是导入遇到重名文件时自动覆盖,适合语料版本更新后重新导入。数据库文件生成后只有几十 MB,后续操作不再反复读原始目录。
导入完成后,按文体查关键词,一条 SQL 就能解决:
sqlite3 lcmc.db "SELECT cat, id, substr(content, instr(content, '把'), 30) FROM docs WHERE content LIKE '%把%' LIMIT 20;"instr(content, '把')找到关键词在文本中的位置,substr截取前后共 30 个字符,返回一段可读的上下文,而不是整篇文本。LIKE '%把%'在百万字规模上扫描速度对个人研究足够快。想查“被”字句,直接把两个“把”换成“被”就能复用。
从那以后,每次接到语料处理需求,我都强制走一遍固定流程:先看文件数与编码,再抽查标注格式,清洗后抽三个文件做人工复核,最后导入 SQLite 库。这套流程最多花二十分钟,但帮我挡掉了后面至少一周的返工。整理语料库这件事,耐心比技术更值钱,希望帮到你。
本文还有配套的精品资源,点击获取