用Python和规则引擎实现古诗词生成器:91行代码的创意实践
2026/9/19 6:18:00 网站建设 项目流程

前阵子参加了一个“91行代码创意赛”,规则特别简单:用不超过91行代码实现一个有意思的项目,语言不限。当时我翻了不少往届作品,有做贪吃蛇的,有做网页爬虫的,还有做聊天机器人的。我琢磨着能不能做一个跟传统文化沾边、又带点“智能感”的东西,最后定了古诗词生成器。很多人一听“诗词生成”就觉得得靠大模型,但这次赛制的亮点恰恰在于极简代码,要求你在这么少的行数里做出一个能跑、有输出、有创意的作品。我最后用Python写了一个基于规则和随机采样的“智能诗词生成器”,输入主题词,它能在几毫秒内生成一首押韵的五言或七言绝句,虽然谈不上什么深度学习,但作为创意赛作品已经有足够的趣味性和可玩性。

这个项目适合刚接触Python或者对文本生成感兴趣的开发者参考,尤其是那些想在代码比赛中快速出作品、又不想堆复杂依赖的朋友。这篇文章会把我的设计思路、关键代码逻辑、踩过的坑全部整理出来,内容包括用韵脚表替代语音模型、用词库和模板实现“智能”、如何在行数限制内做格律校验,以及我用过的排查技巧。如果你也想写一个类似的生成器,可以直接照着思路抄作业。

1. 项目初衷与整体设计思路

1.1 为什么选诗词生成器

诗词生成算是文本生成领域里一个很有意思的切入口。它有一个特别好的优势:形式高度固定,五言就是五个字,七言就是七个字,绝句就是四句。这种强结构意味着不需要复杂的自然语言生成模型,只要把字词填进合适的框架里,读起来就会自带一点“诗感”。再加上押韵和平仄这两条约束,反而让生成器有了明确的目标,代码写起来不会像无头苍蝇一样乱撞。

从创意赛的角度看,诗词生成器也特别容易吸引观众。你现场跑一下,输入“山水”或者“思乡”,马上出来一首押韵的诗,效果非常直观。相比之下,做几个普通的数据处理Demo,远没有这种“生成类工具”令人眼前一亮。我在决定选题时还考虑过“对联生成器”,后来觉得对联的格律和词性要求更严格,91行内很难做像样,于是退一步选了绝句生成。绝句允许一定程度的语义跳跃,只要意象风格统一,句子之间不太讲究严密的逻辑,这给随机生成留下了很大的空间。

确定方向后,我对“智能”这两个字做了重新定义。在这个项目中,智能不是说机器真的理解诗意,而是指它能根据用户输入的主题词,从词库中选出相关意象,再按照押韵和平仄规则组装成诗。用户看到的是“我说了一个词,它真的围绕这个词写了四句诗”,这就够了。这种规则加随机的方案,在91行约束下算是性价比最高的选择。

1.2 91行代码的规划与取舍

开始写之前,我先在草稿纸上把功能拆成几块:数据、生成逻辑、命令行交互。数据就是词库和韵脚表,生成逻辑负责把词拼成句子,命令行交互负责接收参数和打印结果。这个拆分看起来简单,但恰恰是它决定了后续能塞下多少东西。如果一上来就想做GUI、做用户学习、做语料训练,那91行肯定不够用,必须提前砍掉。

最占行数的是数据部分。早期版本里,我试图把词库放在外部文本文件里,每次运行时用open()读取。这样的好处是代码行数少,但运行时依赖一个词库文件,提交作品时要多打包一个文件,而且文件读写的中文编码问题会让人抓狂。后来为了省事,我直接把词库写成了Python字典,放在代码文件里。这样虽然占了几十行,但程序自包含,任何机器上只要装了Python就能跑,省去了各种环境问题。对于一个小型创意赛来说,自包含往往比优雅更重要。

另一个果断放弃的东西是平仄全量字典。如果想把每一个常用汉字都标注平仄,代码量会爆炸。我采取的办法是只给词库里出现的字做平仄标注,说白了就是“用到什么标什么”。这样词库本身就包含了平仄信息,生成时直接读取,不用临时计算。这个思路在代码比赛里很常见:用空间换时间,不追求全量数据,只保证已有素材的可用性。

1.3 技术选型:Python 还是传统方案

选Python几乎是必然的。它语法简洁,内置了randomsyscollections这些常用模块,写起来非常紧凑。而且Python3默认Unicode,对中文字符的处理比C语言省心太多,不会动不动就蹦出编码错误。C语言当然能写,但光实现一个像样的字符串分割和哈希表就够呛,91行根本不够用;Java就更不用说了,一个类定义加public static void main就得占好几行。创意赛不限制语言,自然要选最短路径。

我用Python写的时候还特别注意“一行能做完的事绝不写两行”。比如判断一个词是否押韵,通常需要取最后一个字,再查韵脚字典,我用line[-1] in RHYMES[rhyme]一行搞定。列表推导式在这种场景下也是神器,代码紧凑,逻辑也很明确。不过后来我发现,过度压缩也不一定是好事,这个在后面的“参赛提交”章节里我会详细吐槽。

2. 核心实现:诗词生成引擎拆解

2.1 押韵引擎:用韵母表代替语音模型

押韵是整个生成器最重要的部分,也是最容易被新手忽视的地方。很多初版生成的“诗”读起来别扭,问题就出在句尾字不押韵。要想在91行内解决押韵,最直接的方法是建一张“韵脚表”。它的结构很简单:一个字典,键是韵母,值是一组常用平声字。

RHYMES = { "ang": ["光", "香", "霜", "乡", "长", "芳"], "an": ["山", "寒", "残", "烟", "船", "湾"], "ao": ["高", "遥", "桥", "涛", "箫", "潮"], "a": ["花", "霞", "家", "纱", "涯", "斜"], }

看到这里有人会问:为什么选这几个韵?我的经验是,优先选平声字多、常用字多、意象范围广的韵。anganao这三个韵在现代普通话里属于“开口音”,唱出来响亮,古人写诗也特别喜欢用。而一些窄韵,比如eiou,虽然也有名句,但常用字太少,随机生成时翻来覆去就是那几个字,读几遍就腻了。韵脚表的价值在于,“押韵”不再是运行时去查拼音库再用算法计算韵母,而是提前把字按韵分好,用字典倒排索引,用的时候直接抽。

还有一个细节:古诗词里的很多字和现代读音并不一致。比如“斜”在平水韵里属麻韵,和“花”“家”可以通押,但现代人读“xié”,如果硬要按普通话韵母分组,它就该归到ie韵。我在这个项目里选择了按现代普通话韵母来押韵,而不是严格遵循古韵。原因很简单:作品最终是给现代读者看的,普通听众不会去翻平水韵,只要听感舒服,就算押韵成功。如果你将来想做更考究的版本,可以再引入平水韵表,但91行内没有必要。

2.2 词库组织与随机采样

有了韵脚表,下一步是准备句子素材。我没有把整句诗作为生成单元,而是把词打散成“意象元素”,按主题分类放进词库。比如“山水”主题下有“青山、碧水、白云、孤舟、松风、溪流”,“思乡”主题下有“故乡、明月、归雁、离愁、客船、远山”。每个词的长度尽量控制在两个字或三个字,这样方便拼接成五言和七言。

POOLS = { "山水": ["青山", "碧水", "白云", "孤舟", "松风", "溪流", "月", "花"], "思乡": ["故乡", "明月", "归雁", "离愁", "客船", "远山", "梦", "秋"], "默认": ["春风", "落日", "长江", "古道", "飞鸟", "人间", "雨", "烟"], }

为什么不用整句模板?因为整句模板的生成结果太有限,比如你写死“孤舟蓑笠翁”,那不管随机多少次,都只是在换前后句,核心还是那一句。而“词袋”加“句式模板”的组合,可以从几十个词里随机搭配出几百种句子,生成结果的多样性要强得多。为了进一步增加变化,我还在每一种主题里混入一些通用词,比如“春”“秋”“风”“云”“愁”“梦”这类万物皆可搭配的意象词。它们能在一定程度上冲淡主题词重复带来的单调感。

随机采样主要用random.choice,非常简单。但直接随机拼接容易出现逻辑不通的句子,比如“孤舟长江月”这种词性与意象还能接受,但有时候会拼出“白云飞鸟愁”,感觉像三个名词硬凑在一起。为了解决这个问题,我在句式模板上做了手脚。五言诗我常用的模板是“2字名词 + 2字名词 + 1字形容词”,或者“2字名词 + 2字动词 + 1字名词”;七言则在五言基础上多加一个“2字状语”或“3字短语”。模板把词性顺序固定下来,即使随机生成,也不会跑出太离谱的组合。

2.3 格律校验:五言七言与平仄

格律是诗词的骨架,但91行内做完整格律校验不现实,所以我的方案是“近似校验”。绝句最常见的押韵方式是第二句和第四句押韵,第一句可押可不押;第三句通常不押韵,而且末字常用仄声。我依照这个规则,确保第一句、第二句、第四句的末尾字从韵脚表里取,第三句末尾字则从仄声字池里取。

平仄方面,我提前给词库中的每个字标注了平仄。按照普通话发音,一声二声为平,三声四声为仄。比如“青”是平声,“水”是仄声。在拼接每一句时,我并不是生成完再检查平仄,而是在取词阶段就做约束。比如五言句式“平平仄仄平”,那么第一个字必须从平声开头,第二字也必须平,第三四字要仄,最后是平。如果random.choice抽到的词不符合当前槽位的平仄,就重新抽,直到抽到满足条件的词为止。

一开始我用的是“生成后校验”策略,先随机拼一句,再算平仄匹配度,不匹配就整句重来。这样做的缺点是失败率太高。五言句有5个位置,每个位置平仄匹配的概率大约一半,整体不匹配的概率非常高,经常要重试几十次才能得到一句合格诗。后来我改成“约束采样”,按平仄槽位直接过滤可选的词,生成效率和成功率都大幅提升。这个思路虽然是针对诗词生成的,但放到很多领域都有启发:如果你的输出需要满足一系列条件,最好在设计阶段就把条件嵌进采样过程,而不是事后反复试错。

2.4 主循环与交互

命令行交互是整个项目最外面的壳。我最终设计成两种用法:一种是不带参数直接运行,程序会提示你输入主题和韵脚;另一种是通过sys.argv读取参数,比如执行python poem.py --theme shanshui --rhyme ang,适合批量展示。为了减少行数,我用sys.argv自己解析参数,没有引入argparse

主循环其实非常简单:

def main(): theme = sys.argv[1] if len(sys.argv) > 1 else "默认" rhyme = sys.argv[2] if len(sys.argv) > 2 else "ang" poem = generate_poem(theme, rhyme) print(poem)

每次运行生成一首诗,如果用户想多生成几首,可以加一个循环。但创意赛现场演示时,一般只展示一首最有代表性的作品,所以默认生成一首就够了。为了让结果更“惊艳”,我还在内部实现了一个“生成十首选最优”的隐藏逻辑,这在我后面讲调优时再展开。整个交互设计的原则就是:把用户的输入降到最低,一键出结果,让人第一眼就感受到“哇,居然能生成诗”。

3. 实操过程与核心环节实现

3.1 数据准备:从爬虫到内置列表

这个项目真正耗时的环节不是写代码,而是整理数据。我第一次尝试做诗词生成器时,第一反应是去网上下载《唐诗三百首》的文本,然后用程序切分句子、统计字频,甚至想过训练一个简单的马尔可夫链。但在这个创意赛里,如果语料放在外部文件,读写文件至少要多花5到10行代码;如果把语料直接嵌进代码,一个《唐诗三百首》的文本能占几百行,直接超限。

所以我把数据策略改成了“手工精选”。我从两三百首脍炙人口的古诗里,抽取高频意象词,再按照主题分类。比如“月亮”这个词在思乡诗里出现频率极高,我就把它放进“思乡”主题;“江水”“孤舟”常出现在羁旅题材,我就放进“山水”或“远游”主题。经过几次迭代,最终每个主题下保持十到二十个词,全表加起来也就三四十个词,数据量非常小,但覆盖面足够应付随机生成。

整理词库时我特别注意三个问题:一是词的通用性,避免出现太生僻的典故词;二是词的长度,尽量统一为两个字,方便五言和七言拼接;三是词的平仄,宁可使用意象不那么惊艳但平仄明确的词,也不要为了辞藻牺牲格律。很多初学者做生成器,容易把词库弄得又大又杂,结果生成的句子平仄混乱、押韵失败,其实问题不在代码逻辑,而在词库的“干净程度”。词库干净了,后面的规则才能稳定生效。

3.2 关键代码段实现与讲解

这里贴一段我从最终版本里摘出来的核心函数,职责是“根据主题和押韵生成一行五言诗”。虽然为了阅读方便我做了些简化,但思路和最终版是一致的。

def gen_line(pool, rhyme, pattern): line = [] for length in pattern: candidates = [w for w in pool if len(w) == length] if not candidates: candidates = pool line.append(random.choice(candidates)) line[-1] = random.choice(RHYMES[rhyme]) return "".join(line)

这段代码接收三个参数:词库、韵脚、句式模板。pattern是一个长度列表,比如[2, 2, 1]表示“两个两字词加一个一字词”。循环里按照每个槽位的长度去词库中筛选候选词,然后随机选一个填进去。最后强制把末字替换成韵脚字,这样就能保证句尾押韵。

这里有个小技巧:把末字替换掉,而不是在选词时就限制末字。因为一首绝句里,每一句的末尾字并不一定都来自主题词库,可能是单独的韵脚字。比如“青山遮不住,毕竟东流去”,如果强行要求“不住”里的“住”也变成韵脚字,句子意思就会被破坏。把韵脚字单独管理,会让生成更灵活。当然,替换完末字后,整句的平仄可能被破坏,所以我还会加一轮平仄过滤,如果末字导致整句平仄不符合模板,就重新抽一个韵脚字,直到匹配为止。

3.3 调优:让诗词更像“诗”

第一版生成器跑出来,说实话很灾难。举一个例子:它生成过“青山碧水白云孤舟月”,句子本身全是意象,却不像诗,更像一个景点列表。问题出在两点:一是动词太少,名词堆太多;二是缺少虚词和连接语,句子没有“起承转合”。

调优的第一步是增加动词和虚词。我在词库里加入了“入、照、悬、落、生、摇、带、随”这类单字动词,也加入了“自、独、空、何、谁、又”这类虚词。让句式从“名词+名词+名词”变成“名词+动词+名词”。比如“白云入孤舟”,立刻就有了一丝动态感。

第二步是引入“生成再筛选”的机制。我先让它生成十句候选,然后用一个简单评分函数打分,分数最高的留下。评分项包括:句子中是否包含主题词、末字是否押韵、平仄匹配度、是否包含动词或虚词。每项都能用一两行代码实现,折算成评分权重后,最后挑出来的句子明显“诗味”更浓。这个机制在很多生成器里都能见到,本质是“从大量随机结果中选择一个满足约束的最优解”,虽然笨,但非常有效。

3.4 打包与参赛提交

代码写完之后,我做的第一件事是用wc -l poem.py统计总行数。第一次统计出来是107行,超出指标,不得不开始精简。精简动作主要分三步:去掉多余空行和注释,把import randomimport sys合并到一行,把一些重复的列表推导式提取成公共函数。但我也遇到一个坑:压缩代码时把变量名改成了单字母,函数名也改成了f1f2之类的,结果第二天自己都看不懂了,调试起来非常痛苦。

后来我意识到,91行限制并不是要你把代码写成“天书”,而是在保证可读性的前提下,尽量精简。我的最终版保留了清晰的函数命名和少量关键注释,只去掉空行和冗余逻辑。提交时,我在同一个压缩包里放了一个README,说明项目思路和用法,README不计入行数,但起到了很重要的说明作用。评审老师不会因为代码行数刚好91就把你当神仙,他们更看重思路是否清晰、效果是否打动人心。

4. 常见问题与排查技巧实录

4.1 中文编码问题

中文编码是所有中文Python项目绕不开的坑。我在自己的电脑上运行一切正常,一放到旧版Windows命令行里,中文全部变成乱码,输出的诗像外星文。原因很简单:Windows控制台默认代码页是GBK,而Python3源码保存为UTF-8,两边不对付。

解决方法是:在脚本开头加上import syssys.stdout.reconfigure(encoding="utf-8"),强制标准输出使用UTF-8。如果运行环境特别老,还可以在命令行执行chcp 65001切换代码页。另外,源代码文件最好在编辑器里设置为“UTF-8无BOM”格式,避免在文件开头出现不可见字符。这些小细节在你本地开发时察觉不到,等提交到比赛服务器演示时就容易翻车。

4.2 随机结果不稳定

生成器每次输出的诗都不一样,这是特性,但调试的时候很讨厌。你刚发现某一个韵脚字导致平仄错误,想复现问题,结果下一次运行随机到的完全是另一批字,压根没法检查。我的办法是给程序加一个可选的随机种子参数,调试时固定使用random.seed(42),这样每次生成结果完全一致,定位问题特别方便。

展示作品时,随机种子还能帮大忙。你可以先在家里用某个种子生成一首特别好的诗,记住种子值,现场演示时用同一个种子跑,效果稳定,不会出现随机到一句烂诗的尴尬。等观众看腻了,再取消种子,让他们感受“每次生成都不一样”的惊喜。这算是一个很实用的参赛技巧。

4.3 押韵不准的排查

写了几代版本后,我发现押韵不准的问题往往不在生成逻辑,而在韵脚表本身。有些字是多音字,比如“长”既读cháng又读zhǎng,我把它放在ang韵脚表里,在普通话中确实押韵,但如果程序内部按拼音标注,就可能出现读取到另一个读音的情况。解决办法是:每个韵脚表的字只保留一个常用读音,并且只用于该韵。

还有一个容易踩的坑:近似韵的合并。现代普通话里iningeneng在某些方言里分不清,但在诗词创作里如果混用,读起来会有一点不舒服。我自己的选择是:在创意赛里可以适当合并angianganian,因为发音接近,听感不错;但尽量不要合并ining,否则容易让人听出“别扭”。我的排查手段也很简单:单独写一个测试脚本,把每个韵脚表里的字和它对应的韵母打印出来,用眼睛扫一遍,基本就能发现错误。

4.4 性能与随机尝试次数

在加入平仄约束采样后,生成效率一度变得很低。原因是词库不够大的时候,能同时满足长度、平仄和押韵条件的词可能只有一个甚至没有,程序就陷入无限重试。我最开始没意识到,还在循环里写了while True,结果直接卡死。后来优化成“如果某个槽位没有候选词,就放宽条件,从更大范围的通用词库中取词”。

另一个性能优化是给词库建索引。提前按“首字平仄”和“末字平仄”分类,比如ping_start = [w for w in pool if 平仄[w[0]] == 1],采样时直接从这个分类里选,不用每次临时遍历整个词库。批量生成几百首诗的时候,这个优化能明显减少延迟。还有一个很细节的优化:用"".join(line)拼接字符串,而不是在循环里不断line += word。字符串在Python里是不可变对象,频繁拼接会产生大量临时对象,拖慢速度。虽然这里生成量不大,但养成了好习惯,后面扩展到大规模文本生成时就不会踩坑。

5. 一些个人体会与后续扩展

5.1 从91行到更多可能

比赛结束之后,我并没有把代码丢在一边。在后续的版本里,我逐步把它扩展到了几百行:加入严格平水韵字表、支持七言绝对、加入藏头诗模式、增加更多主题词库,还尝试用简单的马尔可夫链从真正的古诗中抽取转移概率,让句子之间的衔接更自然。每一步扩展都让生成器变得更“聪明”,但我也越来越怀念91行版本的那种纯粹与克制。

如果你想继续玩下去,有几个方向值得试试。一个是“藏头诗生成”,在首字位置固定用户想要的词,其余位置继续用随机和规则填充;另一个是“词牌名生成”,比如模仿《清平乐》《浣溪沙》的字数和平仄结构;还有一个是“诗词接龙”,随机从前一句的末字出发,找到同韵字开头的下一句。这些扩展都建立在这次的基础之上,代码逻辑差别不大,主要是数据和规则的变化。

5.2 给参赛者的实用建议

如果你也想参加类似的行数限制创意赛,我有几句非常实在的建议。第一,先把功能砍到“不可再砍”的地步,先跑出一个最小可用的版本,再考虑加花活。第二,数据准备工作要提前做,很多项目表面上卡在代码逻辑,实际卡在素材质量上。第三,提交前一定要换一台干净环境跑一遍,不要只在自己电脑上测试,编码问题、依赖缺失、文件路径问题都会在陌生环境里暴露出来。

最后再分享一个我印象很深的技巧:在命令行里跑python poem.py --theme 山水 --rhyme ang之后,如果随机到一句特别好的句子,赶紧用--seed 42固定下来,再微调词库继续生成。这个工作流让我在比赛展示时,随时都能复现最好看的作品。后来我又把韵脚表和词库拆成了两个外部JSON文件,虽然需要处理文件读写,但主代码反而更精简了。不过这些都是后话。参加完这次91行代码创意赛,我最深的体会是:当限制足够大,好的设计比堆功能重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询