简介:面向自然语言处理研究者与机器学习工程师,这份综合报告聚焦大规模预训练语言模型的数据集构成。报告基于2018年至2022年初公开信息,逐项比较GPT-1、GPT-2、GPT-3、GPT-NeoX-20B、Megatron-11B、MT-NLG和Gopher七个模型的训练数据来源、规模、令牌数量与内容类型,并重点剖析Wikipedia、Common Crawl、Books、Reddit链接等常见语料的占比与偏差,同时指出GPT-3中Books1、Books2的数据分析疑点。针对当前模型文档中数据集透明度不足的问题,作者提出了改进方向,强调参照透明度标准披露细节,为后续数据集构建和模型训练提供重要参考;读者可据此了解各模型数据组合的异同,识别语料偏差风险,也可作为构建新数据集时的对照基准。资源为单个PDF文件,压缩包大小2.36MB,内含分章节分析、对比表格、参考文献与附录材料,信息密度较高。已有111人学习下载,适合需要深入理解大模型数据底细的读者查阅。
1. 大规模预训练语言模型数据集:先把“喂什么”想清楚
“把模型做大不如把数据做干净”,这句话在训练千亿参数模型时不是口号,而是成本约束下的必然选择。做过大规模数据清洗的人都知道,几十 TB 的原始网页文本里,重复内容、机器生成的噪声和低质量会话要占掉很大一块;直接拿去训练,模型学的不是语言规律,而是“背题”。下面要拆解的是大规模预训练语言模型数据集的完整处理链路:不同来源怎么配比、重复数据怎么去重、质量怎么打分、采样概率怎么设。适合正在跑预训练、准备扩充训练语料,以及想搞清 LLaMA、Mistral 这类模型为什么选这些数据的工程师。
2. 从 Common Crawl 到精编语料:预训练语言模型数据集的来源与构成
2.1 数据不是“一堆文本”,是分层的四类来源
预训练语言模型的数据集在工程上从来不是一个巨大的 txt 文件,而是几十个来源、几种格式、几套清洗历史拼起来的混合体。按内容和作用可以分成四个层级:网页底座、知识库、书籍与长文、代码与结构化数据。每个层级承担的能力不同,处理方式也不同,混在一起统一清洗是后面所有坑的根源。
网页底座是绝对主力,代表是 Common Crawl 这类网络爬虫产物,规模最大但噪声也最大;知识库指 Wikipedia 和各语言百科,规模小但事实密度高;书籍和长文负责长程依赖和叙事连贯性,训练模型“把话说完整”的能力;代码语料则直接影响程序语言理解和逻辑推理。对话与指令数据虽然量小,却决定了模型最终的回复格式,通常放到后训练阶段再介入。
实际切分数据时可以参考下面这张分层表,它也是多数开源数据集构建时采用的思路:
| 数据层 | 典型来源 | 规模量级 | 对模型能力的贡献 |
|---|---|---|---|
| 网页底座 | Common Crawl WARC/WET | 原始 PB 级 | 语言流利度、常识覆盖 |
| 清洗后网页 | C4、RefinedWeb 等 | 清洗后 TB 级 | 预训练主训练体 |
| 知识库 | Wikipedia 及各语言百科 | GB 到 TB 级 | 事实性知识、实体关系 |
| 书籍与长文 | 开源书籍语料 | TB 级 | 长程依赖、叙事连贯 |
| 代码 | GitHub 公开仓库快照 | TB 级 | 程序语法、逻辑推理 |
| 对话与指令 | 开放问答、合成指令 | GB 级 | 指令跟随、回复格式 |
这里说的量级是行业常见区间,不是某个特定数据集的精确统计。实际建库时你会发现,网页底座往往比其它所有层加起来还要大两个数量级,所以它的清洗质量基本决定了模型的下限。
2.2 网页语料从 WARC 到纯文本:warcio 解析的最小流程
Common Crawl 的物理格式分三层:WARC 是完整抓取包,WET 是已经抽出来的纯文本,WAT 是元数据。大多数人直接下载 WET 用,但如果要自己做定制清洗,往往还是得回到 WARC,自己控制解析逻辑。常见做法是用 warcio 这个库读取,它不是把整个文件解压进内存,而是按记录流式读取,这对动辄几百 GB 的压缩包来说很关键。
import warcio from warcio.archiveiterator import ArchiveIterator with open("CC-MAIN-2024-*.warc.gz", "rb") as f: for record in ArchiveIterator(f): if record.rec_type != "response": continue url = record.rec_headers.get_header("WARC-Target-URI", "") payload = record.content_stream().read() # payload 是原始 HTTP 响应体,下一步需要剥 HTML 标签逻辑说明:ArchiveIterator 会逐条返回 WARC 记录,rec_type 用来过滤掉请求记录和元数据记录,只保留真正的 HTTP 响应;content_stream 是惰性读取,不会把整个大文件一次性加载。文件名里的星号是通配符,实际按 crawl 批次和 segment 编号组织,你需要在构建脚本里先用 glob 展开真实路径。
解析出响应体后还要走几步固定动作:剥掉 HTML 标签、去掉 script 和 style 里的内容、做 HTML 实体反转义。不要在这一步省事,后面所有质量过滤都建立在“拿到干净的正文文本”这一前提上。
2.3 高价值子集怎么挑:启发式打分先于模型过滤
从原始网页里捞出来的文本,质量参差到什么程度都有。直接上分类器或困惑度过滤太早,成本高且不可解释。我一般会先跑一轮启发式打分,用几条简单规则快速把明显不行的文档筛掉,再做精细过滤。
def heuristic_score(text: str) -> float: sents = text.split("。") if len(sents) < 3: return 0.0 alpha = sum(ch.isalpha() for ch in text) total = max(len(text), 1) return (alpha / total) * min(len(sents) / 10, 1.0)逻辑说明:这个函数用两个信号衡量文本质量。字母密度过滤掉表格、乱码和 Base64 噪声;句子数量下限保证文档有基本上下文,不是一句口号或一条导航。分数只用来排序,不建议设绝对阈值截断,而是排序后按分位数切分,比如保留前 80%。
启发式打分的优点是可解释、零成本、不会误杀新领域文本,缺点是只看局部特征,抓不住“整体读起来通顺但内容是模板拼凑”的文档。所以它只能当第一道粗筛,把明显垃圾清走,给后面的模型过滤留出干净输入。
2.4 开源数据集能直接用吗:C4、The Pile 与自建取舍
做数据集时最常见的第一个问题是:有没有现成的能用?C4、The Pile、SlimPajama 这些开源数据集质量不错,在通用英文任务上表现稳定,省掉大量清洗时间。但它们的短板也很明显:中文占比偏低,代码和领域文本比例固定,没法针对下游任务调节。自己做预训练采集时,我见过不少团队的做法是“开源数据集打底 + 自建领域语料叠加”,而不是二选一。
叠加的时候注意去重要跨两个集合做。开源数据集内部已经去重过,但和自建语料之间必然有重叠,尤其是转载量大的新闻和技术博客。直接拼接会让某一部分内容在最终语料里被隐式过采样,后面配比阶段的权重计算就失真了。所以数据源管理从一开始就要给每个来源记录原始路径和版本,后面回查重复来源时用得上。
3. 重复数据与清洗:预训练语言模型数据集的第一道减法
3.1 重复数据的三种形态,以及它们如何伤害预训练
预训练数据集里的重复不只是“同一篇文章出现两次”这么简单。实际数据里重复有三种形态,危害也不一样:完全重复指内容一字不差,来自镜像站、爬虫重抓;近重复指内容主体相同但带少量差异,典型是转载时加了编辑按语;模板重复则是页面框架相同而正文不同,比如商品详情页、评论区和多语言自动翻译页。
完全重复最直接的后果是训练效率下降,模型把大量计算花在重复梯度的文本上。近重复更隐蔽:模型会把转载版本里的编辑按语也学进内部知识,表现为生成内容里频繁出现与正文无关的插入句。模板重复会造成最典型的“背题”现象,模型记住的不是语言规律,而是某个网站的页面结构。
| 重复形态 | 典型来源 | 推荐手段 |
|---|---|---|
| 完全重复 | 镜像站、爬虫重抓 | MD5 精确去重 |
| 近重复 | 转载改写、页面框架相同 | MinHash + LSH 模糊去重 |
| 模板重复 | 导航栏、标签云、评论区 | n-gram 覆盖率 + 规则过滤 |
三种形态处理手段完全不同。代价越低、判据越硬的手段放在越前面,能精确判定的先精确判定,剩下模糊的再交给概率方法。
3.2 精确去重:用哈希处理 TB 级文本的起点
精确去重用 MD5 或 SHA-256 对文档做指纹,用哈希集合判重。这套方案在 TB 级数据上跑得动,瓶颈只在外排序和磁盘空间,不需要 GPU。对中文数据要注意先做 Unicode 规范化再做哈希,否则全角和半角、不同编码造成的同一篇文章会被当成两个不同文档。
# 每个文档一个哈希,按哈希值排序后找重复 find corpus -name '*.txt' -print0 | xargs -0 md5sum > doc.md5 awk '{print $1}' doc.md5 | sort | uniq -d逻辑说明:第一行把语料目录下所有 txt 文件计算 MD5,结果写到 doc.md5;第二行提取哈希列,排序后用 uniq -d 列出出现次数大于 1 的哈希值。要注意这里只列出了重复的哈希,如果要拿到具体哪些文档重复,需要再 join 回原文件或记录文件名列表。
精确去重的局限也在这里:一个字节不同就不算重复。实际数据里转载文本几乎都会带后缀、版权声明或编辑修改,所以精确去重只能清掉最蠢的那部分重复,剩下的交给下一层处理。
3.3 MinHash + LSH 模糊去重:参数怎么设才能稳定跑
模糊去重的工业级做法是 MinHash 加 LSH。基本思想是把文本切成的定长 shingle 看成集合,两个文档的重复程度用 Jaccard 相似度估计,再用 LSH 把高相似度候选分到同一桶里做验证。datasketch 库里这两步都是现成的,关键在参数。
from datasketch import MinHash, MinHashLSH def char_shingles(text: str, k: int = 8): text = text.strip().lower() return {text[i:i + k] for i in range(len(text) - k + 1)} def minhash_for_text(text: str, num_perm: int = 128): m = MinHash(num_perm=num_perm) for shingle in char_shingles(text): m.update(shingle.encode("utf-8")) return m m = minhash_for_text(doc) lsh = MinHashLSH(threshold=0.75, num_perm=128) lsh.insert(doc_id, m)逻辑说明:char_shingles 按字符窗口生成 n-gram,对中文来说一般直接用字级窗口,不需要分词;MinHash 把这个集合压缩成一个固定长度的签名;LSH 用 threshold 控制相似度的判定边界。插入后可以用 query 方法从桶里取出相似文档候选,再做精确 Jaccard 计算。
参数是这套方案真正要调的地方。num_perm=128 是精度和内存的平衡点,降到 64 会快很多,但小文档的估计误差会明显变大;threshold=0.75 表示 Jaccard 相似度高于这个值判定为重复,0.7 到 0.85 之间都有人在用,对新闻转载类语料我一般从 0.8 起调。shingle 长度 k=8 适合中文,如果语料偏短,比如标题和摘要为主,降到 5 更合适。
提示:LSH 是概率型结构,相似度恰好卡在阈值附近的文档会漏判。生产环境常见做法是“MinHash 召回候选 + 精确 Jaccard 验证”两阶段,先用宽松阈值召回,再用精确计算剔除假阳性。
3.4 清洗规则集:把 HTML 残迹、乱码和广告位清出去
去重之后是清洗。清洗规则的关键是按顺序执行,顺序错了效果就差很多。比如先做 HTML 剥离再做 Unicode 规范化,否则标签里的实体字符会影响文本统计;先过滤语言再过滤长度,否则短文本过滤会把混合语言里本来有效的句子误删。
| 步骤 | 规则 | 常用参数 |
|---|---|---|
| HTML 剥离 | 去掉 script/style/header/footer 标签 | BeautifulSoup 的 decompose() |
| Unicode 规范化 | NFKC,统一全角半角 | 中文语料必须做 |
| 乱码检测 | mojibake 正则匹配替换 | 匹配常见乱码字节序列 |
| 语言过滤 | fastText lid.176 模型 | 目标语言概率大于 0.8 |
| 长度过滤 | 句子数大于等于 3,字符数大于 200 | 按语种和用途调整 |
| 行级重复 | 相邻行去重 | 连续出现 5 次以上剔除 |
提醒一句:清洗是减法,跑完一轮先看数据分布再决定下一轮。很多团队在清洗阶段过度激进,把多样化的文本全杀光了,模型训练出来语言正确但风格单一。清洗目标是去掉确定有害的内容,而不是把语料“打磨”成只有一种形态。
4. 质量打分与过滤:预训练语言模型数据集的第二道筛子
4.1 质量信号的选择:困惑度、分类器与规则各有盲区
启发式规则能清掉垃圾,但抓不住“语言通顺但内容空洞”的文本,比如自动生成的商品描述、SEO 堆砌文章、机器翻译的残次品。第二种筛子要给每条文档打一个质量分,然后按分数截断。业界常用三类信号:困惑度、分类器打分、规则打分,它们各有盲区,需要组合使用。
困惑度的直觉是“一个语言模型觉得这句话有多意外”,文本越混乱、越不符合统计规律,困惑度越高。它不需要标注数据,跑起来快,但受模型先验影响大,Gopher、T5 做数据集过滤时都实践过这条路线。分类器能捉住“读着通顺但模板化很重”的文本,缺点是需要手工标注一部分数据来训练。
规则打分在三个方案里最容易被低估。它其实最适合拦截确定性问题,比如硬编码的脏词列表、乱码特征、超长无标点段落。它的好处是可解释,坏处是覆盖面窄,只能做兜底,不能做主过滤。
4.2 用 GPT-2 困惑度给文档打分:能直接跑的样例
用困惑度过滤有一个很直接的实现:拿一个小型语言模型当评分器。常见做法是加载 GPT-2,对每篇文档算平均负对数似然,再取指数的平均得到困惑度。打分结果排序后按分位数截断,比如保留困惑度最低的 70% 到 80%。
from transformers import GPT2LMHeadModel, GPT2TokenizerFast import torch tokenizer = GPT2TokenizerFast.from_pretrained("gpt2") model = GPT2LMHeadModel.from_pretrained("gpt2") model.eval() def perplexity(text: str) -> float: inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) with torch.no_grad(): logits = model(**inputs).logits shift_logits = logits[:, :-1, :].contiguous() shift_labels = inputs["input_ids"][:, 1:].contiguous() loss_fn = torch.nn.CrossEntropyLoss() loss = loss_fn( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ) return torch.exp(loss).item()逻辑说明:模型输出的 logits 和输入标签做平移对齐,每个位置的预测概率与真实 token 求交叉熵,平均后取指数换算成困惑度。分数越低表示这段文本越符合模型对自然语言的预期。注意这里用truncation=True截断到 512 token,超过 512 的文档只算了前半段,算长文档时要分段处理再按 token 数加权平均。
提示:gpt2 的 tokenizer 是英文 BPE 词表,对中文语料打分偏差明显。处理中文时要么用支持中文的 GPT-2 变体,要么把打分模型换成多语言模型,否则过滤结果会系统性偏向英文语料。
4.3 分类器细筛:标几百条数据,训练一个质量二分类
困惑度对模板化文本的识别效果有限。商品页面、SEO 垃圾文语义是连贯的,困惑度并不高,但内容重复度高、可读性差。这类问题用质量二分类器更合适:手工标注几百条高质量和低质量样本,训练一个文本分类器,然后对全量语料打分。
用 fastText 是最低成本的做法。每行一条文本,标签前缀用__label__high和__label__low,训练和推理都是命令行操作。
# 训练一个文本质量二分类器 fasttext supervised \ -input train.txt \ -output quality_model \ -epoch 25 \ -wordNgrams 2 \ -dim 100 # 对单条文档打分 fasttext predict quality_model.bin doc.txt逻辑说明:supervised 模式训练的是有监督分类器,输入是训练文本,输出是质量类别。wordNgrams 2 会让模型看到相邻两个词的组合,这对抓“模板化短语”很重要,比如商品详情页里反复出现的固定表达。epoch 25 在几千条训练样本上足够,再多会过拟合。
这一层最关键的是标注质量,而不是模型结构和参数。标注时要把低质量样本标注清楚,标注重点是“类型”而不是“喜好”,比如 SEO 堆砌、机器翻译痕迹、无信息量短句。分类器训练完成后要用一批模型没见过的数据做人工抽检,防止分类器把某个领域的正常文本误判为低质量。
4.4 三层筛网怎么配合:按数据源切分,而不是按全量排序
三个质量信号不是互相替代,而是流水线上下游。我建议的配合方式是:第一层困惑度做粗筛,按分位数截断把语义混乱的文本清掉;第二层分类器只剔除高置信的坏样本,避免误杀;第三层规则做兜底,拦截编码错误和黑名单内容。这里有一个常见的错误做法,是把所有来源的文档混在一起统一排序后按比例截断。不同来源的困惑度分布差异很大,代码语料的困惑度天然比百科高,混在一起会系统性地清掉某一个来源。正确做法是按数据源分组,在每个来源内部排序再截断。
| 层级 | 操作 | 目标 | 误杀风险 |
|---|---|---|---|
| 第一层 困惑度 | 按分位数截断 | 去掉语义混乱文本 | 中 |
| 第二层 分类器 | 高置信坏样本剔除 | 去掉模板化低质文本 | 低 |
| 第三层 规则 | 黑名单、长度下限 | 拦截编码垃圾 | 最低 |
每轮过滤之后都要做一次“被过滤样本抽检”,确认被删掉的文档里没有意外的高价值内容。数据过滤是迭代过程,不是跑一条流水线就结束。
5. 数据配比与采样:预训练语言模型数据集的第二个旋钮
5.1 配比决定能力上限:从 token 预算反推概率
模型最终会具备什么能力,很大程度上由语料里各类内容的 token 占比决定,而不是由语料文件大小决定。同一批网页文本,清洗后有效 token 数可能只剩三成;Books 数据文件很大,但按行数统计的 token 密度并不高。所以做配比的时候,先定目标 token 占比,再由各来源的有效 token 量反推采样概率。
拿一个通用中文模型举例,常见的目标配比大概是:清洗网页 60%、书籍与长文 15%、百科 10%、代码 10%、对话与指令 5%。这个比例不是标准答案,只是说明性的起点。你的下游任务偏代码就调高代码占比,偏知识问答就调高百科和书籍占比,关键是把“目标占比”和“实际文件大小”区分开。
| 数据源 | 目标 Token 占比 | 实际数据量 | 说明 |
|---|---|---|---|
| 清洗网页 | 60% | 大 | 语言底座,决定流利度 |
| 书籍与长文 | 15% | 中 | 决定长程依赖能力 |
| 百科 | 10% | 小 | 事实密度最高 |
| 代码 | 10% | 中 | 程序语言与逻辑 |
| 对话与指令 | 5% | 小 | 回复格式 |
在计算配比之前,先确保每个数据源都经过同一套 tokenizer 统计有效 token 数。不同清洗历史会导致同一份文本的 token 效率差很多,用文件大小做分母算出来的权重是错的。
5.2 用带温度的采样权重修正数量不均衡
配比最大的坑是数据量不平衡。网页语料轻易上千亿 token,百科可能只有几十亿,直接按目标比例采样,几次 epoch 就把百科数据反复学了很多遍,导致过拟合。常见做法是引入温度参数做平滑采样。
target = {"web": 0.60, "book": 0.15, "wiki": 0.10, "code": 0.10, "chat": 0.05} size = {"web": 900e9, "book": 30e9, "wiki": 5e9, "code": 40e9, "chat": 2e9} T = 0.5 raw = {k: target[k] / (size[k] ** T) for k in target} total = sum(raw.values()) weight = {k: v / total for k, v in raw.items()} print(weight) # 数据量小的书籍和对话会被明显过采样,网页占比下降逻辑说明:每个来源的权重等于目标占比除以数据量的 T 次方。size 的次方项相当于给数据量做缩放,T 越小缩放越弱,小语料来源会被抬得越高。T=1 时完全按 target 比例分配 token,小语料会被反复重采样;T=0 时完全按来源数量比例走,目标占比形同虚设。0.3 到 0.7 是常见取值区间,T=0.5 是个不错的起点。
采样权重要在训练循环里转成每个 batch 的实际取数逻辑。常见做法是按权重为每个数据源分配一个采样器,batch 的样本来源由 these 权重抽样决定。还要定期检查实际投喂的 token 数与目标占比的偏差,因为不同来源的文档长度分布差异很大。
5.3 多语言与混合来源的坑:别让英文和模板文本主导
多语言数据集的第一个坑是英语天然过剩。Common Crawl 里英文网页占到一半以上,在没有语言采样策略的情况下,英文会挤占其它语言的训练量。第二个坑是低资源语言的文本往往来源单一,典型的小语种新闻站就那么几个,去重不干净的话,模型容易把站点的模板风格当成语言规律。
解决多语言不平衡的常见手段是“分语言配比”,先用语言识别模型对每条文档打标,再按目标语言占比做分组采样。采样时语言概率不是拍脑袋定的,而是按下游任务的验证集表现来回调:如果某个语言的下游任务分数明显落后,优先检查是数据占比不够还是该语言语料去重不干净。
代码语料也要单独提一句。代码数据要先按扩展名过滤,再按许可证过滤。许可证过滤是硬要求,不要因为少部分数据量而模糊处理,这直接关系到数据集能不能对外发布和模型能不能商用。
6. 先小规模试错,再盯三个数据指标
6.1 用 1% 的数据跑配方验证
大规模预训练跑一次动辄几万美元,直接拿最终数据集去训练来验证配比是代价最大的做法。常见做法是先用 1% 到 2% 的数据量,保持原有配比不变,训练一个小模型到 10B 到 20B token,看几组候选配方的 loss 曲线差异。数据量缩小后训练时间从几周降到几小时,足够判断哪套配方更平滑、下游任务分数更高。
# 从各子集按 1% 等比抽样,保持原始配比 for f in subsets/*.txt; do lines=$(wc -l < "$f") shuf -n $(( lines / 100 )) "$f" >> ablation_sample.txt done逻辑说明:脚本对每个数据源文件按行数抽 1%,抽样时使用 shuf 保证随机性。抽样后拼接成一个文本文件供小规模训练使用。这一步的关键是抽样比例必须对所有来源一致,否则验证出来的配比分不清是配方差异还是抽样偏差。
6.2 训练日志里加三个指标:重复率、退避率、有效 token 率
数据质量好不好,不能只看训练 loss。loss 降得很漂亮但重复率很高,说明模型在背数据而不是学语言。训练进入稳定期后,我一般会盯这三个指标:重复率、退避率、有效 token 率。
重复率计算简单,可以在训练前对数据集做一次扫描,统计出现次数大于 1 的文档在总 token 里的占比。退避率指训练时因长度超限被截断的文档比例,退避率过高说明语料准备阶段没做好长度分析。有效 token 率是清洗后 token 数除以清洗前原始 token 数,它反映清洗管线的工作效率,主要用于评估清洗步骤是否过度。
import hashlib from collections import Counter seen = Counter() for chunk in chunks: digest = hashlib.sha256(chunk.encode("utf-8")).digest() seen[digest] += 1 dup_count = sum(c - 1 for c in seen.values()) ratio = dup_count / len(chunks) print(f"duplicate_ratio={ratio:.3f}")逻辑说明:对每个 chunk 计算 SHA-256 摘要并计数,统计计数大于 1 的额外出现次数。这个值除以总 chunk 数就是重复率。注意这里算的是“完全重复”,实际生产环境还应配合 MinHash 做模糊重复率统计,才能反映近重复文本的比例。
6.3 用一组稳定的小任务做锚点:loss 降不代表能力涨
最后的验证手段是拿一组稳定的小型下游任务做锚点,贯穿整个数据调优周期。loss 是全局平均视角,下游任务才能暴露具体能力短板。任务集不要求覆盖全面,但要稳定、快、和你的数据来源强相关。
| 单点能力 | 推荐小评测 | 数据源相关性 |
|---|---|---|
| 知识问答 | MMLU 子集、常识问答 | 百科、书籍 |
| 代码能力 | HumanEval、MBPP | 代码语料 |
| 长文连贯 | 摘要、续写评测 | 书籍、长文 |
| 多语言 | 各语言困惑度对比 | 各语言子集 |
调完一批数据后,跑一遍锚点任务并记录分数。最好把每次数据变更和对应分数记录成表,这样哪次改动导致哪个能力下降,回查数据变更记录就能定位。把重复率统计脚本挂进每天的数据构建流程里,再跑一轮小规模训练对比,这是当前成本最低、收益最稳定的数据质量闭环。
本文还有配套的精品资源,点击获取