诗歌接龙:用Python实现古诗续写的NLP项目全解析
简介:面向NLP入门与Python实践者的诗歌接龙项目完整代码包,聚焦中文分词、词性标注、拼音转换、押韵规则匹配及网络爬虫等核心环节,适合希望通过完整实战案例理解自然语言处理流程的学生与开发者。压缩包共17个文件,主要包含Python源码、jieba/pypinyin等库的依赖说明、预训练数据文件、可执行程序、txt说明文档及docx详细设计文档,其中docx对项目背景、模块设计与调用流程做了系统说明,整体大小仅5.86MB,轻量便于快速部署。已有496人学习浏览。项目中除基础爬虫与文本处理外,还提供了基于规则与统计方法的接龙算法实现,并附有项目目标、实现过程与使用方法,能帮助读者从分词、音韵匹配到算法调优获得完整实战闭环,适合作为课程设计或NLP入门练习的参考。
1. 诗歌接龙是什么:一个让Python写诗的下一个字的NLP项目
“诗歌接龙”听起来像语文课上的游戏,但落到程序里,它其实是NLP自然语言处理里最典型的序列生成任务:用户给出一句诗或半句诗,程序需要预测下一个字,甚至把后面的诗续完。用Python实现这样一个项目,意味着你要走完中文语料清洗、序列建模、概率采样、结果评估的完整链路。它的难度比文本分类高一档,但比从头做对话机器人低很多,特别适合用来检验你对Python字符串处理、数据结构和简单模型的掌握程度。你做出来的东西也不是那种跑完loss就扔掉的玩具——它能把“床前明月光”续成“地上鞋两双”,也能在调好参数后生成像模像样的五言句。这个项目适合谁?刚学完Python基础、想找带文本场景的项目练手的人,或者已经在用n-gram做关键词提取、想跨进生成式NLP的人。接下来我按自己实际做过的思路,把这条路上的选型、代码、参数和坑一次讲清。
2. 先想清楚接龙的数学逻辑:从n-gram到序列模型的选型思路
2.1 诗歌接龙不是在写诗,是在预测下一个字
很多人第一次做诗歌接龙,脑子里想的是“让AI学会作诗”,于是直接上GPT。但真正的第一步是把问题简化成条件概率:给定前文“床前明月”,下一个字是“光”的概率是多少?如果模型能算出候选字在给定上下文下的概率分布,那么接龙就变成一个采样问题——按概率分布挑一个字,然后把它拼到输入末尾,继续预测下一个字。
这个思维决定了技术路线。诗歌接龙的输入输出都是字符序列,本质是语言模型(Language Model)。语言模型做的事就是最大化序列概率 P(text) = P(w1) * P(w2|w1) * ... * P(wN|w1..wN-1)。而接龙只要计算 P(下一个字 | 当前已生成序列),所以训练的样本就是“给定前面N个字,预测第N+1个字”。这个视角统一了所有后续做法:n-gram里的转移概率表、LSTM里的softmax输出、Transformer里的自回归解码,全部是在求这个条件分布。
这里要强调一个容易被新手搞混的点:接龙其实不需要先分词。中文诗歌很多时候一个字就是一个语义单元,按“字”做语言模型比按“词”做更稳。词表就是全部出现过的字,几百个字就够用,训练快、可控性强。按词的话,古诗里大量典故、人名、地名会切成碎片,反而学不好。我一般默认用字符级建模,除非要做主题分类才考虑词级特征。
2.2 数据准备:把古诗库变成模型能吃的序列样本
数据是诗歌接龙的命根子。常见做法是用公开的古诗数据集,里面包含唐诗、宋词、诗经等文本。拿到的原始数据通常是这种格式:
[{"author": "李白", "title": "静夜思", "paragraphs": ["床前明月光", "疑是地上霜"]}]也有的是纯文本,一行一首诗,或描写段落后跟诗句。第一步是把所有诗句抽出来,去掉作者、标题、注释。注意古诗里会有繁体字、异体字,比如“裏”和“里”,“閒”和“闲”。我一般会统一转成简体,做法是用opencc库:pip install opencc-python-reimplemented,然后:
import opencc cc = opencc.OpenCC('t2s') # 繁体转简体 text = cc.convert(raw_text)转完之后还要做清洗:去掉空格、换行、标点(保留句号逗号也行,但接龙目标是诗句,通常去掉标点让模型自己学);去掉长度小于4或大于12的行,因为五言绝句一般是5或7个字一句,太短的行多半残缺,太长的可能是散文混入。清洗后的语料长这样:
床前明月光 疑是地上霜 举头望明月 低头思故乡有了这个纯文本列表,就可以构造训练样本了。如果是n-gram,需要统计“前N-1个字 -> 下一个字”的计数;如果是LSTM,需要把每句诗转成整数序列,再用滑动窗口切出输入和标签。这一步是整个项目的地基,没做好后面模型再强也白搭。
2.3 选型对照:n-gram、LSTM与Transformer该用哪个
搞清楚任务本质后,选型就清晰了。常见做法是分三档:
n-gram(字符级):简单到用Python字典就能实现。它统计“前面n-1个字”这个状态出现时,后面跟的每个字的次数,然后算概率。优点是训练只要一次遍历语料,完全没有GPU需求;缺点是只能记住连续n个字内的关系,n设大了矩阵稀疏爆炸,n设小了生成出来的句子前后逻辑差。对诗歌接龙这种短文本,n=3或4已经能生成语法通顺但内容随机的小诗。
LSTM/GRU:能捕捉远距离依赖,比如律诗里上下句的语义呼应。训练需要神经网络框架,我一般用PyTorch,因为动态图方便写采样逻辑。LSTM接收前文序列的embedding,最后一个时间步的输出通过全连接层映射到字典大小的向量,softmax后就是下一个字的概率。它比n-gram学到的泛化能力强,你输入一句不在语料里的“半句诗”,LSTM也能接出个差不多的下句。缺点是训练时间受序列长度和数据量影响,字符级词表小的情况下,CPU也能跑,但效率低,还是建议装个CUDA版PyTorch。
Transformer/预训练模型:如果你手里有百G语料或者想直接用中文GPT-2,那效果肯定最好,但这不是普通“python实现NLP项目”该走的路。预训练模型像是拿电锯切豆腐,模型下载几个G、显存要十几G,而诗歌接龙本身数据量不大,几百KB古诗就够。除非你后来要做风格迁移、多轮对诗,否则用Transformer从头训是在给自己找罪受。
我自己的建议路线是:先花半小时用n-gram跑通整个“输入-生成-输出”流程,感受一下概率采样的效果;再花两小时把同样的数据喂给LSTM,对比质量差异。两条路写一遍,你对序列生成的理解会深很多。下面的章节我就按这个顺序展开。
3. 用纯Python实现第一个可用的诗歌接龙:字符级n-gram与beam search
3.1 编码与计数:构建转移概率表
n-gram的实现核心是计数器。我以“三元语法”为例,也就是n=3:用前两个字预测下一个字。你需要一个字典,key是长度为2的字符串(或元组),value是另一个字典,记录每个下一个字的出现次数。
import os import random from collections import defaultdict, Counter def load_lines(path): lines = [] with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if len(line) >= 5 and len(line) <= 7: # 只保留五言或七言 lines.append(line) return lines def build_trigram(lines): # key: 前两个字组成的字符串, value: Counter(下一个字 -> 次数) model = defaultdict(Counter) for line in lines: # 在开头加两个特殊字符,让首字母也有前文 padded = '^^' + line + '$' for i in range(len(padded) - 2): context = padded[i:i+2] next_char = padded[i+2] model[context][next_char] += 1 return model这段代码做了两件关键的事。第一,给每一行首尾加了特殊字符:^表示句首,$表示句尾。这样生成的时候,模型知道如何开始和结束。第二,用defaultdict(Counter)构建两层字典,外层的key是“前两个字”,内层的key是“下一个字”。统计完成后,你随时可以查model['明月']['光']得到“明月”后面跟“光”的次数。
注意我过滤了长度在5到7之间的行。严格来说五言是5字,七言是7字,但古诗里也有四言和杂言,过滤掉能减少噪声。如果你想让模型也能生成七言和五言混合,就不要死卡长度,可以在构造样本时把句子按逗号再分割,逐句加入。
下一步是把这个计数模型转成概率模型。因为要用到随机采样,我直接保留计数,生成时再除以总数,或者用random.choices按权重采样。后者更省事。
3.2 beam search采样:让接龙不只会重复
有了模型,最朴素的生成是贪心:每次选概率最大的字。但这样会陷入循环,比如“床前明月光”之后永远接“疑是地上霜”,因为训练语料里这个组合频率高。更糟糕的是,如果模型只学到高频搭配,你就会得到“床前明月光,床前明月光”的复读机。
解决手法是用beam search(束搜索):每一步维护K个候选序列,每个候选序列按累计log概率排序,下一轮对每个候选继续展开所有可能的字,再取前K个。这样生成的质量远好于贪心,又比纯随机稳定。
def beam_search(model, prefix, beam_width=5, max_len=10): # prefix: 用户输入的字符串,至少2个字符(因为n=3) start = prefix[-2:] # 取后两个字作为初始context candidates = [(start, prefix, 0.0)] # (context, full_text, log_prob) for _ in range(max_len - len(prefix)): new_candidates = [] for ctx, full, lp in candidates: counts = model.get(ctx) if not counts: continue # 没有见过这个context,放弃该分支 total = sum(counts.values()) for ch, cnt in counts.items(): prob = cnt / total new_lp = lp + math.log(prob) new_full = full + ch new_ctx = ctx[1] + ch new_candidates.append((new_ctx, new_full, new_lp)) # 只保留分数最高的beam_size个 new_candidates.sort(key=lambda x: x[2], reverse=True) candidates = new_candidates[:beam_width] # 若所有候选都断了,提前结束 if not candidates: break # 返回分数最高的完整文本 candidates.sort(key=lambda x: x[2], reverse=True) return candidates[0][1]这个函数每步对当前所有候选展开所有可能的下一个字,计算累计对数概率,然后截断到beam_width。对数概率避免了很多个小概率相乘导致的浮点下溢,这是做beam search必须养成的习惯。参数beam_width越大,生成的多样性越好,但耗时线性增加。对诗歌接龙,5到10就够。
3.3 三个关键参数:n、温度、beam size怎么设
n-gram里的n决定记忆力长度。n=2时很容易生成无厘头的顺口溜;n=4时生成的句子更像原诗片段,但数据稀疏非常明显,很多context没见过导致生成中断。我实测下来n=3是性价比最高的。如果你语料量大到几万首,可以试n=4,但要把未登录词处理做好——比如context没见过时回退到较低阶的n-gram,这叫backoff。
beam width不能太大,太大容易让所有候选都来自同一高频分支,反而失去多样性。这里有个反直觉点:beam search的目标函数是“序列整体概率最高”,这意味着它会优先选语料里出现过的整句,而不是有创意的句子。所以如果你想看到模型“创作”新诗而不是背诵原诗,需要在采样时引入温度参数。
温度(temperature)是软max里的缩放系数。在生成时,我们把概率换成p_i = exp(log(p_i)/T)再归一化。T>1时分布变平缓,低概率字也有机会被选中;T<1时分布变陡峭,趋向贪心。我用n-gram时一般不在beam search里加温度,因为beam search本身就是取top,温度会影响排序。正确做法是:想要质量高、像古诗,用beam search(T=1),想要花样多、能“作诗”,改用random sample并按温度重新加权概率。两种路径我都保留在代码里,随场景切换。
另外一个容易忽略的参数是最大生成长度。诗歌接龙通常续3到4个字就能构成完整一句(因为用户输入半句,或者想续下一句)。我的经验是:如果用户输入的是“床前明月”,最大长度设为10,让它生成通顺的七言;如果用户输入的是完整“床前明月光”,再接龙就是出下一句,最大长度设为12也够。长度太长会让模型变散文,失去诗的节奏感。
到这里,你已经有一个不开GPU也能跑的诗歌接龙脚本。它会读古诗文件,记住所有三字结合,接着用户输入的半句诗返回一个续句。如果你想继续提高生成质量,下一步就该上深度学习了。
4. 把接龙升到深度学习:用LSTM做更自然的续句
4.1 构造训练数据:滑动窗口生成特征与标签
深度学习的训练数据不再是计数表,而是整数序列。先把所有诗句拼接成一个长序列,或者按句切割。我倾向于按句切割,但为了学习上下文关系,也可以把整首诗作为一个序列,中间用分隔符连接。这里提供一种简洁的按句切法:
def build_dataset(lines, seq_len=5): # 建立字->id映射 chars = sorted(set(''.join(lines))) char2id = {c: i for i, c in enumerate(chars)} id2char = {i: c for c, i in char2id.items()} X, y = [], [] for line in lines: ids = [char2id[c] for c in line] # 用前seq_len个字预测后面所有字 for i in range(len(line) - seq_len): X.append(ids[i:i+seq_len]) y.append(ids[i+seq_len]) return X, y, char2id, id2char这段代码的核心是滑动窗口。比如句子“床前明月光”转成ids后,seq_len=3时,[床,前,明]预测月,[前,明,月]预测光。这个做法让一条五言诗句产生两个训练样本。注意我没有把整首诗不分句来滑窗,因为古诗的上下句之间有语义呼应,但接龙通常只延续半句,跨句训练反而会让模型去记忆下一句开头,降低泛化性。如果你要玩藏头诗、要续一整首,那再改成跨句滑窗。
seq_len是模型看到的“上文长度”。n-gram里的n是固定的3,LSTM里的seq_len可以灵活调。对七言诗,seq_len设为7能覆盖完整的一句;但我发现seq_len=5到6已经足够,因为接龙时用户给的前缀通常不会太长,而且LSTM内部有隐状态,理论上能记住更长信息。设太长会加大训练负担,还会让模型过于依赖前文的精确顺序。
4.2 搭建一个可训练的中文诗歌LSTM模型
这里用PyTorch搭一个两层LSTM。词表大小就是刚才的char2id长度,一般几百到一千。embedding维度我选128,隐藏单元选256。下面是完整模型定义:
import torch import torch.nn as nn class PoemLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2, dropout=0.3): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, x): # x: (batch, seq_len) emb = self.embedding(x) # (batch, seq_len, embed_dim) out, _ = self.lstm(emb) # (batch, seq_len, hidden_dim) last = out[:, -1, :] # 取最后一个时间步 logits = self.fc(last) # (batch, vocab_size) return logits这里有几个关键点:batch_first=True让输入形状变成(batch, seq_len, feature),这是PyTorch LSTM的习惯性写法;out[:, -1, :]只取序列最后一个时间步的输出,因为预测目标就是最后一个字之后的字。如果你用双向LSTM,这里要拼接前后两个方向,但古诗接龙只能看上文,所以用单向LSTM更合理。
训练时用交叉熵损失,但注意别把标签做成one-hot,直接用nn.CrossEntropyLoss()配合整数标签就行。优化器我常用Adam,学习率设0.001。下面是训练循环的骨架:
def train_model(model, X, y, epochs=30, batch_size=128, lr=0.001): optimizer = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.CrossEntropyLoss() dataset = torch.utils.data.TensorDataset( torch.tensor(X, dtype=torch.long), torch.tensor(y, dtype=torch.long) ) loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) for epoch in range(epochs): total_loss = 0 for xb, yb in loader: model.zero_grad() logits = model(xb) loss = loss_fn(logits, yb) loss.backward() optimizer.step() total_loss += loss.item() if (epoch + 1) % 5 == 0: print(f"epoch {epoch+1}, loss {total_loss/len(loader):.4f}")训练结束后保存model.state_dict(),方便下次加载。注意embedding层会把每个字映射成一个向量,LSTM学的是“在这种向量排列下,下一个字通常是什么”。如果你的语料只有几千首,过拟合会很明显,所以训练到loss降到一定值后就得停止,不要追求完美。
4.3 训练与采样:loss降到多少才够用
训练loss不是越低越好。我拿全唐诗五万多首做实验,字符级词表大约3000字,在两层LSTM 256隐藏单元下,loss约在1.8左右就能稳定生成通顺句子;降到1.5以下时模型开始倾向于复读原句,生成创新性下降。这个规律挺反直觉的——很多NLP项目追求loss越低越好,但生成任务里loss太低等于记住了训练集。
采样时,深度学习模型不像n-gram有硬性的context匹配失败问题,因为它对任意输入都能给出softmax分布。但正因如此,没有temperature调节的话,它更容易生成高概率的常见字。我常用temperature=0.8生成,既能保住通顺度,又有一定创新。下面是采样代码:
def sample(model, prefix, char2id, id2char, seq_len, max_len=20, temperature=0.8): model.eval() # 把prefix补到seq_len长度 chars = list(prefix) for _ in range(max_len - len(prefix)): # 取最后seq_len个字,不够则左侧补起始符(这里用第一个字的id补) context = chars[-(seq_len-1):] if len(chars) > seq_len-1 else chars if len(context) < seq_len: pad = [char2id[chars[0]]] * (seq_len - len(context)) context_ids = pad + [char2id[c] for c in context] else: context_ids = [char2id[c] for c in context[-seq_len:]] # 确保长度正确 context_ids = context_ids[-seq_len:] x = torch.tensor([context_ids], dtype=torch.long) with torch.no_grad(): logits = model(x)[0] # 温度采样 logits = logits / temperature probs = torch.softmax(logits, dim=-1) next_id = torch.multinomial(probs, 1).item() chars.append(id2char[next_id]) return ''.join(chars)采样时每次只取最后seq_len个字作为输入,然后逐个生成。注意我用了torch.multinomial而不是argmax,这是生成任务的关键——纯argmax会生成大概率但无聊的句子;multinomial按概率随机抽,配合temperature可以得到多样且合理的输出。
如果你想让输出更像格律诗,可以在采样后加一个大体检测:检查最后一个字的韵脚,或者检查每句字数。但这些都算是后处理,放在进阶技巧里展开。
5. 诗歌接龙避坑指南:数据、生成与环境的四个典型翻车点
5.1 古诗库的编码坑:繁体、异体和不完整的标点
现象:模型生成的句子全是简体字,但训练语料里的“里”和“裏”被当成两个不同字,导致“故里”这个常见词被拆散。更严重的是,有的古诗库直接从古文献扫描OCR出来,混有“丨”“丶”等生僻符号,会拉高词表大小,浪费训练空间。
原因:数据来源不是为机器学习准备的,原始文本包含繁体、异体和噪声符号。我一开始偷懒直接用原始语料跑,结果词表里冒出3000多个罕见的收字,模型把所有精力用在区分这些低频字上,生成质量大打折扣。
解决:训练前统一做三步:先用opencc转简体;再手动清掉非汉字字符;最后过滤掉长度异常的行。这一步没法全自动,需要定期抽查样本。血的教训是,别相信任何“已经清理好”的古诗下载包,自己跑一遍清洗才放心。
5.2 训练时的loss不降:学习率与序列长度
现象:LSTM训练50个epoch,loss一直在3.0附近徘徊,生成的全是“之乎者也”一类的高频虚词。
原因:最常见的是学习率太大导致loss震荡,或者学习率太小导致模型卡在较差局部解。另一个隐藏原因是序列长度太短,比如seq_len=2,模型根本学不到语义。还有一个容易忽略的:没有加embedding的初始化,或者输入id从0开始但字典里有0填充,导致padding干扰。
解决:先把学习率调到0.0005,batch_size调小一点,观察loss是否有下降趋势。如果序列长度小于5,先改成5。如果还是不动,检查输入数据是否真的被正确塑形,用print(X[0], y[0])看一眼前后对应关系。我遇到过把X和y错位一行的情况,模型等于在猜随机数,loss当然不降。
5.3 生成内容全是“的”“了”:温度太低与词频熵
现象:无论用户输入什么,生成的下一句都是“春风得意马蹄疾”这种高频诗句,或者反复输出“的”“了”这种虚词。
原因:古诗语料里“之”“人”“风”等字出现频率极高,模型学到的先验分布被这些高频词主导。如果temperature设得太低(比如0.2),softmax输出几乎等同于argmax,模型就只会选这些高频词。beam search的宽度如果设得太大,也会导致候选全是高频组合,看起来反而单一。
解决:把temperature调到0.8~1.2,多试几次。另外可以在采样时对高频虚词做降权,比如把“的”“了”“之”的logits减去一个大常数。这个方法虽然粗暴,但很有效。还有一个技巧:如果使用beam search,从5个候选中挑一个“不是最顺口”的,反而更有诗意。
5.4 环境依赖:numpy版本和Python 3.8的兼容问题
现象:代码在一台机器上跑得好好的,换到另一台装了Python 3.8的机器上,import torch直接报错,或者numpy操作报AttributeError: module 'numpy' has no attribute 'float'。
原因:PyTorch版本和numpy版本不匹配,老版本numpy里np.float这种别名在numpy 1.24之后被移除了。很多诗歌接龙项目demo写于两三年前,里面用了np.float、np.int,在新环境里直接崩。
解决:给项目写一个requirements.txt,锁定版本。我常用的组合是python==3.8.18、numpy==1.23.5、torch==2.0.1。不要一上来就装numpy 2.x,除非你确认代码里没用旧别名。还有一个更稳做法:用from numpy import float这种地方全改成float,或者干脆pip install numpy==1.23.5一步到位。环境问题虽然不大,但能卡你半天,这真不是玄学。
6. 进阶技巧:从单一接龙到藏头诗与风格控制
当你的接龙模型能稳定续出通顺句子时,下一步可以让输出更“好玩”或者更“可控”。这里我给你三个我实际用过、效果明显的小改造。
第一个是藏头诗生成。用户指定四个字,比如“山河无恙”,要求每句开头分别是这四个字。实现思路叫约束beam search:在beam search扩展候选时,对于每一句的第一个字符,只允许候选等于指定字。具体来说,你可以在生成前设置一个forced_chars列表,第i句生成时,将beam search的每一步查询改为:当生成的字符位置是句首时,直接过滤掉不是目标字的候选。这样做需要注意:被强制选择的字如果在模型里概率很低,整句的log概率会比其他候选低,于是beam search可能把整个分支剪掉。解决方法是放宽剪枝:对被强制的字乘上一个放行权重,或者干脆直接不让概率参与排序,只让后面的词决定排序。这种方法虽然简单,但能让输出每句都以指定字开头,实现成本不到20行代码。
第二个是风格控制。你想让模型生成“李白一点”还是“杜甫一点”,可以在训练数据上下功夫:把作者信息作为条件,在输入序列最前面加一个“风格标记”。比如对李白的诗,输入为“[李白]床前明月光”,对杜甫的诗输入为“[杜甫]国破山河在”。训练时模型会把风格标记当作前文的一部分,生成时你也把“[李白]”拼在输入前缀前面。这样同一个模型能感知不同诗人的用词习惯,算是很轻量的conditional language model。如果你不想重新训练,也可以在采样后做规则过滤,比如检测是否含“剑”“酒”“月”——但规则永远不如数据条件自然。
第三个是韵律校验。古诗接龙最让人头疼的是最后一句的韵脚。你可以准备一个韵表,在生成最后一个字时,只采样压韵母的字。做法很简单:把logits中不符合当前韵部的位置全部置为负无穷,然后重新softmax。注意必须先确定本首诗压什么韵,通常是第一句的最后一个字决定。如果用户输入的是“床前明月光”,最后生成的字必须压“ang”韵,于是“霜”“乡”“望”“裳”成为候选。这个校验可以放在温度采样之后,也可以直接把采样空间限制在韵部内。我的习惯是先限制韵部再采样,这样既快又自然。
我自己做过一版用藏头诗+LSTM的生成器,翻车的地方在于强制字常常是“之”“以”这类虚词,导致整句变成“山之高兮,水之长兮”,虽然压韵但不口语。后来我把强制字出现的概率阈值放宽,甚至在候选为空时允许放弃该句改用次优句,才算稳定下来。这是血泪经验:约束越多,生成质量越容易崩,需要给模型留一条“破例”的后路。
这个项目做到最后,你会发现诗歌接龙的核心价值不在于“写诗有多像”,而在于你对语言模型生成机制的完整理解——从计数到神经网络,从贪心到beam search,从过拟合到温度调节。这些经验会直接迁移到其他生成任务,比如切词、评论生成、甚至简单对话。希望我的这些踩坑记录和参数习惯能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取