资讯动态

Python实现中文文字雨:终端版与pygame图形版完整指南

发布时间:2026/10/16 23:01:43 来源:尧图企业网站定制
文字雨这个效果我第一次在网上刷到的时候第一反应是“这玩意儿用Python是不是半天就能搞定”结果真正动手才发现英文数字雨的教程满大街都是换成一整屏中文之后坑比想象中多得多字体、编码、列宽、刷新率哪一个没处理到位画面都会一秒变灾难现场。这篇文章把我从零到一折腾出来的完整方案写在这里包含终端版和pygame图形版两套代码、每一步的原理说明以及我在实际调试中被坑出来的几条经验。适合两种人看一是想用Python做点多媒体小玩意儿的初学者二是想给桌面搞一个中文氛围屏保的摸鱼选手。1. 文字雨到底在写什么先拆穿它的原理1.1 所谓文字雨其实是一张不断右移的字符画很多人以为文字雨是高深算法拆穿了就是三个关键词随机、刷新、留痕。经典的数字雨效果本质是在屏幕的每一列上有一串字符以恒定或随机的速度往下移动。每串字符的头部最亮往尾部方向依次变暗形成残影。当它移动到屏幕底部就从屏幕顶部重新生成。整个过程在视觉效果上产生“雨坠落”的错觉。用程序语言描述就是维护一个二维区域。x方向是屏幕列y方向是行每个坐标点存放一个字符然后用一个无限循环持续更新每个雨滴的位置最后把画面渲染到屏幕上。英文数字雨的字符池是“0-9”加上一些运算符字符宽度统一渲染起来非常省心。中文则不同汉字天然自带内容感和古风感但每个字占两个英文字符的显示宽度字库又大写不好就是满屏豆腐块加错位。这也是我把标题定为“中文文字雨”的原因——英文版本随手就能写中文版本才是真正值得拆开来讲的东西。1.2 两种路线终端版和图形版实现中文文字雨有两条路线可选。第一条是终端版直接在命令行窗口里渲染字符利用ANSI转义序列控制光标和颜色。优点是零第三方依赖任何能跑Python的机器都能立刻运行缺点也很明显字符大小、颜色、刷新率都受终端限制画面比较朴素。第二条是pygame图形版创建一个独立窗口用像素来绘制文字。优点是可控性高可以做出真正的拖尾、颜色渐变、汉字粒子效果还能接鼠标键盘交互缺点是需要安装第三方库代码量也更大。我的建议是两条都写一遍。终端版帮你理解“雨滴更新”的核心逻辑图形版帮你理解“渲染表现力”的边界在哪里。顺序反过来也行但总归要两条腿走路。2. 终端版30分钟内跑起来的极简中文雨2.1 环境准备与中文字符的第一道坎终端版只需要Python 3.8及以上版本不需要安装任何第三方库。环境配置其实很简单真正的第一道坎是中文输出。这里我先说结论中文在终端里显示不正常不是你的代码写错了是输出链路上的编码不一致。Python 3源码默认是UTF-8但Windows的控制台默认编码可能是GBK导致中文经过标准输出之后变成乱码。我在Windows上第一次运行的时候就遇到这个问题解决方式是在脚本里强制把stdout重设为UTF-8。同时Windows控制台默认不支持ANSI转义序列需要调用系统接口开启虚拟终端模式。macOS和Linux终端一般开箱即用不需要做这一步处理。以下配置方案是基于我的实际操作整理出来的Windows用户直接抄。import sys if sys.platform win32: import ctypes kernel32 ctypes.windll.kernel32 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) if hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8)2.2 列宽处理一个汉字占两个英文列英文数字雨里每个字符占一列所以雨滴的横向坐标可以跟终端列号一一对应。中文不同一个汉字在终端里占两个半角字符位宽。如果无视这个差异你会看到雨滴在下落过程中左右摇摆原本竖直的一串字变成锯齿状。解决办法很简单把终端宽度除以2得到的数字才是能容纳汉字的列数。这个处理非常关键后面所有渲染逻辑都要围绕这个修正后的列数展开。字符池方面我建议直接用一个中文字符串作为语料。不用太复杂从古诗里挑几句表现力就已经很够了。不同雨滴的头部和尾部会随机采词画面不会重复。2.3 终端动态刷新让字符跑起来终端里做动画核心方法是每帧把光标移回左上角然后整屏重写而不是调用清屏命令。清屏命令在Windows上会闪烁得很厉害而且容易留下残影。ANSI控制码有几个常用的\033[H把光标移动到左上角\033[?25l隐藏光标\033[?25h恢复光标\033[32m设置前景色为绿色\033[37;1m设置前景色为亮白色我做这套方案的时候重点设计了雨滴对象的结构。每个雨滴只负责自己所在的列记录头部所在的行数、下落速度、拖尾长度和相关字符。头部行数不断增加当超出屏幕高度时随机重置到顶部之外。渲染时把每个雨滴的头部和尾部字符填入一个二维字符串数组每个格子要么是汉字要么是两个空格。这样整行重写时即使原来的字符消失了也能被空格覆盖掉不会留下旧帧的残留。2.4 可直接复制的终端版完整代码下面是我调试过的一版完整代码你新建一个py文件直接粘进去运行即可。# -*- coding: utf-8 -*- import sys import time import random from shutil import get_terminal_size if sys.platform win32: import ctypes kernel32 ctypes.windll.kernel32 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) if hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8) CJK list( 床前明月光疑是地上霜举头望明月低头思故乡 海上生明月天涯共此时情人怨遥夜竟夕起相思 空山新雨后天气晚来秋明月松间照清泉石上流 ) class ColumnDrop: 单个雨滴垂直下落带着一段拖尾 def __init__(self, height20): self.head random.randint(-height, 0) self.speed random.uniform(0.3, 1.0) self.length random.randint(3, 8) self.char random.choice(CJK) def step(self, height): self.head self.speed if self.head height: self.head random.randint(-8, -2) self.speed random.uniform(0.3, 1.0) self.length random.randint(3, 8) self.char random.choice(CJK) def render(drops, cols, lines): rows_chars [[ for _ in range(cols)] for _ in range(lines)] for col, drop in enumerate(drops): y int(drop.head) if 0 y lines: rows_chars[y][col] \033[37;1m drop.char \033[0m for offset in range(1, drop.length): yy y - offset if 0 yy lines: rows_chars[yy][col] \033[32m random.choice(CJK) \033[0m frame_lines [.join(row) for row in rows_chars] sys.stdout.write(\033[H \n.join(frame_lines)) sys.stdout.flush() def main(): size get_terminal_size() cols size.columns // 2 lines size.lines drops [ColumnDrop(heightlines) for _ in range(cols)] sys.stdout.write(\033[?25l\033[32m) try: while True: size get_terminal_size() new_cols size.columns // 2 new_lines size.lines while len(drops) new_cols: drops.append(ColumnDrop(heightnew_lines)) drops drops[:new_cols] for d in drops: d.step(new_lines) render(drops, new_cols, new_lines) time.sleep(0.04) except KeyboardInterrupt: sys.stdout.write(\033[?25h\033[0m) sys.stdout.flush() if __name__ __main__: main()如果只想快速验证把time.sleep(0.04)调大一点也没关系。速度是0.04秒一帧大约25帧每秒在终端里已经算流畅了。2.5 跑起来之后你会看到什么运行这段代码后终端会呈现一屏从古诗里随机抽取的汉字以列为单位不断向下坠落。每串文字的头部是亮白色下面的拖尾是绿色。速度不均匀有的列快有的列慢这样画面才会有层次感。按下CtrlC退出时程序会恢复光标并重置颜色。如果运行过程中发现汉字依然乱码大概率是编码设置没生效检查一下是不是在系统自带的终端里运行而不是在某个不支持ANSI的编辑器的内嵌控制台里。终端版的视觉效果可以说“能跑”但离“好看”还有距离。真正的质变发生在pygame版本上。3. pygame版真正能看的文字雨3.1 为什么终端版只能算“能跑”终端版的限制在于它只能打印字符没法做真正的半透明残影。文字雨的观赏性一半来自字符本身另一半来自“亮度衰减”。头部亮、尾部淡这种渐变效果终端根本做不出来。另外终端里的中文字体宽度还受终端主题影响不同环境跑出来的效果千差万别。pygame就完全不同了它自己创建窗口自己控制每一个像素和透明度画面效果完全由代码决定。所以如果你对视觉效果有要求直接上pygame。它的安装方式很简单pip install pygame。如果默认源比较慢换一个可靠的镜像源或者用离线安装包都可以。3.2 中文字体决定成败的隐藏因素pygame的字体模块有个容易踩的坑pygame.font.SysFont(任意名字, size)在找不到指定字体时不会报错而是悄悄回退到默认字体。默认字体往往没有中文字形结果就是满屏方框。我的做法是写一个专门的字体加载函数先遍历常见中文字体名通过pygame.font.match_font确认系统中确实存在这个字体再加载如果全部失败再用pygame.font.get_fonts()按关键词搜索所有可用字体直到找到包含cjk、hei、song、kai、ming、noto、wqy等关键字的字体。这个检查过程很有必要因为它能直接暴露系统没有中文字体的问题。实测下来Windows上微软雅黑基本都能命中macOS通常能命中PingFangLinux上如果装了文泉驿或者Noto字体也能顺利通过。3.3 雨滴模型、拖尾与帧循环图形版里每个雨滴有四个核心属性横向列位置、纵向坐标、下落速度、拖尾长度。与终端版不同图形版的纵向坐标是浮点数更新时连续增加渲染时再转成整数像素位置。这样雨滴的下落是平滑的不会一格一格地跳。拖尾的实现有两种常见思路。一种是用列表保存每个雨滴最近的历史位置和字符绘制时给历史字符分配不同的亮度另一种更偷懒但效果更自然——每帧往主画面上叠加一层半透明的黑色矩形。旧画面会随帧数逐渐变暗新字符保持明亮残影效果自动出现。我采用第二种思路因为它的沉浸感更强旧字符不是直接被清掉而是慢慢隐没在黑暗里很像电影里的特效。3.4 完整可运行的pygame版代码# -*- coding: utf-8 -*- import sys import random import pygame pygame.init() WIDTH, HEIGHT 1200, 800 FONT_SIZE 28 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(中文文字雨) clock pygame.time.Clock() CJK list( 床前明月光疑是地上霜举头望明月低头思故乡 海上生明月天涯共此时情人怨遥夜竟夕起相思 空山新雨后天气晚来秋明月松间照清泉石上流 ) def load_cjk_font(size): names [ microsoftyahei, simhei, simsun, microsoftjhenghei, pingfangsc, notosanscjksc, wqymicrohei, wenquanyimicrohei ] for name in names: if pygame.font.match_font(name): return pygame.font.SysFont(name, size) for info in pygame.font.get_fonts(): if any(k in info for k in (cjk, hei, song, kai, ming, noto, wqy, han)): return pygame.font.SysFont(info, size) raise RuntimeError(系统没有可用的中文字体请先安装中文字体) font load_cjk_font(FONT_SIZE) cell_w, cell_h font.size(测) char_cache {} def get_char_surface(ch, color): key (ch, color) if key not in char_cache: char_cache[key] font.render(ch, True, color) return char_cache[key] class Drop: def __init__(self, x): self.x x self.y random.uniform(-HEIGHT * 0.5, 0) self.speed random.uniform(1.0, 3.5) self.length random.randint(5, 14) self.trail [] def step(self): self.y self.speed self.trail.append((self.y, random.choice(CJK))) if len(self.trail) self.length: self.trail.pop(0) if self.y HEIGHT 60: self.reset() def reset(self): self.y random.uniform(-HEIGHT * 0.3, 0) self.speed random.uniform(1.0, 3.5) self.length random.randint(5, 14) self.trail [] drops [Drop(x * cell_w) for x in range(WIDTH // cell_w)] fade pygame.Surface((WIDTH, HEIGHT)) fade.set_alpha(30) fade.fill((0, 0, 0)) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.blit(fade, (0, 0)) for drop in drops: drop.step() for drop in drops: for i, (yy, ch) in enumerate(drop.trail): if i len(drop.trail) - 1: color (230, 255, 230) else: ratio i / max(1, len(drop.trail) - 1) level int(ratio * 3) / 3 green 80 int(140 * level) color (0, green, 0) surf get_char_surface(ch, color) screen.blit(surf, (drop.x, int(yy))) pygame.display.flip() clock.tick(60) pygame.quit() sys.exit()这段代码直接运行你会看到一个1200x800的窗口里面汉字像雨一样不停下落。头部字符接近白色尾部逐渐变绿且颜色是平滑衰落的。3.5 字体缓存可能被忽略的性能关键pygame里每帧都要渲染大量的汉字Surface对象。如果每次都调用font.render性能会很差窗口一大、雨滴一密帧率立刻掉到30以下。解决方式是缓存。同一个字符、同一种颜色只渲染一次后面直接从字典里取。这个优化立竿见影可以把帧率稳定在60帧。我代码里还有一个细节将颜色档位离散化用ratio * 3 / 3把连续的渐变压缩成几个固定档位。这样颜色样本数量可控缓存命中率更高画面看起来依然是渐变的但底层性能开销小了很多。4. 中文文字雨避坑实录4.1 乱码不是玄学是编码链路断了中文文字雨最典型的翻车现场是代码能跑但中文变成问号或者方块。常见原因有两个。第一个是终端编码问题。这个在Windows上尤其多见。Python输出中文到stdout时终端如果按GBK解码UTF-8就会乱码。解决方式就是前面代码里写的sys.stdout.reconfigure(encodingutf-8)或者在系统终端执行chcp 65001。注意这个操作不要在代码里写死成只有Windows才执行其他系统要保持原样。第二个是pygame使用了不支持的字体。前面已经强调过SysFont找不到字体时不会报错只会在窗口里显示方框。判断字体是否真的支持中文就靠pygame.font.match_font加关键词搜索如果找不到就主动抛异常不要让程序默默用错误字体跑。4.2 错位中文字符的宽度不是1运行文字雨时出现“同一列的字左右乱晃”的问题基本可以确定是列宽计算错误。在终端里英文字符宽度记为1中文字符宽度记为2。代码里必须先columns // 2再把结果作为雨滴列数。如果不除一个汉字会占据两个列的位置下一帧字符的位置就会错位。在pygame里每个中文字符的宽高由字体决定不同字体的单元格尺寸不一样。不要用固定像素值计算列距要用font.size(测)拿到实际渲染尺寸再按这个尺寸去排布雨滴的横向坐标。4.3 卡顿从每帧新建Surface到字符缓存如果你的显卡不差、窗口也不大但帧率低问题多半出在内存碎片上。最简单有效的优化方向是缓存所有渲染好的Surface对象。字符总数量有限颜色档位也有限组合一下最多几百个Surface完全可以全部缓存下来。反过来如果每次都写font.render(...)一帧里上千次调用就是对CPU和内存分配器的双重攻击。另外雨滴数量不要贪多一列一个雨滴就够了过于密集反而会让视觉焦点分散看不清拖尾层次。4.4 常见问题速查表症状大概率原因解决手段Windows控制台中文显示为问号输出编码链路是GBKstdout强制UTF-8或chcp 65001字符显示为方框/豆腐块运行环境缺少中文字体检查match_font加载系统现有中文字体同一列的汉字左右错位中文字符宽度未按2计算终端列数除以2画面严重闪烁每帧清屏或重写范围过大使用ANSI光标复位避免清屏命令pygame帧率低每帧重复渲染文字Surface字符缓存颜色档位离散化雨滴下落是一跳一跳的y坐标取整方式不对用浮点坐标累加绘制时再转int5. 玩法扩展让文字雨变成你的内容载体5.1 语料定制从单字到词组的陷阱把雨滴的语料从单字换成词组画面会更有内容感。但这里有一个容易忽略的问题词组长度不一致会导致拖尾参差不齐。比如你随机的词组有时候是“你好”有时候是“人工智能”两个字和四个字的渲染宽度完全不同雨滴的尾部就会长短不一整列的下落节奏也会乱掉。我的建议是坚持用“单字池”或者把所有词组固定为同一个字符数。真想做成弹幕风格的文字雨就从成语库或者两个字库中统一取样。如果你想要热词动态更新效果可以让程序启动时从一个文本文件里读取词库这样不需要改代码就能换内容。5.2 颜色、风向与交互颜色主题可以玩出很多花。绿色是经典款头部亮白、尾部深绿。换个思路头部用金色、尾部用暗红就是赛博古风。再抽象一点按雨滴的横向坐标给不同列分配不同色相出来的画面就是彩虹雨。交互方面最实用的改造是在pygame事件循环里监听鼠标位置用鼠标x坐标去控制雨滴速度。比如鼠标越靠右雨速越快按空格键切换语料主题。这类改动成本很低但演示给朋友看的时候交互性会让人明显觉得“这玩意儿是活的”。5.3 把文字雨变成视频/动图做出来的效果还想拿去发个短视频可以把pygame的每一帧保存成PNG图片序列再用ffmpeg合成视频。保存帧的做法是在主循环里加一行pygame.image.save(screen, fframe_{frame_count:06d}.png)等程序跑完用一条命令把图片序列转成视频ffmpeg -framerate 30 -i frame_%06d.png -c:v libx264 -pix_fmt yuv420p rain.mp4如果要动图就把编码换成-vf fps20,scale800:-1 rain.gif。这样不用额外安装录制软件也可以得到清晰流畅的输出。这个内容后续还可以扩展成桌面屏保、直播弹幕挂件甚至放进某种创意展厅里当背景。起点不高但延伸空间不小。我自己做完这两版之后最大的感受是视觉特效这件事80%的功夫都在“亮度管理”上。怎么让新的字符亮起来、旧的字符慢慢消失才是画面质感的来源。剩下20%在字体选择上中文字体选对了整个效果就成功了一大半。建议你先抄终端版把原理跑通然后再花一个周末把pygame版调成自己的风格。最后提醒一句拖尾长度和速度一定要做成可调参数别人看的时候真正会被吸引住的就是这两个旋钮。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑