简介:本资源是一套基于BERT模型的中文文本纠错完整实现方案,面向NLP初学者、自然语言处理开发者及智能输入法、在线教育等场景的技术实践者,解决中文错别字识别与修正这一典型序列标注任务。压缩包共28个文件,包含16个Python源码(涵盖数据预处理、BERT模型构建、训练/评估/推理全流程)、10个文本配置文件(如混淆词表、同音/同形字库、停用词及词频统计)、1个README说明文档和1个模型文件,整体大小16.85MB,结构清晰、模块解耦,便于学习调试与二次开发。已有197人学习下载,资源提供开箱即用的训练脚本与预置模型,配套详细项目说明与技术原理梳理,覆盖从环境配置、数据准备到模型微调与实际预测的全链路实践,特别适合理解BERT在纠错任务中的适配设计与工程落地细节。
1. 基于BERT的中文文本纠错不是“调个API就完事”:它是一套可落地、可调试、带规则兜底的端到端系统,适合NLP工程师快速验证纠错逻辑、教育类产品做轻量级部署、内容平台做预审过滤
你见过那种“输入错字,模型直接吐出正确句”的demo吗?很炫,但上线后一跑真实用户输入就崩——漏纠、误纠、把“张三丰”改成“张三峰”,甚至把“苹果手机”硬纠成“平果手机”。这不是模型不行,是没搞清中文纠错的底层逻辑:它从来不是纯概率生成,而是语言学规则 + 上下文语义 + 错误模式先验三股力的博弈。这个压缩包(基于bert进行中文文本纠错python源码+模型+项目说明.zip)之所以值得拆,是因为它没走“BERT+Decoder”那条玄学路,而是用BERT做检测器(Detector)+ 规则/词典做修正器(Corrector)的双阶段设计,所有模块都开源、可打断点、可替换、可加日志——连custom_confusion.txt这种人工整理的形近音近字对都给你留了接口。它不承诺100%准确率,但保证你改3行代码就能接入自己业务里的错别字库;不依赖GPU服务器,CPU上跑predict_mask.py也能每秒处理20+句子;更关键的是,它把BERT真正当成了“上下文感知的错误定位器”,而不是万能改写黑匣子。如果你正卡在“模型训出来F1高,但线上一用就翻车”的阶段,或者需要给编辑后台加个实时标红功能,这份源码就是你该抄的第一份作业。
2. 模型结构与双阶段机制:为什么不用Seq2Seq?因为中文纠错的本质是“找错+换字”,不是“重写”
2.1 BERT Detector:不是做分类,而是做token-level的“疑似错误”打分
这个项目没用BERT-Base直接接一个全连接层输出[correct, wrong]二分类——那是初学者最容易踩的坑。它用的是Masked Language Modeling(MLM)微调思路:把原始句子中每个字按一定概率mask掉(比如“我爱北*”),让BERT预测被mask位置最可能的字(如“京”),再对比预测字和原字是否一致。如果预测字≠原字,且置信度>阈值(默认0.6),就标记该位置为“疑似错误”。
核心逻辑在detector.py里:
# detector.py 关键片段 def detect_errors(self, text: str) -> List[Tuple[int, str, str, float]]: tokens = self.tokenizer.tokenize(text) input_ids = self.tokenizer.convert_tokens_to_ids(['[CLS]'] + tokens + ['[SEP]']) attention_mask = [1] * len(input_ids) # 构造mask:对每个token位置,临时替换成[MASK] error_candidates = [] for i in range(1, len(tokens)+1): # 跳过[CLS]和[SEP] masked_input = input_ids.copy() masked_input[i] = self.tokenizer.mask_token_id inputs = torch.tensor([masked_input]).to(self.device) masks = torch.tensor([attention_mask]).to(self.device) with torch.no_grad(): outputs = self.model(inputs, attention_mask=masks) predictions = outputs[0][0, i] # 取第i位置的logits probs = torch.nn.functional.softmax(predictions, dim=-1) top_k = torch.topk(probs, k=5) # 获取top5预测字及其概率 pred_chars = [self.tokenizer.convert_ids_to_tokens([idx.item()])[0] for idx in top_k.indices] pred_probs = top_k.values.cpu().numpy() # 判断:原字是否在top5?若不在,或原字概率<0.6,则标记为疑似错误 orig_char = tokens[i-1] if orig_char not in pred_chars or pred_probs[0] < 0.6: error_candidates.append((i-1, orig_char, pred_chars[0], float(pred_probs[0]))) return error_candidates注意:这里
i-1是token在原始tokens列表中的索引,不是字符位置。中文分词后一个字就是一个token(用的是BertTokenizer,未启用WordPiece切分),所以tokens[i-1]对应原文第i-1个字。但要注意:如果原文含标点、空格、emoji,tokenizer.tokenize()会将其拆成独立token,需在返回结果时映射回原始字符串坐标——这点在corrector.py里用text_utils.char_position_to_token_position()做了补偿。
2.2 Rule-based Corrector:BERT只负责“指哪”,规则库负责“打哪”
检测出“北*”位置可疑,BERT预测可能是“京”“京”“京”……但为什么选“京”而不是“津”?靠的是same_pinyin.txt(同音字)、same_stroke.txt(形近字)、custom_confusion.txt(业务定制混淆对)三层规则叠加。corrector.py的修正流程是:
候选生成:对每个疑似错误位置,收集4类候选字:
- 同音字(来自
same_pinyin.txt,如“北”→“贝”“备”“背”) - 形近字(来自
same_stroke.txt,如“北”→“比”“此”“匕”) - 预训练BERT MLM top5预测字(如“京”“津”“京”“经”“惊”)
- 自定义混淆对(
custom_confusion.txt中“北:京”直接命中)
- 同音字(来自
重排序打分:对每个候选字,计算3项得分:
pinyin_score:与原字拼音相似度(编辑距离/声母韵母匹配)stroke_score:笔画数差值绝对值(越小越好)context_score:将候选字代入原句,用kenlm语言模型打分(kenlm目录下已提供训练好的中文ngram模型)
最终决策:取加权得分最高者(权重可调,默认
pinyin:0.4, stroke:0.3, context:0.3),若最高分<阈值(默认0.5),则不纠正,仅标红。
# corrector.py 中 candidate_scoring 函数节选 def score_candidate(self, char_orig: str, char_cand: str, context: str) -> float: pinyin_sim = self._pinyin_similarity(char_orig, char_cand) stroke_diff = abs(self._get_stroke_count(char_orig) - self._get_stroke_count(char_cand)) stroke_score = max(0, 1 - stroke_diff / 10) # 笔画差≤10才有效 # 构造新句子:替换原位置字符 new_context = context[:self.pos] + char_cand + context[self.pos+1:] lm_score = self.lm_model.score(new_context) # kenlm返回log概率 return 0.4 * pinyin_sim + 0.3 * stroke_score + 0.3 * (lm_score / 1000.0) # 归一化lm_score提示:
kenlm模型是轻量级ngram(3-gram),比BERT推理快10倍以上,适合CPU部署。它的score()返回的是log概率,数值越负代表越不通顺,所以代码里做了/1000.0粗略归一化,避免压倒其他两项。你完全可以换成自己训练的kenlm模型,只需替换kenlm/model.bin并更新config.py中的路径。
2.3 模型与词典的协同边界:什么该BERT管,什么该规则管?
很多团队失败在于让BERT“既当裁判又当运动员”——既要判断错不错,又要决定改成啥。这个项目划清了三条线:
| 任务类型 | 由谁处理 | 为什么 |
|---|---|---|
| 高频错字泛化(如“的得地”混用、“在再”混淆) | BERT Detector + custom_confusion.txt | 这些错误有强统计规律,BERT能从上下文捕捉,但需人工标注混淆对强化 |
| 低频专有名词纠错(如“张三丰”→“张三峰”) | same_pinyin.txt + person_name.txt | BERT没见过“张三丰”,但规则库知道这是人名,且“丰/峰”同音,优先保留人名库中的正确形式 |
| 领域术语纠错(如医疗文本“心肌梗塞”→“心肌梗死”) | custom_word_freq.txt + place_name.txt | 词频文件告诉系统“梗死”在医学语境中比“梗塞”更常见,规则库兜底 |
这种分工让系统具备可解释性:你可以打开custom_confusion.txt,看到北:京、己:已、拔:拨等200+对人工校验过的混淆字,随时增删;也可以在person_name.txt里加一行张三丰,下次检测到“张三峰”就会优先纠正为“张三丰”。
3. 数据准备与模型加载:不是扔个txt就行,中文纠错的数据清洗有3个反直觉细节
3.1 训练数据格式:必须是“原句\t纠错句”对,且纠错句要严格保持原句长度
BERT Detector的训练数据不是单句,而是平行语料对。项目说明里提到的CCTC数据集,其标准格式是:
我爱北* 我爱北京 今天天气很好 今天天气很好 他去上海玩 他去上海玩注意:第二列不能是“我爱北京”“今天天气很好”“他去上海玩”——纠错句必须和原句字符数完全一致。因为Detector的loss计算是逐token的:对每个位置i,预测字与原句第i个字是否相等(binary classification)。如果纠错句变长(如“北*”→“北京”),会导致token对齐错乱。
实际操作中,我们用data_processing.py做预处理:
# data_processing.py 中 prepare_training_data 函数 def prepare_training_data(raw_file: str, output_file: str): with open(raw_file, 'r', encoding='utf-8') as f: lines = [line.strip() for line in f if line.strip()] with open(output_file, 'w', encoding='utf-8') as fw: for line in lines: if '\t' not in line: continue src, tgt = line.split('\t', 1) # 关键校验:长度必须一致 if len(src) != len(tgt): # 尝试用空格补齐(针对末尾缺失) if len(src) > len(tgt): tgt = tgt.ljust(len(src), ' ') else: src = src.ljust(len(tgt), ' ') # 若仍不等长,跳过(真实数据中极少发生) if len(src) != len(tgt): continue # 写入:原句\t纠错句\t标签序列(0=正确,1=错误) labels = [str(int(s != t)) for s, t in zip(src, tgt)] fw.write(f"{src}\t{tgt}\t{''.join(labels)}\n")血泪经验:曾有个团队用爬虫抓的“错字-正字”对,没做长度校验,训练时loss一直不降。debug发现90%的样本标签序列全是0(因为纠错句比原句短,zip后自动截断),模型学了个寂寞。长度校验不是可选项,是必选项。
3.2 预训练模型选择:别迷信“更大更好”,base-chinese-wwm-ext才是中文纠错的甜点区
项目bert_models目录下提供了两个模型:
bert-base-chinese:Google官方中文BERT Base(12层,768维,12头)bert-base-chinese-wwm-ext:哈工大&讯飞发布的“全词掩码”扩展版(同样12层,但预训练时mask的是整词而非单字,更符合中文习惯)
实测对比(在CCTC测试集上):
| 模型 | F1检测率 | 纠错准确率 | CPU推理速度(sent/sec) | 显存占用(FP16) |
|---|---|---|---|---|
| bert-base-chinese | 0.72 | 0.68 | 18.2 | 1.1GB |
| bert-base-chinese-wwm-ext | 0.79 | 0.75 | 16.5 | 1.2GB |
wwm-ext在检测率上提升7个百分点,因为它能更好理解“北京大学”是一个词,不会把“北京”单独mask。但速度略慢——如果你的场景是编辑后台实时标红,选wwm-ext;如果是离线批量清洗,base-chinese够用且更快。
加载方式在config.py中配置:
# config.py MODEL_NAME = "bert-base-chinese-wwm-ext" # 或 "bert-base-chinese" MODEL_PATH = "./bert_models/bert-base-chinese-wwm-ext" # 必须指向解压后的文件夹提示:
MODEL_PATH下必须包含pytorch_model.bin、config.json、vocab.txt三个文件。如果下载的是.tar.gz,解压后确认路径层级——常见错误是把bert-base-chinese-wwm-ext/文件夹多解了一层,导致config.json找不到。
3.3 词典文件加载:不是静态读取,而是运行时动态构建Trie树提升匹配效率
same_pinyin.txt有12万行,“北:贝,备,背,被,辈…”;same_stroke.txt有8万行,“北:比,此,匕,北…”。如果每次纠错都open()->read()->split(),I/O开销巨大。项目用text_utils.py做了优化:
# text_utils.py 中 build_pinyin_trie 函数 def build_pinyin_trie(pinyin_file: str) -> dict: trie = {} with open(pinyin_file, 'r', encoding='utf-8') as f: for line in f: if ':' not in line: continue char, candidates = line.strip().split(':', 1) if len(char) != 1: continue # 构建Trie:key为拼音,value为{char: [candidates]} pinyin = lazy_pinyin(char)[0] if lazy_pinyin else char if pinyin not in trie: trie[pinyin] = {} trie[pinyin][char] = [c.strip() for c in candidates.split(',')] return trie实际使用时,corrector.py只在初始化时调用一次build_pinyin_trie(),构建内存中的Trie结构。后续get_pinyin_candidates('北')直接查表,O(1)时间复杂度。同理,same_stroke.txt也构建成stroke_trie,按笔画数分桶存储。
避坑:
lazy_pinyin来自pypinyin库,但默认带声调(如“bei1”)。项目用lazy_pinyin(char, style=.NORMAL)[0]去掉声调,确保“北”“贝”“备”都映射到bei。如果你的环境没装pypinyin,pip install pypinyin即可,无需额外配置。
4. 训练与评估全流程:从零开始跑通,关键参数和收敛信号全解析
4.1 训练命令与超参解读:batch_size=16不是拍脑袋,是显存与梯度稳定的平衡点
项目train.py支持两种模式:
--do_train:从头训练Detector--do_eval:仅评估已训练模型
典型训练命令:
python train.py \ --model_name_or_path ./bert_models/bert-base-chinese-wwm-ext \ --train_file ./data/train.txt \ --dev_file ./data/dev.txt \ --output_dir ./output/detector_wwm \ --max_seq_length 128 \ --per_device_train_batch_size 16 \ --per_device_eval_batch_size 32 \ --learning_rate 2e-5 \ --num_train_epochs 3 \ --save_steps 500 \ --logging_steps 100 \ --seed 42参数详解:
| 参数 | 值 | 为什么这么设 |
|---|---|---|
--max_seq_length | 128 | 中文纠错句普遍较短(<50字),128足够覆盖99%样本,过长会浪费显存 |
--per_device_train_batch_size | 16 | 在单卡V100(32G)上,16是稳定上限;若OOM,可降至8,但需同比例调高--gradient_accumulation_steps |
--learning_rate | 2e-5 | BERT微调的经典值,比ImageNet迁移学习的1e-3小100倍,防止破坏预训练知识 |
--num_train_epochs | 3 | CCTC数据集约5万句,3 epoch≈15万步,足够收敛;更多epoch易过拟合 |
训练过程中的关键信号:
- Loss下降曲线:前1000步应快速下降(从5.0→1.2),之后缓慢收敛。若1000步后loss>2.0,检查数据格式是否正确(特别是标签序列长度)。
- Dev F1 plateau:验证集F1在epoch2后期应稳定在0.75±0.02。若持续上升,可多训1 epoch;若下降,说明过拟合,需早停。
- GPU显存占用:
nvidia-smi观察,V100应稳定在22~24GB。若>28GB,立即中断,检查--per_device_train_batch_size是否设错。
4.2 评估指标不止F1:必须看“漏纠率”和“误纠率”这两个业务敏感指标
evaluate.py输出不只是F1=0.75,而是详细分解:
=== Evaluation Results === Total samples: 5000 Detected errors: 1243 True positives: 982 False positives: 87 False negatives: 261 Precision: 0.920 # TP/(TP+FP) —— 误纠率=8% Recall: 0.790 # TP/(TP+FN) —— 漏纠率=21% F1-score: 0.850业务视角解读:
- 漏纠率21%:意味着100个真实错字,有21个没标出来。对编辑后台不可接受,需检查
custom_confusion.txt是否覆盖了高频错字(如“的得地”)。 - 误纠率8%:100次标红,有8次标错了。对用户输入法场景致命,需调高Detector的置信度阈值(
--detect_threshold 0.7)或加强kenlm语言模型权重。
调整策略:
# 提高检测严格度(降低误纠,可能增漏纠) python predict_mask.py --detect_threshold 0.7 --correct_threshold 0.6 # 加强语言模型作用(让BERT更听LM的话) python corrector.py --lm_weight 0.5 --pinyin_weight 0.3 --stroke_weight 0.24.3 模型保存与加载:./output/detector_wwm/pytorch_model.bin才是真模型,别用saved_model/
训练完成后,--output_dir下生成:
./output/detector_wwm/ ├── pytorch_model.bin ← 真正的模型权重(必须有!) ├── config.json ← 模型结构定义 ├── vocab.txt ← 分词词典 ├── training_args.bin ← 训练参数(可忽略) └── tokenizer_config.json ← 分词器配置部署时,detector.py加载路径必须指向此目录:
# detector.py 初始化 self.model = BertForSequenceClassification.from_pretrained( "./output/detector_wwm", # 注意:不是"./output/detector_wwm/pytorch_model.bin" num_labels=2 )避坑:常见错误是把
pytorch_model.bin单独拷到其他目录,然后from_pretrained("path/to/bin")——这会报错OSError: Can't load config for...。from_pretrained()必须传文件夹路径,它会自动读取该路径下的config.json和pytorch_model.bin。
5. 部署与避坑:CPU上跑出20+ QPS的5个硬核技巧,以及上线前必须做的3轮压力测试
5.1 CPU加速三板斧:量化、缓存、批处理
BERT Detector在CPU上默认1~2 QPS,但加三步优化可飙到20+:
① FP16量化(无损精度)transformers支持torch.quantization,但更简单的是用onnxruntime:
# 导出ONNX模型(需先训练好) python -m transformers.onnx --model=./output/detector_wwm --feature=sequence-classification onnx/ # CPU推理(比PyTorch快3倍) import onnxruntime as ort sess = ort.InferenceSession("onnx/model.onnx", providers=['CPUExecutionProvider'])② Tokenizer缓存BertTokenizer的tokenize()很慢,用functools.lru_cache:
from functools import lru_cache @lru_cache(maxsize=10000) def cached_tokenize(text: str): return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text))③ 批处理(Batch Inference)predict_mask.py默认单句处理,改成批量:
# predict_mask.py 支持batch def predict_batch(self, texts: List[str]) -> List[List[Tuple[int, str, str, float]]]: # 构造batch input_ids, attention_mask encoded = self.tokenizer.batch_encode_plus( texts, padding=True, truncation=True, max_length=128, return_tensors='pt' ) # ... 推理逻辑(省略) return batch_results实测:单句耗时120ms → 批处理32句耗时380ms(QPS=32/0.38≈84)。
5.2 常见问题排查:现象、原因、解决,一条都不能少
现象1:predict_mask.py运行报错KeyError: '[MASK]'
原因:vocab.txt里没有[MASK]token,或tokenizer_config.json中mask_token字段缺失。
解决:检查bert_models/bert-base-chinese-wwm-ext/vocab.txt,确认第100行是[MASK];若缺失,从官方bert-base-chinese中复制;同时确认tokenizer_config.json含"mask_token": "[MASK]"。
现象2:检测结果全是[](空列表),没标任何错字
原因:detect_threshold设得太高(如0.9),或custom_confusion.txt为空导致候选字不足。
解决:先用--detect_threshold 0.1测试,确认能输出结果;再检查data/custom_confusion.txt是否被意外清空,或编码格式是否为UTF-8(Windows记事本保存常为GBK)。
现象3:纠错后出现乱码(如“北”→“锟斤拷”)
原因:vocab.txt与pytorch_model.bin版本不匹配,或tokenizer加载路径错误。
解决:确认MODEL_PATH下vocab.txt和pytorch_model.bin来自同一模型包;打印tokenizer.convert_ids_to_tokens([101])应输出[CLS],否则tokenizer加载失败。
现象4:kenlm打分始终为0
原因:kenlm/model.bin路径错误,或模型文件损坏。
解决:运行kenlm/bin/query -m kenlm/model.bin,输入我爱北京,应返回log概率(如-3.245);若报错Failed to load model,重新下载kenlm模型。
现象5:多线程调用时segmentation fault
原因:kenlm和transformers底层C++库线程不安全。
解决:用threading.local()为每个线程创建独立kenlm实例:
class KenLMWrapper: def __init__(self, model_path): self._local = threading.local() self.model_path = model_path def get_model(self): if not hasattr(self._local, 'model'): self._local.model = kenlm.Model(self.model_path) return self._local.model5.3 上线前压力测试:不是跑个ab,而是模拟真实用户行为
第一轮:单句延迟测试
用timeit测100次单句(长度20~50字):
import timeit stmt = "corrector.correct('我爱北*')" setup = "from corrector import Corrector; corrector = Corrector()" latency = timeit.timeit(stmt, setup, number=100) / 100 * 1000 # ms print(f"avg latency: {latency:.2f}ms") # 要求<200ms第二轮:并发QPS测试
用locust模拟100用户并发:
# locustfile.py from locust import HttpUser, task, between class CorrectorUser(HttpUser): wait_time = between(1, 3) @task def correct_text(self): self.client.post("/correct", json={"text": "我爱北*"}) # 运行:locust -f locustfile.py --host=http://localhost:5000目标:100并发下P95延迟<500ms,QPS>20。
第三轮:长尾错误测试
构造1000句含生僻字、数字、标点混合的句子(如“第3.1415926章:圆周率π≈3.1415926…”),验证text_utils.py的字符位置映射是否鲁棒。重点看char_position_to_token_position()函数是否能把“π”正确映射到token索引。
6. 进阶技巧:如何用3行代码把纠错系统变成“可解释AI”,以及我每次上线前必做的后悔药清单
6.1 给每个纠错结果加溯源证据:不是“改成XX”,而是“因为同音(北/京)、形近(北/比)、语言模型得分高(-2.1 vs -3.8)”
用户看到“北→京”只会信一半,但看到“同音匹配(北/京)+ 语言模型支持(-2.1 > -3.8)”就信八成。corrector.py的correct()方法返回CorrectionResult对象,扩展它:
# corrector.py 新增 class CorrectionResult: def __init__(self, original: str, corrected: str, details: List[dict]): self.original = original self.corrected = corrected self.details = details # [{'pos': 3, 'orig': '北', 'cand': '京', 'reason': '同音+LM'}] def to_explainable_dict(self) -> dict: return { "original": self.original, "corrected": self.corrected, "corrections": [ { "position": d["pos"], "original_char": d["orig"], "corrected_char": d["cand"], "evidence": d.get("evidence", []) } for d in self.details ] } # 在 correct() 中填充 evidence def correct(self, text: str) -> CorrectionResult: # ... 检测、候选、打分逻辑 details = [] for pos, orig, cand, score in corrections: evidence = [] if orig in self.pinyin_trie.get(self._get_pinyin(orig), {}): evidence.append("同音匹配") if self._is_stroke_similar(orig, cand): evidence.append("形近匹配") if self.lm_model.score(text.replace(orig, cand)) > self.lm_model.score(text): evidence.append("语言模型支持") details.append({"pos": pos, "orig": orig, "cand": cand, "evidence": evidence}) return CorrectionResult(text, new_text, details)调用时:
result = corrector.correct("我爱北*") print(json.dumps(result.to_explainable_dict(), ensure_ascii=False, indent=2))输出:
{ "original": "我爱北*", "corrected": "我爱北京", "corrections": [ { "position": 3, "original_char": "北", "corrected_char": "京", "evidence": ["同音匹配", "语言模型支持"] } ] }提示:前端可据此渲染tooltip,鼠标悬停显示“同音匹配:北/京拼音均为bei;语言模型支持:‘我爱北京’得分-2.1,高于‘我爱北*’的-3.8”。
6.2 我每次上线前必做的后悔药清单:5分钟,保你少背3个月锅
从那以后我每次上线新版本纠错系统,都强制走一遍这5件事,雷打不动:
- 查
custom_confusion.txt最后10行:确认没混入#注释或空行(#开头的行会被readlines()读入,导致split(':')报错); - 跑
python test_data.py:项目根目录下这个脚本会验证所有词典文件格式(same_pinyin.txt每行必须含:,person_name.txt不能有重复名); - 用
--debug模式跑3个典型错句:python predict_mask.py --debug --text "己所不欲",确认日志输出[DEBUG] Detected error at pos 0: 己->已 (score=0.82); - 检查
kenlm/model.binmd5:md5sum kenlm/model.bin,和README里提供的md5比对,防止下载损坏; - 在
config.py里把DEBUG_MODE = True改为False:这个开关控制是否打印详细日志,上线必须关,否则磁盘IO爆炸。
这5件事做完,通常能提前发现80%的线上事故。比如上周,custom_confusion.txt被同事用Excel另存为时加了BOM头,导致Python读取报UnicodeDecodeError——要不是第1步,就得等用户投诉才发现。
希望帮到你。
本文还有配套的精品资源,点击获取