先说明一点:很多做 NLP 的朋友经常把目光聚焦在怎么把模型效果调好,却容易低估底层语料库建设的工作量。图像领域有 VOC、YOLO、COCO 这类相对规范的数据集可以使用,文本语料库却往往更麻烦:语言杂、来源杂、时间跨度和版权情况也杂。我最近在整理面向“全球各国对华主题”的语料数据库(2003-2023 年),踩了不少坑,也沉淀出了一套比较稳定的流程。这篇文章会把整个从立项、选料到清洗上线的思路完整写出来,适合正在做语料库、数据集清洗、多语言 NLP 项目的朋友参考,也能给做跨语言内容分析的同学一些数据侧的启发。
1. 语料库项目为什么这样设计
1.1 项目到底要解决什么问题
刚开始接到“全球各国对华语料数据库”这个需求时,很多人第一反应是去搜新闻、抓网页。但真正做起来会发现,这件事难点不是“没有数据”,而是“数据太乱、太杂、太不可控”。
我们要建的数据集,本质上是一个跨国家、跨语言、跨时间(2003-2023 年)的文本集合,文本内容以不同国家媒体与公开渠道围绕中国相关话题产出的报道、评论、说明性文本为主。它的价值主要体现在两块:第一,为研究者提供一个可定位、可过滤、可统计的“多语种中国相关话语”文本池;第二,为 NLP 模型提供足够丰富的真实语料,支撑文本分类、关键词抽取、主题演化、跨语言对齐等任务。
如果只把它理解成“抓一批文章”就大错特错了。一个合格的语料数据库必须回答清楚几个问题:每条文本到底属于哪个国家、什么语言、什么时候发布、什么主题类别、原文链接在哪里、文本是否完整、有没有被重复采集、版权是否允许二次分发。这些问题才是项目的骨架。
我在项目初期先把用户场景列了一遍,发现不同人对“语料”的期待差别很大。有人想做关键词历时统计,有人想训练本地化的多语种分类模型,还有人只是想快速检索到某个时间段、某个地区对特定事件的表达方式。需求不统一,数据设计就不能太随意。最后我决定采用“原始文本 + 丰富元数据”的双层结构:正文尽量完整,但每条记录配套字段必须足够规范,让下游能按自己的方式切片。
1.2 为什么时间跨度定在 2003-2023 年
2003-2023 这个时间窗不是随便拍的。对于大多数目标媒体,2003 年之前的内容要么没有完整的线上存档,要么数字化质量很差,很多报纸的早期网页版在结构上非常混乱,解析成本会急剧上升。2023 年作为截止点,则是因为数据整理需要一段“冷却期”,太靠近当前时间的内容,在确权和归档上容易有滞后。
从研究视角看,二十年跨度也是一个比较理想的中长周期窗口。它可以覆盖文本风格的变化、媒体形态的演变以及多个技术阶段。比如早期文本偏正式的深度报道,后来开始出现大量短平快的网络新闻,再往后又加入了社交媒体来源的转述和科普类整理。这些变化本身就是语料库的“研究价值点”,绝不是简单地将它们清洗掉。
我在筛选时也刻意保留了发布时间字段的精度,哪怕原始页面只提供到月份级别,也会尽量解析出来。原因很直接:时间维度是做历史趋势分析的基础,哪怕暂时用不到,也不能在数据阶段就把它丢掉。
1.3 数据集的边界条件
全球各国“对华”语料,很多人会误以为要把两百多个国家和地区的文本都做到完全平均。实际操作中一般不这样处理,也没有必要。语料覆盖要考虑的是代表性,而不是机械的国别平均。
我采用的做法是:先按语种覆盖主要地区,再按媒介重要性补充特定国家来源。比如英文语料覆盖全球多个区域,同时补充法文、西文、阿文、俄文、日文、韩文、德文等多语种来源,以扩大观察视角。筛选文本时,只要正文围绕中国相关的文化、经济、科技、旅游、社会等公共话题展开即可,不对具体历史语境或微观事件作立场性判断。
这一点非常重要。数据集的建设者最好不要替使用者做观念判断,否则会过早地把分析角度焊死。正确做法是尽量客观地把“什么时间、什么地方、哪个媒体、谈了什么主题、原文结构如何”记录下来,至于怎么解读,交给研究者或模型去完成。
2. 数据源选择与版权合规
2.1 优先吃透公开RSS和开放授权资源
数据源是整个项目的生命线。我在早期就明确了一条原则:优先选择公开提供 RSS、Atom 或具备明确开放授权的内容源,而不是用到处爬取的方式搞“一锤子买卖”。
RSS 的好处不只是采集方便,更重要的是它对采集方友好,站点通过 RSS 主动暴露内容更新,自带标题、发布时间、摘要和链接等结构化信息,非常省事。尤其是很多国际媒体和内容平台,至今仍保留着完整稳定的 RSS 输出,完全可以作为长期增量更新的主通道。
除了 RSS,我还会重点吸收那些使用知识共享许可的文本资源,这类内容一般在页面底部或版权声明中明确写了允许非商业转载、署名引用等条款,对构建研究型数据集非常合适。另有一类可选来源是国际机构、高校研究中心公开发布的研究简报与说明性文档,它们通常语言规范、结构完整、归属清晰,作为补充语料也很合适。
2.2 早期内容怎么补
2003-2013 年之间的内容,很多源站已经改版,原链接经常失效。这时候单纯靠 RSS 肯定不行,因为 RSS 只给“现在”,不给“历史”。
我的处理方案是引入公共网页存档。网络存档服务保留了历史网页快照,可以按 URL 和时间点回溯。早期数据的采集思路其实是反推:先用语义关键词和时间过滤去检索语料候选,找出候选页面的原始 URL,然后从存档中获取指定时间快照,再走常规的正文抽取流程。
这里有一个值得强调的经验:不要等到下数据的时候才去找历史链接,应该在项目早期就维护一张“种子 URL 表”。这张表记录哪些媒体值得收、各自首页栏目结构大致什么样、历史上的内容 URL 规律是什么。一个很常见的规律是很多新闻站点的 URL 里直接包含年月日甚至文章 ID,只要你抓到一个旧的入口页,就能顺藤摸瓜把一批历史文章都拉出来。
当然,早期网页的编码、排版都非常混乱,很多已经不是你今天看到的 HTML 结构。所以补早年数据时,时间成本要按新数据的 3-5 倍预估。
2.3 版权红线和二次分发策略
版权问题是在项目起步阶段就要明确的问题。我的基本判断是:语料库可以在内部研究中使用,但对外发布时不能一股脑地把全文全部公开。
项目最终采用的分发策略是“三层设计”:
- 元数据层:标题、时间、来源、语言、国别、主题分类、原文链接等,可以公开分享;
- 摘要层:对文本做适当长度的自动摘要,或提取关键短语,方便研究者快速判断文本是否相关;
- 全文层:仅保留在本地,对于有明确开放授权的来源提供全文文件,其他情况仅给链接和获取方式。
这样做既照顾到了研究需求,也降低了版权风险。很多单篇内容单独看不长,但汇集到一定规模后,构成对原作品的实质性替代风险会明显上升,所以不能抱侥幸心理。
3. 语料处理的完整落地方案
3.1 采集端:调度策略和多源管理
我先设计了统一的采集框架,而不是每个数据源写一套单独脚本。框架需要处理三类事情:抓取动作、正文抽取、状态记录。
抓取动作最简单的实现是维护一个任务队列。每个任务对应一个订阅源或一个页面列表,包含基础信息和抓取时间。调度器按一定频率检查任务,决定是否发起请求。为了不给目标站点造成压力,同一域名下的请求间隔至少控制在 3-5 秒,并且设置超时重试机制。
下面是一个简化版的思路:
import feedparser from dataclasses import dataclass @dataclass class FeedTask: name: str url: str country_code: str language: str topic_category: str last_fetched: str def fetch_feed(task: FeedTask): # 解析订阅源 parsed = feedparser.parse(task.url) for entry in parsed.entries: raw_item = { "source_name": task.name, "country_code": task.country_code, "language_code": task.language, "category": task.topic_category, "title": entry.get("title", "").strip(), "link": entry.get("link", "").strip(), "published_at": normalize_pub_time(entry.get("published", "")), "summary": clean_html(entry.get("summary", "")), } # 该 URL 是否已入库 if not is_duplicate_url(raw_item["link"]): insert_to_queue(raw_item)注意发布时间的解析不能直接依赖 feedparser 返回的字符串,不同站点格式差异很大。真正入库前要统一转换为 ISO 8601 标准格式,并带上时区信息,否则后续做时间过滤会非常痛苦。
正文抽取环节,我强烈推荐 trafilatura 这类专门针对新闻网页的内容抽取库。它和通用 BeautifulSoup 方案最大的不同是,trafilatura 自带很多“正文识别”逻辑,会自动剔除导航、页脚、广告和推荐位,对多语言网页也有不错的兼容性。
import trafilatura def extract_article(url: str) -> tuple[str, str]: downloaded = trafilatura.fetch_url(url) if not downloaded: return "", "" # 提取正文,保留段落结构,不保留超链接 text = trafilatura.extract(downloaded, include_links=False, include_images=False) # 同时拿到页面元信息 meta = trafilatura.extract_metadata(downloaded) return text, meta这里有个隐藏坑:trafilatura 对短文本和列表页的识别会出现空白结果。遇到这类情况不要直接丢弃,先检查原文 HTML,有些媒体文章内容被放在 JSON-LD 脚本或特殊节点里,需要针对源站结构做少量定制。
3.2 文本清洗:多语言环境的特殊处理
清洗这一步我把它拆成了五轮:
第一轮是 HTML 实体与标签清理。即使是 trafilatura 抽取过的文本,也可能残留少量字符实体,比如 、&等,需要统一还原。
第二轮是编码标准化。多语言语料最大的噩梦是编码不一致。不同时代、不同技术栈的网页可能分别用了 UTF-8、ISO-8859-1、Windows-1252 甚至 GB2312。入库统一转成 UTF-8 是底线,没有商量余地。
第三轮是语种识别与标签确认。虽然 RSS 通道已经带了语言标签,但很多媒体是内容自动翻译后发布的,语言标签和正文实际语言会不一致,尤其是亚洲语言和欧洲小语种混排的时候。我使用语种识别模型对每个文本做二次确认,识别置信度较低的文本进入人工复核队列。
下面是语种识别的参考脚本:
import fasttext # 例如使用开源语言识别模型 lid.176.bin model = fasttext.load_model("lid.176.bin") def detect_language(text: str) -> tuple[str, float]: text = text.replace("\n", " ")[:1000] prediction = model.predict(text) lang_code = prediction[0][0].replace("__label__", "") confidence = prediction[1][0] return lang_code, confidence第四轮是敏感信息处理。如果是公开发布的数据集,建议写一个正则规则库,把邮箱、电话号码、个人身份证和账号类信息直接打码或移除。这个过程不要完全依赖正则,正则只处理规则明确的类型,真正的兜底要靠人工抽样检查。
第五轮是文本长度门槛过滤。有些页面虽然抓取成功,但正文只有一句话,说明它可能是列表页或链接跳转页,信息量很低。我设定的经验阈值是:纯文本内容少于 120 个字符且不包含可提取关键词时,直接标记为低价值,不进入正库。
3.3 去重:不能只比标题
语料库去重是最容易被低估的环节。英文媒体报道中国相关话题时,同一事件往往会被多家媒体互相转载,标题可能改了,但正文高度相似。如果只靠标题 hash 根本去不掉。
我用的是 SimHash + 滑动窗口组合方案。SimHash 适合处理长文本近似重复,先把正文分词、计算关键词权重,然后生成 64 位指纹,再用汉明距离判断相似度。阈值我调在距离小于等于 3 时视为重复候选,再对候选对做一次正文编辑距离或 Jaccard 相似度确认。
仅仅做全文相似度还不够,同一机构经常会在不同栏目发布内容,文本里只有导语不同,后面整段一样。处理思路是对前 50 个字符和正文最后 80 个字符分别做一次短文本去重,同时记录彼此之间的相似度。
重点提醒一下:去重后的数据不能简单地“删除一条”。正确做法是对重复组添加一个group_id,保留最早收录的一条作为主记录,其余标记为duplicate_of=主记录ID,这样后续扩容或需要保留多条转载渠道时,还能恢复原状。
3.4 元数据结构:从第一天就要设计好
很多非工程背景的人做数据集,一开始只管把文本堆在一起,后面要分析时才发现缺字段,再回补工作量巨大。好的语料库,元数据必须从第一天就设计清楚。
我设计的核心字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| doc_id | string | 全局唯一文档 ID,形如 CN-GB-20230817-xxx |
| country_code | string | 媒体所属国家/地区代码,ISO 3166-1 alpha-2 |
| media_name | string | 媒体名称或来源机构名称 |
| source_type | string | 来源类型:rss / archive / article_list |
| language_code | string | 正文主语言代码 |
| url | string | 原文链接 |
| url_archive | string | 存档链接,无则空 |
| title | string | 清洗后的标题 |
| content | text | 清洗后的正文 |
| summary | text | 自动摘要 |
| published_at | datetime | 统一到 UTC 时间的发布时间 |
| collected_at | datetime | 入库时间 |
| topic_category | string | 主题粗分类 |
| doc_length | integer | 正文字符数 |
| duplicate_group | string | 去重组 ID |
| license_note | string | 版权授权说明 |
doc_id 的生成规则建议有一定语义,不要用自增数字。比如CN-GB-20230817-0001能直接看出媒体所属国家、文章发布时间和序号,后面做排查时一眼就能定位来源,比纯数字 ID 高效很多。
3.5 质量评估:数据集的“体检报告”
语料库交付前一定要做一次全面体检。我从四个维度来做评估:
第一是覆盖度。统计国别分布、语种分布、时间分布和媒体分布,确认没有出现某个语种占比过高的偏斜。如果做的是多语种任务,各语种数量差异超过一个数量级就需要重点关注。
第二是纯净度。通过关键词搜索抽样检查,比如随机抽取 500 条文本,人工判断正文是否完整、是否与标题吻合、有没有明显乱码或机器翻译残留。
第三是时间一致性。重点检查发布时间字段有没有异常值,比如早于 2003 年或晚于采集日期,还包括同一数据源发布时间的时区是否切换正确。
第四是正文有效性。统计正文平均长度、首段长度分布和段落总数,如果某些来源的平均文本长度明显低于整体水平,通常意味着抽取规则有问题。
这些检查写成一个自动报告脚本,每次全量更新后跑一遍,发现异常项再定位修复。宁可让发布晚几天,也不能把一个带着大量脏数据的数据集交出去,语料库的信誉建立很难,毁掉却很容易。
4. 多语言文本处理的常见雷区
4.1 时间格式的“时区陷阱”
多语言文本来自全球各地媒体,发布时间格式五花八门。英文媒体一般用 RFC 822 格式,比如Wed, 16 Aug 2023 08:00:00 +0000,中文源站常见的是北京时间但含中英文月份,欧洲媒体还会有夏令时切换问题。
统一策略是全部转为 UTC 时间存储。我写了一个时间解析函数,按不同优先级尝试多种格式,解析失败时保留原始字符串并打上time_parse_failed标记。这个标记很重要,不要默默填充默认时间,否则后面做时间序列分析时会混入无意义的数据。
夏令时问题在多语种语料里尤其隐蔽。某些欧洲媒体夏季会把时间偏移写成+0200,冬季写成+0100。如果不做转换,同一个时间点在不同月份看起来会相差一个小时。好在这个问题只影响精确到小时的分析,普通按天聚合基本没影响。
4.2 乱码与混合文本
2003 年到 2015 年之间的网页文本,经常出现整段乱码,常见特征是特殊字符如é、’反复出现。这类问题大多是双重编码导致的,也就是原始内容被从 UTF-8 当作 Latin-1 再次编码。我自己写了一个乱码检测函数,通过统计无用字符在正文中的占比来判断,超过阈值就把文本单独抽出来做二次修复。
小语种的语言识别也有坑。语种识别模型通常同时支持几十上百种语言,但识别结果容易被标题或短文本干扰。建议识别时不要用整篇文章,而是从正文里取中段 500-1000 字,因为新闻正文前段可能包含英文人名、机构名,容易让模型误判。
4.3 字符标准化细节
有些语言存在多种 Unicode 编码方式,比如某些拉丁字母可以用“基础字母加组合变音符号”表示,也可以用预组合字符表示。在做关键词匹配时这两种方式长得一样但字节不同,会直接影响下游检索。
处理方式是用 Unicode NFKC 标准做归一化,把兼容字符替换成标准形式。同时把全角标点转为半角标点(东亚中日韩文场景下需要保留句读则单独处理),删除零宽空格和不可见控制字符。
这个步骤看起来不起眼,却经常能让很多下游任务的效果有肉眼可见的提升。
5. 语料库可以用在哪,怎么用更实用
5.1 文本分类和主题演化任务
建成后的语料库最直接的应用是做主题分类模型。由于语料跨年份覆盖了二十年的语言表达变化,直接用它做监督训练会面临分布漂移问题:2005 年的文本和 2020 年的文本在词汇分布上有明显差异。
我在实践中倾向于按照年份把数据切成年度子集,在每个子集上分别微调一个轻量级分类器,再观察不同年份模型在类别识别上的差异。这种做法不追求单一模型的极致精度,而是把模型当“观测工具”,用来帮研究者发现语料本身的内容变化。
5.2 关键词历时的信息挖掘
以原始语料为基础,做关键词历时统计是非常自然的研究路径,比如观察不同时间窗口内主题词的频次变化。这里要提醒一个很容易出错的地方:直接统计词频会被文本长度影响。正确做法是先按文档计算关键词出现率,再做年度聚合,防止某一年突然收入大量长文本导致频次虚高。
还可以对关键词做上下文共现分析,把每个关键词出现位置的前后各 5-10 个词提取出来,形成“关键词搭配片段”,这个结构非常有利于快速判断某个时期文本的关注角度,比单看词频有意义得多。
5.3 跨语言对齐与检索增强
由于语料中含有大量多语言同主题文本,可以尝试做跨语言段落对齐。思路是先用语义向量模型分别把各语言文本编码成向量,然后按主题分类和时间相近原则做召回,再用相似度阈值精排。对齐后的语料可作为机器翻译的辅助验证集,也可以作为百科类问答模型的外部知识来源。
在这个方向上,我发现检索增强生成模式特别适合这套语料库。使用者提出问题后,先从库里检索出相关文本片段,再由生成模型结合片段作答。好处是可以追溯答案来源,降低大模型可能出现的“一本正经胡说八道”现象。因为语料是带完整元数据的,检索结果还可以附带时间、来源和国家等信息,大大提升了可信度。
6. 项目维护与后续更新节奏
6.1 增量更新的频率设计
语料库不是一遍建完就结束的静态资源。如果目标是 2003-2023 年,实际上从 2024 年之后,新内容仍然会陆续出现,因此我预留了增量更新接口。
增量更新的频率取决于数据源特性。对新闻类源,每 6 小时检查一次 RSS 变更即可;对机构报告和学术出版物,每日更新一次足够。更新不是简单追加,每一轮增量都要跑完整条清洗链,否则脏数据会越积越多。
我维护了一个监控表,统计每个数据源最近一周的采集成功率、去重率和文本平均长度。如果某个源连续多次采集成功率为零,就触发移除或替换评估。如果大量文本的长度突然变短,大概率是站点改版导致抽取器失效,需要尽快人工介入。
6.2 数据集的版本管理
数据集同样需要版本管理,和代码项目一个道理。我在发布时采用vYYYY.MM的版本号,每个版本固定记录三件事:新增了多少条文本、哪些数据源被增加或下线、清洗规则做了哪些调整。
这一点容易被人忽略。很多人下载数据集之后做了一段时间分析,发现结果异常,但不知道是不是数据出现变化。版本说明文件可以极大减少下游使用者的排查成本。我在每个版本目录下方放了一个CHANGELOG.md,记录变更内容,还会把每次全量校验报告一并附上。
6.3 长期存档策略
语料库的存储介质需要异地多备份。我用的是“主存储 + 两个备份介质”的组合:一份放在支持检索的数据库里,一份做成压缩包放对象存储,另一份放在本地只读盘。不要以为云端就万无一失,历史数据一旦丢失,补采成本往往比初次建库还要高。
原始网页 HTML 要不要保留,我的建议是有条件就保留。因为文本抽取规则很难做到一次完美,保留原始 HTML 意味着后续算法升级后还能重新清洗,不需要重新抓取,也不依赖源站仍然在线。
7. 最后再分享一点个人心得
整个项目做下来,我的体会是:语料库建设的核心其实不是“采集”,而是“克制”。你得克制住贪多求全的冲动,不是所有文本都值得收进库;也得克制住提前简化元数据的冲动,现在省的事,将来都会加倍还回来。数据的价值在反复使用中才会显现,只有在结构上足够规矩的语料库,才经得住不同角度、不同方法的多次挖掘。
如果你正准备开始做类似的数据集,建议不要一上来就写爬虫,先把这几个文档做出来:数据源清单、字段设计表、版权授权台账、清洗规则说明。这些纸面工作看着慢,实际是后面少走弯路最有效的办法。数据是可以重跑的,但调研和设计阶段错过的问题,很多都是无法靠重跑补救的。