ARTICLE DETAIL

资讯详情

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

中文图文检索实战:用Chinese-CLIP双塔模型搭建高效检索系统

中文图文检索实战:用Chinese-CLIP双塔模型搭建高效检索系统 简介面向计算机视觉课程设计与期末大作业场景提供一套基于Chinese-CLIP的图文检索系统Python源码及文档说明适合需要快速搭建完整项目的本科生也适合作为多模态检索方向的学习范例。系统支持文本到图像与图像到文本的双向检索涵盖模型部署、训练评估、界面交互等主要模块代码注释齐全部署简单使用门槛低。压缩包共59个文件包含40个Python脚本、9个JSON配置、编译缓存与Markdown说明文档app.py控制系统主流程cn_clip目录封装预处理与推理评估逻辑utils.py与text2image.py等工具脚本支撑完整检索链路目录结构清晰便于二次开发。资源包仅543KB已有177人学习。借助这套资料可快速掌握多模态检索应用实现思路界面美观、功能完善稍作调整即可作为期末高分作业提交。1. 中文图文检索大作业为什么我选Chinese-CLIP而不是自训双塔做计算机视觉大作业遇到图文检索任务第一反应往往是“先训一个模型再说”。但课程设计的时间、显存和答辩压力都摆在那里数据量小、训练周期短、还得在演示时稳定复现。我的做法是直接用开源的Chinese-CLIP做双塔编码器不碰训练把工作量集中在数据准备、特征建库和检索接口上。这套方案对中文Query和中文图片描述的匹配效果比拿英文CLIP硬套好一个量级而且全程Python可读、可改、可截图写报告。适合计算机视觉入门阶段的课程设计、毕业设计预研以及想快速搭一个中文图文检索Demo的开发者。你想做成“输入一句话从图片库里找到对应图片”这样的系统用这个方向是最短路径。2. 双塔检索架构与Chinese-CLIP选型精度、成本和课程设计档位2.1 从CLIP到Chinese-CLIP双塔为何成为图文检索缺省方案图文检索系统的核心问题是让“一段中文描述”和“一张图片”在同一个向量空间里可比。早年的做法是用图像分类网络抽标签、目标检测抽物体名再用关键词倒排索引去匹配这套东西在中文场景下很别扭“一只猫坐在沙发上”和标注“猫、沙发”能撞上换一句“猫趴在家里的椅子上”就检索不到。问题出在标签空间太小描述里的语义在标签之间被直接切断了。CLIP系列解决的就是这件事图像塔和文本塔通过对比学习把同一个batch里的正样本对拉近、负样本对推远最后让两座塔输出的向量可以直接做点积算相似度。推理阶段不需要任何训练标签对图片跑一次encode_image、对文字跑一次encode_text两边输出就在同一个空间里。这种双塔结构成为工业界图文检索的缺省方案原因是它的在线召回成本很低图片特征全部离线算好存进索引线上只有文本塔走一次前向候选集再大也能在几十毫秒内完成召回。Chinese-CLIP是阿里在CLIP基础上做的中文预训练版本。英文CLIP在中文Query上基本等于黑匣子先翻译再匹配翻译丢一层语义检索分就失真。Chinese-CLIP直接用中文语料做对比学习文本塔换成了中文BERT图像塔仍是ViT输出维度对齐接口几乎无感替换。对课程设计来说这就是“少训练、高效果”的平衡点。为什么不自己训练双塔对比学习的损失函数大家都熟但真正决定双塔效果的是Infonce温度系数、batch_size和负样本质量。温度系数控制相似度分布的尖锐程度batch_size决定一个batch里能看见多少负样本。课程设计一般只有几百张图一个batch里撑死几十个负样本训练出来的双塔连“猫”和“狗”都分不利索而Chinese-CLIP预训练语料覆盖海量中文互联网图文对直接拿来用效果等于白捡。2.2 选型边界什么时候能抄作业什么时候要换架构先说清楚这套方案的适用范围。它成立有三个条件第一任务确确实实是“图文双向检索”不是图文生成或视觉问答第二文本侧是中文描述长度在几十个字以内第三图片规模在几千到几十万张之间特征库单机内存能装下。超出这些条件就不能直接抄要么换特征提取器要么上向量数据库。对比三条技术路线的性价比方案训练成本中文效果推理成本课设适配度自训双塔 ResNetBERT高需要百万级图文对依赖语料质量低不推荐答辩难交代迭代过程英文CLIP零差中文语义被打散低不推荐演示效果没法看Chinese-CLIP零好低推荐效果和代码都可解释“自训双塔”最大的坑是数据集规模。图文对比学习需要至少百万级图文对才能在向量空间里形成稳定结构课程设计普遍只有几百张图训出来的模型相似度分数没有区分度答辩时Top10的分值可能全部粘在0.95附近老师一问就露馅。如果项目要求“必须展示自己训练的模型”可以保留Chinese-CLIP做特征底座在它输出的512维特征上训练一个浅层Linear Head做下游排序既有训练代码又有预训练底座的效果兜底。重量级也有梯度。课程设计默认选chinese-clip-vit-base-patch16理由很实际显存占用约2GB普通笔记本无独显也能用CPU抽特征只是慢一些如果你想展示大模型效果且机器有16GB以上显存可以换chinese-clip-vit-large-patch14如果目标机器是纯CPU且内存小chinese-clip-rn50能跑但中文语义效果明显降一档。我的建议是除非机器实在跑不动否则不要用ResNet-50的版本ViT-B/16在中文描述上的泛化能力是值那几百MB权重体积的。2.3 系统文件结构设计源码、权重、特征库各放哪课程设计最容易翻车的不是模型是项目目录。老师打开你的Python项目希望从README一眼看到“入口脚本、数据、特征库、结果示例”。如果文件夹里塞满final_final_v2.py印象分直接掉一半。我一般这样组织chinese_clip_retrieval/ ├── README.md ├── requirements.txt ├── config.py # 全局参数模型名、路径、batch_size ├── data/ │ ├── images/ # 待检索图片 │ ├── annotations.json # 图文对标注 │ └── queries.txt # 检索测试查询 ├── features/ │ ├── image_feats.npy │ └── text_feats.npy ├── scripts/ │ ├── extract_features.py # 抽取特征 │ ├── build_index.py # 建索引 │ └── run_server.py # Flask检索服务 ├── evaluate/ │ └── recall_at_k.py # 评估脚本 └── docs/ └── 报告提纲.md权重不放进源码目录因为体积大老师也不会重新下载。在config.py里配置model_name OFA-Sys/chinese-clip-vit-base-patch16代码第一次运行时自动去HuggingFace拉权重如果答辩现场没有网络提前用snapshot_download把权重拉到本地cache_dir再把config.py里的路径指过去。这个细节能救一场答辩。要说明的是features/目录下的.npy文件是中间产物不需要进git。课程设计交源码时把特征库生成脚本和README里的一键重建命令写好老师在自己机器上跑一遍就能复现比直接塞一个几百MB的npy文件更规范。3. Python环境搭建与数据准备把中文图文对整理成可检索的JSON3.1 Python环境与依赖安装open_clip、transformers和PyTorch的搭配环境搭建这一步我踩过好几次版本坑。Python版本建议直接用3.10配合conda创建独立虚拟环境不要动系统自带的Python不然装PyTorch时很容易污染基础环境。跨平台也没问题Windows、macOS、Linux的安装命令都一样唯一区别是PyTorch的CUDA版本选择。conda create -n clip_retrieval python3.10 conda activate clip_retrieval pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers open_clip_torch pillow numpy pip install flask gradio如果你是纯CPU环境第二行的--index-url要改成https://download.pytorch.org/whl/cpu不要照抄CUDA版本。open_clip_torch这个包名容易记错注意是open_clip_torch而不是open_clip装错会得到一个同名但完全不同的包。装完之后跑一句验证python -c import open_clip, torch, transformers, PIL; print(env ok)这段命令做了两件事验证三个核心库都能被导入同时确认当前conda环境是你要用的那个。在vscode里调试时最常遇到的“No module named open_clip”就是因为vscode的Python解释器选到了base环境而不是clip_retrieval在vscode右下角切换解释器即可。git bash和Windows PowerShell下执行conda activate的写法不同如果你用PowerShell需要先执行conda init powershell再重开终端。3.2 自建数据集与标准标注文件JSON比目录命名更省事动手抽特征之前先把数据组织好。图文检索的数据最小单位不是一个文件名而是一条image_id file_name caption的图文对。哪怕你只有20张图也建议按这个结构组织因为后面所有代码——抽特征、建索引、评估——都从annotations.json这一个文件读数据绝不散落在各处。import json, os from PIL import Image img_dir data/images annos [] for name in sorted(os.listdir(img_dir)): path os.path.join(img_dir, name) try: im Image.open(path) im.verify() except Exception: print(f[skip] broken image: {name}) continue annos.append({ image_id: os.path.splitext(name)[0], file_name: name, caption: 一只猫坐在沙发上 # 这里换成真实的中文描述 }) with open(data/annotations.json, w, encodingutf-8) as f: json.dump(annos, f, ensure_asciiFalse, indent2) print(fdone, {len(annos)} pairs)这段脚本的逻辑很简单遍历图片目录用Image.verify()检查文件完整性损坏的跳过并在终端打印出来每张图生成一条图文对最后写入annotations.json。ensure_asciiFalse是必须的否则中文描述会被转成\uXXXX形式自己看没问题但老师打开文件看到一屏Unicode转义会很难受。那caption怎么来如果你用公开数据集Flickr8k-CN和COCO-CN都有现成的中文描述标注下载后把官方标注字段映射到这个JSON结构就行不需要人工写。如果只是课堂演示级别我一般会人工写20到30条覆盖“物体位置颜色动作”四种典型Query比如“白色的小狗趴在红色地毯上”。注意caption不要全部写成“一张XX的图片”这种模板化描述会让检索失去语义区分度。3.3 图片清洗损坏文件、透明通道和超大分辨率的三个坑数据质量决定检索结果上限。模型再强图片解码失败、格式混杂、分辨率悬殊都会在特征抽取阶段冒出来。第一个坑是坏图下载的数据集里总有一两张截断的JPEGImage.open不报错但im.verify()会炸。前面代码已经处理了。第二个坑是透明通道PNG带Alpha通道Image.open出来是RGBA四通道直接喂给预处理函数会报通道数错误统一convert(RGB)。第三个坑是分辨率过大一张8000像素宽的照片解码会吃掉几百MB内存批量抽特征的时候很容易OOM。import glob from PIL import Image bad [] for path in glob.glob(data/images/*): try: im Image.open(path).convert(RGB) if im.width 224 or im.height 224: bad.append((path, too small)) if max(im.width, im.height) 512: im.thumbnail((512, 512)) if not path.lower().endswith((.jpg, .jpeg)): im.save(path, JPEG, quality90) except Exception as e: bad.append((path, str(e))) print(bad files:, bad)这段脚本做了四件事统一转RGB、过滤小于224像素的小图、把超大图缩到最长边512、把PNG/BMP统一转成JPEG。这里thumbnail((512, 512))是按比例缩放的不会拉伸变形。为什么不直接缩到224因为Chinese-CLIP预处理里会再裁一次中心区域提前缩到512保留更多上下文裁切后仍然清晰直接缩到224会损失细节。如果你的数据本身全是高清图这一步可以省掉但课程设计的图片往往来源杂乱清洗脚本能帮你提前暴露很多问题。4. Python特征抽取与索引构建Chinese-CLIP核心实现和三个关键参数4.1 加载Chinese-CLIP模型预处理必须和训练时保持一致模型加载是整个项目的核心环节。用open_clip库可以一行完成模型和预处理器的搭建这是Chinese-CLIP官方推荐的路径比手动拼transformers的组件省事得多。import open_clip import torch device cuda if torch.cuda.is_available() else cpu model_name OFA-Sys/chinese-clip-vit-base-patch16 model, _, preprocess open_clip.create_model_and_transforms( model_name, pretrainedhf-hub:OFA-Sys/chinese-clip-vit-base-patch16, cache_dir./weights ) tokenizer open_clip.get_tokenizer(model_name) model.to(device).eval() print(model loaded, output dim:, model.text_projection.shape)这段代码的关键点有三个。第一pretrainedhf-hub:...这个写法表示权重从HuggingFace上torch hub格式的权重文件加载不是用transformers的from_pretrained两者机制不同如果你手头已经下载了本地的pytorch_model.bin可以直接把pretrained改成本地路径。第二create_model_and_transforms返回三个值中间那个是训练时用的预处理我们用不到用下划线占位第三个preprocess是推理时要用的它内部包含了缩放、中心裁剪和归一化。第三text_projection的shape是[512, 512]它是文本塔最后的投影层能打印出来说明模型加载完整。预处理参数必须和训练时保持一致否则特征空间就错位了。Chinese-CLIP图像侧使用的归一化均值和标准差是固定的预处理项值输入尺寸224 x 224归一化均值(0.48145466, 0.4578275, 0.40821073)归一化标准差(0.26862954, 0.26130258, 0.27577711)这个参数如果你自己手动处理图片必须原样抄进去。用open_clip.create_model_and_transforms返回的preprocess则不需要关心因为官方已经封装好了。另外要注意的是中文CLIP的tokenizer是基于中文字符的不是英文词级别的所以不需要在Query上做分词直接原样传入即可。4.2 图像特征与文本特征抽取L2归一化是检索分数可信的前提特征抽取的代码可以拆成两个函数分别处理图像和文本。两者结构相同先做预处理再走模型编码最后做L2归一化。import numpy as np from PIL import Image import torch def extract_image_features(image_paths, model, preprocess, device, batch_size16): feats [] for i in range(0, len(image_paths), batch_size): batch_paths image_paths[i:i batch_size] images [preprocess(Image.open(p).convert(RGB)) for p in batch_paths] images torch.stack(images).to(device) with torch.no_grad(): out model.encode_image(images) out out / out.norm(dim-1, keepdimTrue) feats.append(out.cpu().numpy()) return np.concatenate(feats, axis0).astype(np.float32)逻辑说明这个函数按batch_size分批处理图片避免一次性把几百张图全部堆进显存。每一批走完encode_image后对最后一维做L2归一化这一步直接决定后续相似度计算的结果是否可信。如果省掉归一化特征向量长度不一点积结果会偏向长向量检索结果会被高曝光图片带偏。最后把结果转成float32存numpy数组比默认的float64省一半内存。文本侧的函数更短def extract_text_features(captions, tokenizer, model, device, max_length52): tokens tokenizer(captions, context_lengthmax_length).to(device) with torch.no_grad(): out model.encode_text(tokens) out out / out.norm(dim-1, keepdimTrue) return out.cpu().numpy().astype(np.float32)这里context_length52是Chinese-CLIP的文本序列上限英文CLIP是77中文按字符计算52个汉字已经能覆盖绝大多数描述场景。如果caption超过52个字符tokenizer会自动截断后面的字直接丢弃。所以写caption时不要写又长又绕的句子短描述反而更贴合训练分布。批量传入多句话时tokenizer内部会自动做padding对齐不需要手动处理。4.3 索引与相似度计算单机numpy足够FAISS留给万级规模特征抽完之后检索就变成了纯向量计算。因为之前已经做了L2归一化向量点积的结果就是余弦相似度可以直接用numpy矩阵乘法def search_by_text(query_feat, image_feats, top_k5): scores image_feats query_feat idx np.argsort(scores)[::-1][:top_k] return idx, scores[idx]这段代码逻辑很直白image_feats是[N, 512]的矩阵query_feat是[512]的向量矩阵乘法的结果[N]就是每张图与Query的相似度。argsort从小到大排序后取反得到从大到小的序号再切前K个就是检索结果。这里没有用scipy的distance函数因为两两算距离在100万张图上非常慢矩阵乘法是向量化计算速度是前者的几十倍。课程设计阶段用numpy就足够了。10万张图的特征矩阵约等于100000 x 512 x 4B约200MB内存单机能轻松加载。只有当图片数量到几十万、要求毫秒级响应的时候才值得换FAISS的IndexFlatIP——它是同样的点积逻辑但用SIMD指令和多线程加速构建索引的API几乎等价。我建议课程设计不要上FAISS理由很简单numpy版本可读性好老师一看就懂FAISS会引入额外依赖和C编译问题对答辩来说是负资产。建库主流程的代码集中在一个脚本里import json, os, numpy as np annotations json.load(open(data/annotations.json, encodingutf-8)) image_paths [os.path.join(data/images, a[file_name]) for a in annotations] captions [a[caption] for a in annotations] img_feats extract_image_features(image_paths, model, preprocess, device) txt_feats extract_text_features(captions, tokenizer, model, device) np.save(features/image_feats.npy, img_feats) np.save(features/text_feats.npy, txt_feats) np.save(features/image_ids.npy, np.array([a[image_id] for a in annotations]))这步跑完后features/目录下有三个文件图片特征、文本特征、图片ID列表。检索时加载这三个文件就可以完全脱离图片目录独立运行了。这里image_ids.npy的作用是把检索结果的索引映射回文件名annotations.json里还有对应的中文描述前端展示时需要一起用。5. 图文检索系统踩坑实录让课程设计翻车的五个排查案例5.1 中文Query返回的Top1毫不相关现象是检索“一只白猫躺在沙发上”返回的第一张图却是厨房或街景。这个坑我在第一次跑通时也中过原因是模型加载时model_name写成了英文CLIP的ViT-B/32名字里没有chinese-clip前缀加载出来的权重是英文CLIP中文Query进去自然没反应。另一类原因是文本超长被截断描述或Query超过52个字符后后半句被tokenizer静默丢弃跑出来的特征只覆盖了前半句语义。解决方法是先打印模型名确认前缀open_clip.get_tokenizer(model_name)的返回值里能看到具体模型标识。然后检查tokenizer截断把Query传进去后打印input_ids的shape如果末尾出现截断标记就按语义裁剪Query把最关键的名词和位置词放在前面。最后还要确认权重确实加载了不是随机初始化随机权重会让所有相似度分数挤在一起这也是下面要说的另一个坑。5.2 相似度全部挤在0.999附近检索结果没有区分度这个现象很典型不管Query换成什么Top5的相似度都接近1.0名次换来换去但图片都是同一批。原因通常是模型权重没有加载成功create_model_and_transforms返回的模型是随机初始化的。随机权重有个特性向量模长接近定值归一化后两两角度都很小点积自然全部偏高。另一个常见原因是数据本身单一比如图片集里全是室内家具Query“海边日落”检索出来也全是室内因为根本没有相关图片。排查方法很直接打印几行中间结果img_feats np.load(features/image_feats.npy) print(mean:, img_feats.mean(), std:, img_feats.std()) print(pairwise sim sample:, img_feats[:3] img_feats[:3].T)正常特征的相似度应该分布在[-0.2, 0.9]之间有一定起伏如果三张完全不同的图之间的相似度都超过0.99先检查权重加载再考虑数据多样性。权重加载问题解决后记得重新抽取特征因为之前存下来的npy文件是随机权重算的。5.3 图片一多就“内存不足”特征库构建到一半被杀死课程设计的图片集如果用到几万张抽特征时经常碰到内存爆掉。这里有两层原因第一层是批量抽特征时图片解码全部在内存里进行如果batch_size设成64每张图解码后占几十MB一批下来就是几个GB第二层是最后把特征concatenate成一个大numpy数组时中间过程会有两份拷贝存在。解决思路是缩减批大小和减少拷贝。图像抽特征的batch_size建议控制在16以内图片解码时thumbnail到512已经减少了很多内存占用。文本侧批大小可以放宽到64因为文本向量轻得多。如果特征库确实超过10万条用np.memmap把npy文件映射到磁盘读取时按块加载而不是一次性载入内存这是最稳妥的做法代价只是检索时磁盘IO稍微慢一点但课程设计的数据量一般用不上这一步。5.4 检索接口响应极慢单次搜索要等两秒这个问题多出现在Flask服务里因为每次请求都重新加载一次image_feats.npy一个200MB的文件在机械硬盘上读取就是几百毫秒到一秒再加上模型编码Query的时间整体就慢了。解决方法是把特征库和模型都放到全局变量里服务启动时加载一次后续请求只做矩阵乘法和编码。from flask import Flask, request, jsonify import numpy as np import open_clip, torch app Flask(__name__) # 启动时加载一次 image_feats np.load(features/image_feats.npy) image_ids np.load(features/image_ids.npy, allow_pickleTrue) model, _, preprocess open_clip.create_model_and_transforms(...) tokenizer open_clip.get_tokenizer(model_name) app.route(/api/search, methods[POST]) def search(): data request.get_json() query data[query] tokens tokenizer([query], context_length52).to(device) with torch.no_grad(): query_feat model.encode_text(tokens) query_feat query_feat / query_feat.norm(dim-1, keepdimTrue) scores image_feats query_feat.cpu().numpy().T idx np.argsort(scores, axis0)[::-1][:5] results [{image_id: image_ids[i], score: float(scores[i])} for i in idx] return jsonify({results: results})Flask默认是单线程模式多个同学同时访问时也会排队。在app.run()里加threadedTrue参数可以缓解一部分并发压力。如果Query是长文本编码耗时主要在网络模型上CPU下走一次前向在100ms级别可以接受如果要求更快把模型换成rn50版本是最后的选择。5.5 答辩现场模型权重下载失败加载报错404或连接超时这个属于环境问题但发生概率不低。open_clip从HuggingFace拉权重时会访问外网某些网络环境下会超时。解决方法是提前把权重下载到本地答辩时用本地路径加载。可以用HuggingFace官方下载脚本或者直接用snapshot_download拉取整个仓库。python -c from huggingface_hub import snapshot_download; snapshot_download(OFA-Sys/chinese-clip-vit-base-patch16, local_dir./weights)下载完成后./weights目录下会有一个完整模型。加载时把pretrained参数指向本地目录即可。如果网络条件实在不好也可以设置HF_ENDPOINThttps://hf-mirror.com环境变量这是国内常用的HuggingFace镜像站拉取权重会快很多。答辩前一定要在离线环境测试一次完整流程用--offline方式启动Python确认权重已经完全本地化不然现场翻车没有后悔药。6. 答辩前最后一步RecallK评估与源码文档说明的落地写法6.1 用60行Python算清楚RecallK指标答辩时老师问“效果怎么样”最有力的回答是直接报数字而不是“看起来还挺准”。RecallK就是图文检索领域最常用的指标给定一条Query返回K张候选图正确结果是否出现在候选里。对于课程设计我建议算两个方向——文本检索图片和图片检索文本两个方向分别报Recall1、5、10。import numpy as np def recall_at_k(query_feats, db_feats, query_labels, db_labels, k5): scores query_feats db_feats.T # [Q, N] hits 0 for i in range(len(query_feats)): topk_idx np.argsort(scores[i])[::-1][:k] if query_labels[i] in db_labels[topk_idx]: hits 1 return hits / len(query_feats) # 示例调用 img_feats np.load(features/image_feats.npy) txt_feats np.load(features/text_feats.npy) img_ids np.load(features/image_ids.npy, allow_pickleTrue) # 文本检索图片 r1 recall_at_k(txt_feats, img_feats, img_ids, img_ids, k1) r5 recall_at_k(txt_feats, img_feats, img_ids, img_ids, k5) print(fTR-IR Recall1: {r1:.3f}, Recall5: {r5:.3f})这个脚本的逻辑不复杂scores矩阵的每一行是某条Query与所有数据库样本的相似度取TopK后检查正确标签是否在候选里。课程设计的小数据集上10的分数通常明显高于55高于1。如果1是0但你观察检索结果觉得“差不多”那很可能是数据标注错了比如同一张图的image_id在Query和数据库里不一致先检查image_ids.npy的对齐。6.2 文档说明怎么写才能让老师改起来不头疼源码文档说明不是写论文是写给“下一个能跑通的人”看的。我见过很多大作业的README只有安装步骤换一台机器就完全复现不了。建议固定四个部分文档块内容环境说明Python版本、CUDA版本、依赖安装命令数据说明图片来源、标注格式、caption生成方式运行步骤抽特征 - 建索引 - 启动服务 - 测试Query效果报告RecallK结果、困难样本截图源码注释也有讲究。extract_features.py里的batch_size、context_length这些参数旁边一定要写注释因为老师很可能会改参数重跑。每个脚本开头写一行说明注释比如“本脚本负责把data/images目录下的图片抽取特征并保存到features/”。这样改起来有边界感不会出现“改了这里结果全变了但不知道哪里变了”的情况。答辩演示的准备脚本也有技巧提前准备好5条测试Query两条高置信度、两条中等难度、一条模糊描述。高置信度展示系统能力模糊描述展示模型泛化性这样的演示流程比临时输入文字流畅得多。我自己做这套检索系统时最后悔的一件事就是把image_feats.npy放在Git里换机器时新旧文件冲突折腾了半小时后来删掉中间产物、用脚本一键重建反而更省心。希望你少走这个弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表