ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

古籍汉字识别实战:CNN字符级分类与ResNet训练全解析

古籍汉字识别实战:CNN字符级分类与ResNet训练全解析 简介这是一份关于卷积神经网络在古籍汉字识别中应用实践的PDF文献面向数字人文研究者、OCR与深度学习从业者以及对古籍智能化处理感兴趣的技术人员。内容系统梳理了从CNN基本原理、古籍汉字识别难点到基于TensorFlow构建训练集与模型的完整流程。包内为1个PDF文件压缩包总大小约6.39MB包含试验思路、网络结构设计、数据生成技术与识别结果分析等核心内容便于读者直接研读原文图表和公式。目前已有222人学习下载。资料结合具体实践案例介绍了对773个汉字生成约24万训练样本、利用阈值筛选不可识别图片并转人工处理等关键细节可帮助读者理解CNN分类器构建、训练集扩充及实际部署中的排错思路适合作为古籍元数据加工、书法字识别及相关智能处理项目的重要参考。1. 古籍汉字识别为什么绕不开卷积神经网络先让 CNN 认字再谈整页读做过古籍数字化的人大都撞过同一堵墙古籍汉字识别一落到影印扫描件上通用 OCR 的准确率就崩到没法看。字形不规范、异体字多、纸张噪声重、竖排版式把边框和注疏搅在一起端到端模型在这种数据量有限的场景里更训不动。真正先落地的做法是把任务拆成两步先把版面切成单个汉字再用卷积神经网络CNN判断这个字是哪个汉字。这篇实践笔记围绕「CNN 只负责认字」的定位展开讲输入归一化、骨干网络选型、数据增强、训练参数和踩坑记录。适合已经在做 OCR、想往古籍数字化落地的工程师也适合刚接触 CNN 但想尽快跑通第一个汉字识别模型的新手。你不必一开始就搞懂整页识别的所有环节先跑通单字分类后面接版面恢复会顺很多。2. 把古籍汉字识别拆成字符级分类CNN 的输入、感受野与骨干网络取舍先说结论如果你的目标是让古籍汉字识别在项目里真正跑起来不要一开始就追端到端模型。端到端听着省事实际要吞下整页图像的版面噪声和排版变化古籍里竖排、眉批、双行小注混在一起端到端的转录标注成本高到大多数项目撑不住。把任务拆成「找到字 认出字」两个环节CNN 只需要负责「认出字」这一半。这个拆法在古籍领域特别实用也是下面所有参数设定和代码的前提。2.1 顶层设计为什么先切成单字而不是端到端整页识别端到端模型需要大量「整页图像 全文转录文本」成对数据。古籍页面里断句圈点、双行注疏、版框线和虫蛀斑驳交错一行文字的人工抄录成本远高于现代印刷文档生僻字在里面还容易被标注者直接跳过。反过来字符级分类数据可以按字收集同一个「未」字能从不同页面里各切一张拼出几百张样本。这种跨页面、跨版本的样本复用能力让字符级方案在小数据条件下明显占优。CNN 在这个拆法里的任务就是分类器输入一张切好的单字灰度图输出它属于几千个候选字中的哪一类。这个定位让模型可复用、可增量更新今天收集到一批新字形往类别清单里加一类就能继续训练。代价是切分环节的质量决定了模型性能上限这一点我会在第 5 章专门展开。另一个容易忽略的点是 CNN 的平移容忍能力。手工切字不可能每次都把字摆在正中央卷积结构对「同一字形偏移几个像素」天然有很强的容忍度这比传统特征工程时代的模板匹配稳妥得多。只要切分偏移不超过训练增强的范围分类器基本不受影响。2.2 输入归一化灰度、前景居中与尺寸对齐顺序先给参数结论输入建议用单通道灰度图尺寸 96×96。为什么不用三通道古籍影印件本身多为单色扫描决定字形的是墨迹形状不是颜色。为什么不直接做二值化阈值一刀切很容易把淡墨断笔当成背景、把纸色颗粒当成笔画反而丢掉信息。保留灰度让 CNN 自己学「淡墨也是字的一部分」这个规律这是古籍和现代印刷体 OCR 在输入侧最大的区别。下面这段是切完单字后的归一化函数直接用 OpenCV 就能跑import cv2 import numpy as np def normalize_glyph(src: np.ndarray, target_size: int 96) - np.ndarray: 把影印件里切出的单字区域归一化成 CNN 输入。 src: BGR 的单字小图可能带着黑边和纸色杂质 gray cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 先做一次 Otsu 二值化只用于找前景包围盒 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) coords cv2.findNonZero(binary) x, y, w, h cv2.boundingRect(coords) # 留 8% 边距避免笔画顶到边界导致卷积响应被裁掉 margin int(0.08 * max(w, h)) x max(0, x - margin) y max(0, y - margin) w min(gray.shape[1] - x, w 2 * margin) h min(gray.shape[0] - y, h 2 * margin) crop gray[y:y h, x:x w] # 等比缩放后放进 96x96 白底画布不要直接拉成方形 scale target_size / max(crop.shape) resized cv2.resize(crop, None, fxscale, fyscale, interpolationcv2.INTER_AREA) canvas np.full((target_size, target_size), 255, dtypenp.uint8) oh, ow resized.shape y0 (target_size - oh) // 2 x0 (target_size - ow) // 2 canvas[y0:y0 oh, x0:x0 ow] resized return canvas逻辑上要注意Otsu 二值化只用来找前景包围盒真正进入网络的是原始灰度等比缩放而不是直接拉伸成方形避免字形被压扁或拉长用白底填充而不是黑底因为白底更接近真实古籍纸面。参数上 target_size96 是速度和精度的平衡点超过 128 后精度提升有限训练和推理耗时却明显上升。缩小字形时用 INTER_AREA 插值能抑制高频摩尔纹比双线性缩放的细节表现更干净。2.3 感受野与骨干网络选型ResNet 为什么比 VGG 更适合古籍字形卷积神经网络原理里和古籍识别最直接相关的两个概念是感受野和残差结构。对于刻本和写本混用的古籍笔画边缘的细微差异比如「未」和「末」的横画长短要靠浅层卷积的局部响应来分辨而一个字是左右结构还是上下结构需要更大的感受野把整个字形装进去。不用急着把卷积神经网络结构图整张背下来做古籍识别只需要记住三块卷积层抓局部笔画池化层压缩空间最后全连接层做类别输出。3×3 卷积核堆叠既能随层数加深扩大感受野又比 5×5 大卷积核省参数所以现代骨干网络几乎都是小卷积核堆叠。VGG 的问题在于把大量参数放在全连接层上类别一旦上千全连接层就成了显存黑洞。ResNet 用残差连接让深层网络在训练中更容易保留浅层细节这就是它在小样本古籍数据下比 VGG 稳的原因。下面这张表是我在实际项目里的选型参考网络参数量参考单字推理速度适合场景备注VGG-16约 1.38 亿慢不推荐做古籍分类最后一个全连接层在类别多时膨胀太严重ResNet-18约 1120 万快基线首选先跑通流程再换更大网络ResNet-50约 2550 万中样本量 50 万以上对形近字更友好也更容易过拟合MobileNetV3约 250 万很快CPU 批处理/端侧类别多时 top-1 会比 ResNet 低 1-3 个点选型结论很直接不纠结的话第一版直接用 ResNet-18。注意一件事96×96 输入下最后一层感受野已经足够覆盖整个字形所以不需要靠加大输入来补全局信息。当类别数超过 3000 时最后一个全连接层的权重会占到模型参数的一大半显存吃紧时优先换 MobileNetV3而不是砍输入尺寸。3. 从影印古籍到字符级训练集投影切分、标注规则与六种针对性增强数据是古籍识别真正的护城河。模型结构可以照搬现成的但数据管线必须按古籍自己的特性来搭。我的做法是先按「列」切再在列内按「字」切切完再进入标注和增强环节。这里给出的切分代码不是最复杂的但是最容易调通、也最容易解释失败原因的一套。3.1 列切分与列内字符切分投影法的参数与边界坑古籍以竖排为主第一步是把整个页面切成一条条竖列。这一步错了后面全错。最可靠的是投影法对二值图按列统计前景像素数在某一段列区间里几乎看不到笔画像素说明这里就是列与列之间的空白通道。投影切分的核心函数如下import numpy as np def projection_spans(proj: np.ndarray, min_gap: int 10) - list: 把一维投影数组切成连续区间返回 [(start, end), ...]。 spans [] start None gap 0 for i, val in enumerate(proj): if val 0: if start is None: start i gap 0 else: gap 0 else: if start is not None: gap 1 if gap min_gap: spans.append((start, i - gap 1)) start None gap 0 if start is not None: spans.append((start, len(proj) - 1)) return spans实际切列和切字时这样调用foreground page_binary 128 # 黑字白底 v_proj foreground.sum(axis0) # 每一列里的笔画像素总数 columns projection_spans(v_proj, min_gap10) for x0, x1 in columns: col_img page_binary[:, x0:x1] # 在列内按水平投影切字符 h_proj (col_img 128).sum(axis1) char_rows projection_spans(h_proj, min_gap4)min_gap 是最关键的参数。在 300 dpi 扫描分辨率下列缝的 min_gap 取 10-12 比较稳大约是一个标点符号的宽度200 dpi 降到 6-8600 dpi 取 20-24。字符切分的 min_gap 取 4 左右太小会把笔画断口误当字符边界太大会把两个字粘在一起。切出来每个矩形框建议再向外扩 2-3 像素避免把半边笔画随空白一起切掉。提示当页面出现双行小注或眉批时单纯投影法会出现列间粘连。常见做法是先做连通域分析把高度超过单字上限的连通域当成大字主干再回填投影结果做二次拆分不能让投影法直接一票定生死。3.2 标注规则与类别清单最容易被团队吵起来的环节标注规则听起来简单实际上最容易埋雷。第一版标注文件我推荐用 TSV 而不是直接在图片文件名里塞标签方便后期追加别名和统计img_path label_id unicode glyphs/0001.png 120 未 glyphs/0002.png 121 末label_id 必须是稳定的内部 ID不能直接拿 Unicode 码位当分类 ID。原因很直接古籍里大量存在异体字、避讳字、俗字同一个规范字可能对应好几个字形。如果直接按 Unicode 拆类等于把一个语义概念拆成多个类别让模型学好几遍还会互相打架。我一般会在项目里维护一份类别映射表用 JSON 管理字形和规范字之间的关系{ 120: {unicode: 672A, variants: [F0XX, F1YY]}, 121: {unicode: 672B} }标注规则第一条第一版只合并肉眼可确认的绝对同形字异体字归并放到第二版再做必须有文献学背景的人校对。自动化归并不可靠别图快。第二条要预留一个「未登录字」类别专门收那些出现次数极少、暂时没法稳定归档的字形。推理时这个类的输出不要直接当作结果而是进入低置信度复核流程。3.3 六种针对性增强模拟古籍才有的合理失真增强的目标不是造出花哨图片而是模拟古籍扫描件里的合理失真纸色不匀、墨迹洇染、纸张不平、切分偏移和版面倾斜。现代 OCR 常用增强里那种大幅旋转和随机裁剪在这里要降级使用因为古籍字本身是垂直的旋转超过 5° 反而会让模型去学「歪」这个特征。import cv2 import numpy as np def elastic_distort(img, alpha18, sigma4): 弹性形变模拟纸张局部不平整导致的笔画扭曲。 h, w img.shape[:2] dx cv2.GaussianBlur( (np.random.rand(h, w) * 2 - 1) * alpha, (0, 0), sigma) dy cv2.GaussianBlur( (np.random.rand(h, w) * 2 - 1) * alpha, (0, 0), sigma) xmap, ymap np.meshgrid(np.arange(w), np.arange(h)) map_x (xmap dx).astype(np.float32) map_y (ymap dy).astype(np.float32) return cv2.remap(img, map_x, map_y, interpolationcv2.INTER_LINEAR, borderModecv2.BORDER_REPLICATE) def augment_glyph(img, texture_patchNone): aug img.copy() h, w aug.shape[:2] # 1) 小幅旋转 ±3° angle np.random.uniform(-3, 3) matrix cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) aug cv2.warpAffine(aug, matrix, (w, h), borderValue255) # 2) 弹性形变一半概率触发 if np.random.rand() 0.5: aug elastic_distort(aug, alpha18, sigma4) # 3) 笔画粗细扰动膨胀模拟墨迹洇染腐蚀模拟淡墨断笔 kernel np.ones((2, 2), np.uint8) if np.random.rand() 0.3: aug cv2.dilate(aug, kernel, iterations1) elif np.random.rand() 0.3: aug cv2.erode(aug, kernel, iterations1) # 4) 纸张纹理叠加alpha 控制纸色渗透强度 if texture_patch is not None: aug cv2.addWeighted(texture_patch, 0.15, aug, 0.85, 0) # 5) 对比度/亮度抖动 alpha np.random.uniform(0.8, 1.2) beta np.random.uniform(-10, 10) aug cv2.convertScaleAbs(aug, alphaalpha, betabeta) # 6) 切分偏移模拟随机平移 2-4 像素 dx np.random.randint(-3, 4) dy np.random.randint(-3, 4) matrix np.float32([[1, 0, dx], [0, 1, dy]]) aug cv2.warpAffine(aug, matrix, (w, h), borderValue255) return aug为什么把切分偏移模拟放到增强里因为实际版面切分不可能每次都把字摆在正中央切残、切偏是常态。如果训练时模型只见过居中的字形推理时见到偏移几个像素的字性能会打折扣。参数上弹性形变的 alpha18、sigma4 是一组常用起手值笔画不会显著变形但纸张不平整的手感能出来旋转别超过 ±3°古籍字形本身是垂直的旋转过大会把「歪」当成判别特征。4. 跑通古籍汉字分类器ResNet-18 最小训练代码与 3 个必调参数模型实现我用 PyTorch这是这类任务里最容易上手、也最容易改结构的框架。开始别折腾复杂网络ResNet-18 足够支撑第一版流程。下面这段是能直接跑的最小脚本假设你已经准备好了(img_path, label_id)列表。4.1 最小训练脚本从 DataLoader 到 checkpointimport torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import models import cv2 import numpy as np class GlyphDataset(Dataset): def __init__(self, pairs): self.pairs pairs # [(img_path, label_id), ...] def __len__(self): return len(self.pairs) def __getitem__(self, idx): path, label self.pairs[idx] img cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 已是 96x96 img img.astype(np.float32) / 255.0 img torch.from_numpy(img).unsqueeze(0) # (1, 96, 96) return img, torch.tensor(label, dtypetorch.long) def build_model(num_classes): model models.resnet18(weightsNone) model.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) model.fc nn.Linear(512, num_classes) return model model build_model(num_classes3000) optimizer torch.optim.AdamW(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() train_ds GlyphDataset(train_pairs) loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4) for epoch in range(30): model.train() total_loss 0 for img, label in loader: pred model(img) loss criterion(pred, label) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * img.size(0) avg_loss total_loss / len(train_ds) print(epoch, avg_loss) if epoch % 5 0: torch.save({model: model.state_dict(), optimizer: optimizer.state_dict(), epoch: epoch}, glyph_cnn.pt)代码里有几个地方是特意这样写的。weightsNone表示不加载 ImageNet 预训练权重古籍字形和 ImageNet 类别分布差异太大预训练权重帮助有限反而可能迁移不相干的纹理偏置。第一层改成单通道输入后训练速度会比三通道明显提升。最后的 fc 输出 3000 类如果你的类别数上万这一层参数量会非常可观可以按第 5 章的方案处理。这段脚本里没写验证集评估因为字符级验证集的构建本身就容易出错。我的习惯是先看训练 loss 的下降趋势是否稳定再单独维护一个形近字验证集比如把「未/末」「己/已/巳」「戊/戌/戍」这类高危组合单独抽出来看混淆矩阵而不是只盯着整体准确率。4.2 三个必调参数base_lr、batch_size 与标签平滑训练古籍汉字分类器最值得花时间调的是下面这三个参数参数推荐初始值调整方向base_lr1e-3loss 震荡不降时降到 3e-4平台期用余弦退火到 1e-5batch_size64小于 16 时 BN 统计噪声很大优先保证类别均衡采样label_smoothing0.1标注噪声高于平均水平时提到 0.15base_lr 用 AdamW 配合 1e-3 起步。我不建议一上来就用极小的学习率类别多、样本分布不均时小学习率反而容易让模型卡在局部。batch_size 对 BN 层影响很大批量小于 16 时模型输出会明显抖动如果生僻字类别多每个 batch 里可能完全看不到某些类这时要用WeightedRandomSampler按样本数的倒数采样而不是把整个数据集 shuffle 了事。label_smoothing 是被很多人忽略的参数。古籍标注里存在异体字误标、切分十字偏差真实标签噪声比现代数据集高nn.CrossEntropyLoss(label_smoothing0.1)能压住模型在标注错误的样本上过度自信。它不会消除噪声但会让输出概率分布更平滑推理阶段用置信度挑可疑字时更好用。4.3 生僻字样本太少Focal Loss 与类别重采样怎么选古籍数据集天然极端不平衡常用字「之」「不」可能有几千张生僻字只有十几张还有异体字形只在某一页出现过一次。对策通常有两类重采样和重塑损失函数。如果某个类样本数少于 20直接把它加进WeightedRandomSampler按样本数的倒数计算权重保证每个 epoch 都能见到这些稀有类几次。如果类别整体是长尾分布Focal Loss 更省心。class FocalLoss(nn.Module): def __init__(self, gamma2.0, alphaNone): super().__init__() self.gamma gamma self.alpha alpha def forward(self, logits, labels): ce nn.functional.cross_entropy( logits, labels, reductionnone) pt torch.exp(-ce) loss (1 - pt) ** self.gamma * ce if self.alpha is not None: loss loss * self.alpha[labels] return loss.mean()gamma2 是 Focal Loss 最常用的设置它会把难分类样本的 loss 放大让模型把注意力放在那些字形模糊、切割有缺陷的样本上。alpha 可以按 sklearn 的compute_class_weight算出每个类别的权重。实际使用中我的经验是重采样在前 20 个 epoch 保证稀有类能被看到最后 5 个 epoch 换 Focal Loss 做难例磨炼效果比单用一种更稳。如果类别数超过 5000先把样本量少于 30 的类合并进「未登录字」类避免分类器在这些类别上反复瞎猜。5. 古籍汉字识别避坑指南5 个「现象-原因-解法」实锤记录这些坑是模型训练跑通之后才会遇到的藏在数据管线和前后处理里不把整套流程过一遍很难提前预判。每一条我都按现象、原因、解决的顺序拆开讲值得放在手边反复对照。5.1 同一个字被标成两个类别异体字形干扰分类器现象训练集里「為」和「为」是两个独立类别模型在它们之间反复混淆验证集 top-1 卡在 85% 上不去。原因古籍里同一个字存在多种写法比如简体、繁体、异体混在同一套数据里。如果直接按 Unicode 码位建类等于把一个语义概念拆成两个甚至多个类别让模型学两遍类别之间共享字形又互相干扰。解决给每个语义字维护一张字形别名表。第一版训练先不合并等模型跑出混淆矩阵后把互为高频混淆的类别挑出来再交给文献学背景的人确认归并。这个环节不能全自动别图快合错字对分类器的影响比不合并更大。5.2 单字测试集准确率 98%整页识别率却不到 70%现象单独评估字符级模型时 top-1 很高一旦跑整页流程识别率掉得像换了个模型。原因切分环节把字切出了偏移、切残或者把两个字粘连在一起。模型训练时看到的是居中好的 96×96 字形推理时喂进来的却是偏左、截断的版本数据分布不一致性能自然崩。这是字符级方案最隐蔽的坑因为问题出在 CNN 之外。解决训练时加入切分偏移模拟增强把输入图像随机平移 2-4 像素偶尔再做一次 1-2 像素缩放。同时对切分模块做回归测试抽 200 个测试页统计「切残字」和「粘连字」比例超过 1% 就先调切分参数不要急着调 CNN。模型是无辜的数据没喂对。5.3 纸张噪点被模型当成笔画增强方向搞反了现象模型在泛黄噪点多的区域输出概率很高但实际字形并没那么明显的笔画特征打开中间层特征图发现模型在噪声纹理上建立了强响应。原因增强阶段往训练集里拼命加高斯噪声模型最后学到的表征建立在噪点纹理上。CNN 对高频噪声非常敏感古籍纸面本身又满是颗粒感这个坑在古籍场景里比普通文档严重得多。解决别加高斯噪声改用形态学开运算模拟纸面颗粒——先腐蚀再膨胀颗粒感能被削掉一层。再加上真实纸张纹理叠加纹理 patch 必须和古籍纸色一致。整体原则是让前景笔画更突出而不是让噪声更突出。5.4 竖排文本的行列恢复错乱模型输出顺序不是阅读顺序现象整页识别结果按模型输出数组顺序拼接竖排古籍的内容变成从左到右横排被读出来了断行断句全错。原因版面切出来后字符框坐标是从左到右扫描得到的。古籍竖排的阅读顺序是从右到左按列再在列内从上到下如果不按坐标重新排序直接拼数组就是在用横向扫描顺序读竖排文本。解决用 x 中心坐标聚类分列而不是直接按 x 排序聚类后对每一列按 y 坐标排序输出。同一列内如果有眉批、夹注再按字形高度做二次分类。这个还原流程我在第 6 章会给出可直接用的代码。5.5 类别清单上千全连接层把显存撑爆现象类别数从 3000 涨到 8000 之后训练直接 OOM单卡 V100 也吃紧。原因ResNet-18 最后的 fc 层是 512 维输入、8000 类输出光这一层就是 512×8000≈410 万参数优化器状态再占一份显存压力全压在全连接层上。解决最简单的办法是把最后一层换成 GAP 后接 1×1 卷积或者用两层 MLP 过渡减少全连接层参数量。另一种工程上更好落地的方案是拆类别常用字前 1500 类单独一个模型生僻字放另一个模型推理时第一个模型输出熵高说明这个字大概率是生僻字再去请求第二个模型。显存受限时这个方案比硬塞大模型靠谱得多。6. 从单字模型到整页文本置信度过滤、列序复原与人工复核兜底6.1 用列坐标把字符序列还原成竖排文本单字模型输出的是一组(x_center, y_center, char_id, prob)。要还原成可读文本先按 x_center 聚类分列再在每列内按 y_center 排序最后从右往左拼接列。聚类不需要上复杂算法古籍版式的列间距相对固定按列间距中位数做阈值即可。import numpy as np def order_predictions(preds, col_gap60): # preds: [(x, y, char, prob), ...] preds sorted(preds, keylambda p: -p[0]) # 按 x 从右往左 cols [] cur [preds[0]] for p in preds[1:]: if cur[0][0] - p[0] col_gap: cur.append(p) else: cols.append(cur) cur [p] if cur: cols.append(cur) out [] for col in cols: col sorted(col, keylambda p: p[1]) # 列内按 y 从上到下 out.extend(col) return outcol_gap 参数以像素为单位先按扫描分辨率换算通常取列间距中位数的一半。输出时不要把字符直接拼成字符串而是保留「列、行、字、概率」的元数据这样后续断句和复核都有据可查。6.2 用置信度兜底不追求一次性全对当模型输出概率低于阈值时不要硬输出结果。阈值取 0.6-0.7 比较稳低于阈值就把这个字连同截图单独导出成一个待复核清单标注工具显示 top-5 候选让复核人员一次点选即可。古籍生僻字多与其让模型硬猜不如把人力花在确认少量低置信度字上。「模型给候选、人工点选」比纯自动识别更实际也比纯人工快一个数量级。我自己的习惯是每换一批古籍数据先跑 10 个 epoch 看验证集在哪些类上震荡再去翻标注表十次里有八次是字形归并或切分问题而不是网络结构不好。有一次夜间跑完训练第二天看准确率不错但「未」和「末」还是互相混最后才发现是切字时横画长度被边界裁掉了。从此我在验证集里固定保留一组形近字子集每次训练完都单独看「未末巳已」这类高危组合的混淆矩阵比只看整体准确率有用得多。这套流程走下来古籍汉字识别项目会稳很多希望帮到你。本文还有配套的精品资源点击获取
返回列表