简介:这是一套基于Python的古诗生成器完整源码,并集成可直接操作的前端页面,面向对自然语言处理、AI写诗和前后端一体化开发感兴趣的编程爱好者与学习者,可作个人练习、课程设计或兴趣小组的实践素材。压缩包共43个文件、约10.85MB,文件类型涵盖7个Python脚本、5个XML配置文件、5个CSS样式表、5个JavaScript脚本、4个文本文件,以及GIF/PNG图片与字体资源;其中Python脚本负责初始化、数据处理、模型训练与诗词评估,XML/JS/CSS承担项目配置、页面交互和视觉样式,文本与图片素材则提供诗库及界面元素。目前已有323人学习/下载。项目完整呈现了从语料处理、模型生成到前端展示的闭环,并借助中文BERT预训练模型提升生成效果;源码结构清晰、注释到位,便于读者梳理流程,也可作为二次开发、功能拓展或教学演示的参考基础。
1. 古诗生成器是一个被低估的 Python 练手项目:它到底解决什么问题
古诗生成器是一个被低估的 Python 练手项目;把它落地到能打开网页点一下按钮就生成一首五言诗,是理解 Python 后端到前端完整链路的捷径。标题里的“源码”意味着你要拿到的不只是几行 demo,而是一套能跑、能改、能展示的综合工程。它真正练的不是“写诗”的玄学,而是中文语料处理、文本生成模型的搭建、接口封装,以及前端集成设计这四件事。
这套东西适合已经把 Python 基础语法过了一遍、想找个有可视化成果的项目的开发者,也适合准备课程设计或毕设展示的人。难点不在模型本身,而在你愿不愿意把“能出结果”打磨成“稳定可用”:语料干不干净、接口容不容错、前端是否处理了请求失败。我这里按从头搭一套常见方案的路径来说,所有代码都是可以直接复现的落地写法。
2. 接口先行:生成器的模型选型与前后端协议设计
先定协议,再写代码。直接打开编辑器刷模型很容易翻车,因为前端不知道后端要传什么,后端不知道前端要不要 loading。一个能长期改着玩的古诗生成器项目,第一件事是把模型选型和接口约定定下来。
2.1 三种生成方案怎么选:规则、马尔可夫链、LSTM
不同来源的古诗生成器源码,核心思路基本逃不开三类。模板规则最稳定,输出一定是“五言绝句”的格式,但它没有生成能力,只是填词;马尔可夫链快,语料小也能跑,缺点是生成内容跳跃;字符级 LSTM 最自然,能学到“平仄+对仗”的大致规律,但要花时间训练。
对要交作业或者做展示的场景,我更建议用 LSTM。它的代码量看起来比马尔可夫链大,但训练过程有损失曲线可看,生成的句子也更像“诗”。如果只是想快速验证前后端调通,那就先用马尔可夫链,后面再换模型,接口不用变。
| 方案 | 生成质量 | 训练成本 | 适合场景 |
|---|---|---|---|
| 模板规则 | 低,但格式稳 | 无 | 纯页面展示 |
| 马尔可夫链 | 中,语句跳跃 | 秒级 | 快速验证接口 |
| 字符级 LSTM | 较高,有诗感 | 分钟到小时 | 正式项目、课程设计 |
2.2 定义前后端接口:用 JSON 统一请求和响应
前端和后端之间只走一个接口:前端传“起句、生成长度、随机度”,后端返回“生成的诗句”。这样做的好处是,以后把 Flask 换成 FastAPI,或者把前端从原生 HTML 换成 Vue,都不用改生成逻辑。
请求方式定为 POST/api/poem,请求体是 JSON。我要特别说明为什么用 POST 而不是 GET:生成参数里包含汉字,GET 会有 URL 编码问题;另外同一句话反复生成,结果应当不同,这种带副作用的操作放 POST 更合适。
{ "seed": "春江花月夜", "length": 20, "temperature": 0.9 }响应的 JSON 结构也要提前固定。除了生成结果,我还会返回 seed 和 temperature,方便前端展示“这是基于什么生成的”。如果生成过程抛异常,响应里要带一个error字段,而不是让前端拿到空白。
{ "poem": "春江花月夜,风清月满楼。", "seed": "春江花月夜", "temperature": 0.9 }接口约定不一定写在文档里,直接写一个api.md放项目根目录也行。重点是前后端都按同一份字段开发。
2.3 项目目录怎么铺:Flask 前后端一体的常见结构
我做这类项目时习惯用一个 Flask 应用同时管后端接口和前端页面。目录不需要花哨,但要把语料、模型、静态资源分开:
poem_generator/ ├── app.py # Flask 入口,同时提供页面和 API ├── train.py # 训练脚本 ├── corpus/ │ └── 唐诗三百首.txt ├── models/ │ └── poem_model.h5 ├── static/ │ └── app.js # 前端逻辑 └── templates/ └── index.html # 页面模板静态文件放static,HTML 放templates是 Flask 默认约定。这样做最大的好处是前端可以和后端一起启动,省掉跨域问题。如果你打算把前后端彻底分离,那前端就要另起 dev server,后端必须处理 CORS,这个坑放到第 5 章专门说。
3. 后端实现:语料清洗、LSTM 生成与 Flask 接口
后端是整套生成器的核心。我一般分成三步:先清洗语料,再训练模型,最后把模型包成接口。每一步单独跑通,最后合在一起,不要一口气写完再调试。
3.1 语料清洗:把一本诗集变成模型能读的纯文本
古诗文网站上的文本经常夹着全角空格、注释和 Windows 换行符。字符级模型是按字学习的,一个混进去的\u3000也会被当成独立字符,导致字符表变大、生成结果里出现空白。我常用的清洗函数如下:
import re def clean_corpus(corpus_path: str) -> str: with open(corpus_path, encoding="utf-8") as f: text = f.read() # 去掉全角空格、制表符和普通空格 text = re.sub(r"[ \t\u3000]+", "", text) # 只保留汉字、英文半角标点和换行,其余全部丢弃 text = re.sub(r"[^\u4e00-\u9fff,。!?\n]", "", text) # 统一换行符,避免 Windows 的 \r\n 干扰切分 text = text.replace("\r\n", "\n").strip() return text这段代码的关键在第二个正则:\u4e00-\u9fff是统一汉字编码范围,标点只保留古诗里常用的逗号、句号、感叹号和问号。如果你把“123”和字母也保留,模型可能会学到不想要的噪音。
参数说明:corpus_path指向你下载好的 txt 语料;返回值是一个干净的长字符串。要特别提醒,直接读文件时encoding="utf-8"必须显式指定,Windows 默认编码可能是 gbk,不指定会报UnicodeDecodeError。
3.2 训练一个字符级 LSTM:核心代码和参数怎么调
字符级模型的任务是:给你前 20 个字,预测第 21 个字。训练前要把清洗后的字符串切成固定长度的输入和输出序列。这里有一个时间窗口seq_len和步长stride,直接影响训练数据量和生成连贯性。
# train.py 核心训练片段 import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Embedding from tensorflow.keras.callbacks import ModelCheckpoint text = clean_corpus("corpus/唐诗三百首.txt") chars = sorted(set(text)) char_to_idx = {ch: i for i, ch in enumerate(chars)} idx_to_char = {i: ch for i, ch in enumerate(chars)} seq_len = 20 stride = 3 X, y = [], [] for i in range(0, len(text) - seq_len, stride): seq_in = text[i:i + seq_len] seq_out = text[i + seq_len] X.append([char_to_idx[ch] for ch in seq_in]) y.append(char_to_idx[seq_out]) X = np.array(X) y = np.array(y) model = Sequential() model.add(Embedding(len(chars), 128, input_length=seq_len)) model.add(LSTM(256, return_sequences=True)) model.add(LSTM(256)) model.add(Dense(len(chars), activation="softmax")) model.compile(optimizer="adam", loss="sparse_categorical_crossentropy") checkpoint = ModelCheckpoint("models/poem_model.h5", save_best_only=True, monitor="loss") model.fit(X, y, batch_size=128, epochs=30, callbacks=[checkpoint])几个参数一定要理解。seq_len=20表示用 20 个字预测下一个字,太小生成容易跑题,太大训练样本急剧减少。stride=3表示每 3 个字切一个窗口,如果设成 1,数据量会变成三倍,但相邻样本高度重复,训练容易过拟合。
模型结构上,我用两层 LSTM,第一层返回完整序列,第二层只返回最后一个输出,这样能捕捉到前后文的短期依赖。Embedding直接设成 128 维,古诗字表一般只有几千个字,这个维度够用了。batch_size=128在 CPU 上也能接受,如果显存不足就调成 64 或 32。
训练过程里,loss 从 3 左右慢慢降到 1 以下是正常的,不需要等到它到 0。LSTM 生成古诗本身就有玄学成分,重点是模型记住常用字组合和结尾语气词。
3.3 模型加载与生成函数:避免每次请求都重读文件
很多源码会在接口里写load_model,这是最大的性能问题。模型文件几十兆,每次请求都加载一次,页面就会卡好几秒。正确做法是在 Flask 启动时加载一次,放到全局变量。
# generate.py import numpy as np from tensorflow.keras.models import load_model _model = load_model("models/poem_model.h5") _char_to_idx = None # 训练完把映射存成 json 后恢复 def generate_poem(seed: str, length: int, temperature: float) -> str: result = seed for _ in range(length): seq = result[-20:] x = np.array([[ _char_to_idx[ch] for ch in seq ]]) preds = _model.predict(x, verbose=0)[0] preds = np.log(preds + 1e-8) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) next_idx = np.random.choice(len(preds), p=preds) result += idx_to_char[next_idx] return result这里的temperature是控制随机度的关键参数:大于 1 生成更随机但容易不通顺,小于 1 则更保守但容易重复。我在服务层会把它限制到 0.1 到 1.5 之间。
3.4 Flask 接口:把生成函数包成 JSON 服务
到这一步,生成器已经能跑了,现在把它挂到 HTTP 服务上。
from flask import Flask, request, jsonify from generate import generate_poem app = Flask(__name__) @app.route("/") def index(): return app.send_static_file("index.html") @app.route("/api/poem", methods=["POST"]) def poem_api(): body = request.get_json(force=True) seed = body.get("seed", "春江").strip() length = max(1, min(int(body.get("length", 20)), 50)) temperature = max(0.1, min(float(body.get("temperature", 0.9)), 1.5)) poem = generate_poem(seed, length, temperature) return jsonify({"poem": poem, "seed": seed, "temperature": temperature})force=True允许前端即使忘了设Content-Type也能解析 JSON,联调时少踩一个坑。length限制在 50 以内,防止有人调一个 1000 让服务器生成半天。返回的 JSON 和第 2 章约定完全一致,前端可以直接用。
4. 前端集成设计:输入、展示、状态管理与请求联调
前端集成设计不是写一个按钮调接口就完了。你要处理用户输入、加载状态、生成失败提示,还有历史记录。这里用原生 HTML + JavaScript 实现,逻辑简单、没有 Node 依赖,任何人拉下来都能直接跑。
4.1 页面骨架:输入起句与结果容器
templates/index.html是 Flask 默认读取的页面模板。我把按钮、输入框、结果区写好,样式尽量简单。
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>古诗生成器</title> </head> <body> <div class="container"> <h1>古诗生成器</h1> <input id="seed" placeholder="输入起句,比如:春江花月夜" value="春江花月夜"> <button id="submit">生成古诗</button> <p id="status"></p> <div id="result" class="poem"></div> <div id="history"></div> </div> <script src="/static/app.js"></script> </body> </html>这里的value用做默认提示语,用户不输入也能生成。status用来显示“生成中”或“出错了”。结果区单独占一个div,方便用 CSS 控制字体和间距。
4.2 用 fetch 调用后端接口:串起整个链路
static/app.js负责监听按钮点击、调用接口、渲染结果。这里最容易错的点是:fetch 里的headers必须显式声明Content-Type: application/json,否则 Flask 端的request.get_json拿到的可能是None。
const submitBtn = document.getElementById('submit'); const seedInput = document.getElementById('seed'); const resultDiv = document.getElementById('result'); const statusDiv = document.getElementById('status'); async function generatePoem() { const seed = seedInput.value.trim() || '春江花月夜'; statusDiv.textContent = '生成中...'; try { const response = await fetch('/api/poem', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seed, length: 20, temperature: 0.9 }) }); const data = await response.json(); if (data.error) throw new Error(data.error); resultDiv.textContent = data.poem; statusDiv.textContent = ''; } catch (err) { statusDiv.textContent = '生成失败,请检查后端服务是否启动'; } } submitBtn.addEventListener('click', generatePoem);我把错误处理放在catch里,网络断连、后端 500、后端返回error字段都会被捕获。这样页面不会白屏。
4.3 加一个 localStorage 历史功能:前端集成设计的加分项
课程设计只做“点按钮生成”太单薄。我把生成结果存到localStorage,刷新页面后历史还在。这个功能对展示很有用,也能体现前端状态管理的思路。
function saveToHistory(poem, seed) { const history = JSON.parse(localStorage.getItem('poemHistory') || '[]'); history.unshift({ poem, seed, time: new Date().toLocaleString() }); localStorage.setItem('poemHistory', JSON.stringify(history.slice(0, 10))); renderHistory(history); } function renderHistory(history) { const historyDiv = document.getElementById('history'); historyDiv.innerHTML = history .map(item => `<p>${item.time}:${item.poem}</p>`) .join(''); }在generatePoem的函数里,拿到data.poem后调用saveToHistory即可。注意localStorage只能存字符串,所以对象要先JSON.stringify,读取时再JSON.parse。
4.4 联调时先确认 URL:静态页面和后端端口常见错位
前端写完后,第一步不是功能测试,而是确认页面里所有请求地址对不对。Flask 默认跑在http://127.0.0.1:5000,如果你直接用浏览器打开index.html文件,fetch 的/api/poem会变成一个无效的file:///api/poem请求。
正确做法是启动 Flask 后打开http://127.0.0.1:5000/。如果你用 VSCode 的 Live Server 插件打开页面,那就必须给请求地址改成完整的http://127.0.0.1:5000/api/poem,同时后端要处理 CORS。这属于典型的“页面能看到但接口调不通”,多半是地址或跨域问题。
5. 避坑:从 Python 环境配置到模型推理最常见的 5 个翻车点
这个项目坑不少,每一个我都见过真实翻车的场景。这里挑最常见的五个,按“现象、原因、解决”写清楚,照着排查能省下半天时间。
5.1 现象:读取语料时报UnicodeDecodeError或生成结果全是乱码
原因:Windows 下 Python 默认编码是 gbk,而语料文件是 utf-8 保存的,打开时没指定编码。
解决:所有open都显式写encoding="utf-8",包括训练脚本和 Flask 服务。如果文件本身是 gbk,就把encoding="gbk"。判断文件编码的土办法是用 VSCode 打开 txt 看右下角,或者用 Python 的chardet检测。
5.2 现象:训练时报ResourceExhaustedError,CPU 内存或显卡显存不够
原因:batch_size设得太大,或者seq_len太长,导致每次输入模型的矩阵过大。
解决:先把batch_size降到 32,LSTM单元从 256 降到 128。如果还是爆,就减少训练语料,比如只留 200 首诗。生成器项目不需要用全唐诗,语料质量比数量重要。
5.3 现象:生成结果永远是“春江春江春江”这种循环重复
原因:temperature设得太低,导致采样总是选概率最大的那个字;或者训练轮次太多模型过拟合,只记住了特殊字连续出现的片段。
解决:把温度调到 0.8 到 1.2,并且用np.random.choice按概率采样,而不是每次都取argmax。如果过拟合,就把epochs降到 20,加一层Dropout。
5.4 现象:前端控制台报blocked by CORS policy
原因:前端页面运行在5500端口,后端 Flask 在5000端口,浏览器把这两种不同源地址的跨域请求拦截了。
解决:如果你一定要走前后端分离模式,给 Flask 接口加一个after_request的跨域头:
from flask import Flask, request, jsonify app = Flask(__name__) @app.after_request def add_cors_headers(resp): resp.headers["Access-Control-Allow-Origin"] = "*" resp.headers["Access-Control-Allow-Headers"] = "Content-Type" resp.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS" return resp这个*只适合本地开发,部署上线要把Access-Control-Allow-Origin设置成你自己的域名,否则任何网站都能跨域调用你的接口。
5.5 现象:VSCode 里跑得通,换命令行跑就报ModuleNotFoundError
原因:VSCode 默认选中的 Python 解释器和命令行里用的不是同一个,pip install tensorflow装进了系统环境,而 VSCode 用的是虚拟环境,或者反过来。
解决:在项目根目录建一个虚拟环境,统一入口。我一般这样做:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install tensorflow flask numpy然后在 VSCode 里按Ctrl+Shift+P打开“选择 Python 解释器”,选venv下那个路径。这样不管从哪个终端启动,依赖都是同一份。
6. 进阶验证:用温度与押韵检查把生成器调到可展示
到这步,你的古诗生成器已经能完整跑通了。最后一件事是把“能跑”变成“看着像样”。我这里的两个技巧是很多人不会注意的:温度动态调整和简单的押韵校验。
6.1 temperature 不只是参数:按句子长度分段调
短句子用低温度能保证开头不跑偏,长句子用略高温度能让内容更丰富。更自然的写法是生成前 10 个字时温度设 0.7,后面设为 1.0:
def generate_poem(seed, length, temperature): result = seed for i in range(length): current_temp = temperature if i < 10 else temperature + 0.1 next_char = sample_one(result, current_temp) result += next_char return result这个改动很小,但生成的效果比全程固定温度更稳定,尤其对五言绝句这种前几个字影响全诗的场景。
6.2 用押韵检查做自动回归
改完模型后怎么判断有没有变好?不能只靠肉眼。一个便宜的验证方法是检查句尾是否落在常用韵脚上。古诗的用字习惯可以在语料里统计:最后一句末字是否在训练集高频尾字集合里。
rhyme_chars = {"楼", "舟", "秋", "流", "愁", "休", "洲", "头"} def check_rhyme(poem): lines = poem.replace("。", "。\n").split("\n") last_chars = [line[-1] for line in lines if line.strip()] return sum(1 for c in last_chars if c in rhyme_chars) / max(len(last_chars), 1)每次改完训练参数,跑 50 次生成,算平均押韵率。这个数字如果下降,说明模型在“乱写了”。它不完美,但比凭感觉判断可靠得多。
我自己做这个项目的习惯是:先把接口文档写在项目 README 的第一段,再写代码。后来前端换过一次框架,后端从 Flask 换成了 FastAPI,接口字段没动,前端逻辑几乎没有改。这个教训让我确信,古诗生成器的重点不在“生成”两个字,而在于把生成能力做成了别人能调用的服务。希望帮到你。
本文还有配套的精品资源,点击获取