☰
用Python和Pillow自制表情包生成器:图像处理入门实战
2026/10/1 4:54:32 网站建设 项目流程

做这个项目,起因特别简单:办公室里总有同事在群里斗图,手上的表情包来来去去就那么几张,一点也不解气。我当时正在带一个零基础的朋友学 Python,教到文件操作和字符串处理的时候,明显感觉他有点学不动了——语法书上的例子太“习题化”,跟现实生活完全没有连接。后来我一想,干脆写个脚本,让他用代码自己生成表情包:输入一句话,脚本把字压到图片上,自动换行、自动居中、批量出图。结果他玩了一下午,把全家福都做成表情包了,顺带还把 PIL 的常用 API 记了个七七八八。

这件事让我意识到,“用代码自动生成表情包”是个非常适合入门 Python 的实战项目。它不像爬虫那样有各种反爬坑,也不像量化策略那样需要数学基础,只需要用到 Python 最基础的数据类型、字符串处理、函数封装和文件读写,就能做出一个真正有用的工具。今天把这套完整方案写出来,从环境准备到核心代码,再到我踩过的坑,一次性讲透。适合刚学完 Python 基础语法、想找个不劝退的练手项目的同学,也适合想用 Python 处理日常图片但不知从何下手的朋友。

1. 项目思路与方案选型

1.1 为什么做表情包生成器,它能解决什么问题

很多人学 Python 容易陷入一个循环:看教程——抄代码——忘掉——再看教程。原因很简单,学的知识和“我要做什么”之间缺少一个桥梁。而表情包生成器天然具备三个优点,使其非常适合当第一个独立实操项目。

第一,反馈足够强。写一个小脚本,跑完立刻就能看到一张带字的表情包,这种即时成就感比看一百页语法都要管用。第二,覆盖的知识点非常集中且基础:变量、字符串拼接、循环遍历、函数定义、文件读取、第三方库的安装和调用,这些全部是 Python 入门必须掌握的内容,且不需要引入任何复杂框架。第三,它确实能高频使用。做完之后,你以后想配个带文字的图,可以直接跑自己的脚本,而不是打开 PS 手动拉文本框。

所以这个项目的本质,并不是“做表情包”本身,而是通过一个高趣味性的场景,把 Python 的基础能力串联起来。明白了这一点,后面具体学什么函数、用什么库,就都有方向了。

1.2 技术选型对比:为什么是 Python + Pillow

实现给图片加文字这件事,技术方案其实不止一种。我在动手之前专门对比过几个方向,列个表格方便大家看:

方案学习成本批量处理能力中文支持适合人群
Python + Pillow低,API 直白强,脚本循环即可需额外指定中文字体入门首选
OpenCV中高,需理解图像矩阵强同样需字体处理有图像处理基础的人
Photoshop 动作中,需录制定制动作弱,每次要手动处理素材好不想写代码的人
HTML + Canvas中,涉及前端知识一般好前端方向学习者
在线表情包工具几乎为零无依赖平台模板随手使用,不适合批量

最终我选择了 Python + Pillow。原因很简单:Pillow 是 Python 图像处理的事实标准库,API 设计非常接近日常直觉——打开图片、画一条线、写一段文字,每一步都有对应的函数,几乎没有抽象概念需要理解。而且它只是一个普通 Python 库,通过pip安装即可,不需要配置复杂的环境。相比之下 OpenCV 更适合做边缘检测、人脸识别这类真正的图像处理任务,用来写文字属于“杀鸡用牛刀”,而且新手很容易在 BGR 和 RGB 之间绕晕。

1.3 功能拆解:输入、处理和输出

任何程序都跑不出“输入—处理—输出”这条线,表情包生成器也一样。我把需求拆成了三个模块,这样写代码的时候思路清晰很多。

输入部分,有两种:一是模板图片,也就是表情包的底图,它决定了表情包的尺寸和风格;二是文字内容,也就是你想在图上配的那句话。这两者既是必需的,也是后续可以做随机化的关键变量。

处理部分,是核心逻辑。收到一张图片后,程序需要做几件事:读取图片并获取宽高;创建绘画上下文;加载字体;计算文字宽度和高度;把文字居中对齐到指定位置;最后写入图片。如果文字太长,还得按像素宽度动态换行。这部分就是本项目的技术重点,后面会专门展开讲。

输出部分,最简单,把处理完的图像保存为文件。我会生成到独立的output目录里,方便批量查看。理解了这个基础闭环,后面无论怎么加功能——随机文案、批量生成、多个模板切换——都是在处理环节做文章,不会乱。

2. 环境准备与工具链

2.1 Python 安装与 Pillow 库安装

这个项目对 Python 版本要求不高,3.8 及以上都能跑。还没装 Python 的话,去官网下载安装包,安装时务必勾选“Add Python to PATH”这个选项,否则之后在命令行里敲python会提示找不到命令。装完可以在终端里验证一下:

python --version

能正常输出版本号就说明基础环境没有问题。

接着安装 Pillow。这是整个项目唯一的外部依赖,只需要执行这个命令:

pip install pillow

如果你在国内,下载速度不理想,可以用清华的镜像源加速:

pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple

装完建议顺手确认一下版本,Pillow 迭代频率还挺高的,不同版本之间个别 API 有些差异:

pip show pillow

这里补充一个新手极易踩的坑:如果你电脑上同时装了 Python 2 和 Python 3,或者用了一些集成环境,命令行里的pip可能指向的不是你想用的那个 Python。最稳妥的办法是直接用python -m pip install pillow,这样能保证把库装到当前python命令对应的环境里。

2.2 VS Code 开发环境配置要点

写代码的编辑器,我推荐 VS Code,免费、跨平台、插件生态好。针对 Python 开发,只需要装一个官方 Python 扩展插件就够了。

装完插件后有个关键动作:按Ctrl + Shift + P打开命令面板,搜索Python: Select Interpreter,选择你安装 Python 的路径。如果没有正确选择解释器,VS Code 会一直用默认环境,导致运行时代码里import PIL直接报 ModuleNotFoundError,但你可能还莫名奇妙:明明 pip 装过了啊?

VS Code 里运行 Python 脚本的方式就两种:要么点击右上角的三角形运行按钮,要么在终端里执行python 文件名.py。我建议新手养成用终端手动运行的习惯,因为这样更贴近脚本语言的原始用法,后续部署或者定时任务也更容易理解。

2.3 中文字体和素材准备,这一步别偷懒

写英文文本直接可以用系统默认字体,但要往表情包上写中文,必须显式指定一个中文字体文件。这是因为 Pillow 默认字体根本不包含中文字符,你必须加载一个.ttf或.ttc字体文件,它才知道“汉字”长什么样子。

Windows 系统自带字体一般放在C:/Windows/Fonts,常用的有msyh.ttc(微软雅黑)、simhei.ttf(黑体)、simsun.ttc(宋体)。macOS 用户可以用C:/Windows/Fonts换一下路径,也能找到PingFang.ttc。Linux 用户则可能需要安装文泉驿或者 Noto Sans CJK 字体。为了省事,我强烈建议把选好的字体文件复制到你的项目目录下,比如命名为font.ttf,然后代码里直接用相对路径引用,这样换电脑或者分享代码,不用改路径就能直接跑。

素材方面,准备 4 到 5 张表情包底图,统一放到项目根目录的templates文件夹里。尺寸不用太纠结,代码里会对图片做适当处理,但尽量用竖屏或正方形的图,效果会更像一个正经表情包。

3. 核心代码实现与逐段解读

3.1 从打开一张图片开始:Pillow 的基本流程

很多教程喜欢一上来就丢一个完整成品代码,然后让读者复制运行。我不太推荐这种学法,因为一旦出错了你根本不知道问题在哪一层。更稳妥的方式是分步构建,每一步都能验证、能看到中间结果。

先写最小的图像处理脚本:打开图片、缩放到一个合适尺寸、另存为新文件。

from PIL import Image # 打开模板图片,convert("RGB") 是为了统一颜色模式,后面细说 img = Image.open("templates/demo.jpg").convert("RGB") print("图片尺寸:", img.size) # 统一缩放到宽度 600,高度按比例自动计算 width = 600 height = int(img.height * width / img.width) img = img.resize((width, height)) print("缩放后尺寸:", img.size) # 保存到输出目录 img.save("output/demo_resized.jpg")

先跑通这一步,你就能直观理解两个核心概念:一是img.size返回一个包含宽和高的元组(width, height),后面所有居中算法都要依赖这个值;二是resize需要传入一个包含两个元素的元组作为新尺寸,而且它默认不会保持宽高比,所以你在代码里手动按比例计算高度。

我在这里特别强调convert("RGB")这个操作。后面往图上写字,保存成 JPG 格式,如果不做转换,非常容易遇到一个报错:cannot write mode RGBA as JPEG。原因是 PNG 图片通常是 RGBA 四通道,带透明度,而 JPG 不支持透明通道,Pillow 不知道该怎么处理。提前统一转成 RGB 是最省心的做法。

3.2 在图片上写字:字体加载、颜色与居中计算

接下来给图片加文字。这需要用到两个新的类:ImageDraw和ImageFont。ImageDraw相当于给你一张画布上的画笔,ImageFont则负责告诉画笔画出来的字长什么样。

看这段完整示例:

from PIL import Image, ImageDraw, ImageFont img = Image.open("templates/demo.jpg").convert("RGB") width = 600 height = int(img.height * width / img.width) img = img.resize((width, height)) draw = ImageDraw.Draw(img) font = ImageFont.truetype("font.ttf", size=48) text = "今天也要加油鸭" # textbbox 返回文本外接矩形的 (左, 上, 右, 下) bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # 水平居中:画布宽度减去文字宽度,再除以 2 x = (img.width - text_width) / 2 # 垂直位置:放在图片下方,留 30 像素边距 y = img.height - text_height - 30 # 在图片上绘制文字,fill 参数就是文字颜色 draw.text((x, y), text, font=font, fill=(255, 255, 255)) img.save("output/meme_01.jpg")

这里最值得研究的是居中公式。很多第一次写的人会直接把文字写死在坐标(100, 100)上,结果换了一张图,字就跑偏了。正确思路必须先测量出文字本身多宽多高,然后用“画布尺寸减去文字尺寸,取一半”这个思路算起点坐标,这样才能保证不管图片多大、文字多长,永远居中。

textbbox这个方法是我强烈建议你记牢的。它返回的四个值分别表示文字边界框的左、上、右、下坐标,你只需要用右边减左边、下边减上边,就能得到精准的文字宽高。有人可能见过老代码里用draw.textsize()来测量文字,这个方法在新版 Pillow 里已经弃用了,别再照抄老博客的代码。

3.3 自动换行逻辑:按像素宽度断行,而不是按字数

表情包文字经常是一长串,如果直接画上去,字就会超出图片边界,非常难看。大多数入门教程不会涉及这个问题,但它是实际使用中最影响体验的环节。

我的思路是:把一句话拆成一个一个的字符,从第一个开始不断往候选行里追加,每追加一个字符就测量一次当前行的像素宽度;只要宽度不超过设定的最大值,就继续追加;一旦超了,就把当前候选行保存下来,重新开始装下一行。这样就能严格按图片宽度换行,而不是按字数粗鲁地切。

def wrap_text(draw, text, font, max_width): """按像素宽度自动换行,返回一个字符串列表,每项是一行文字""" lines = [] current = "" for char in text: test = current + char bbox = draw.textbbox((0, 0), test, font=font) w = bbox[2] - bbox[0] if w <= max_width: current = test else: lines.append(current) current = char if current: lines.append(current) return lines

注意一个细节:我这里用的是“逐字符判断”,而不是按英文单词的空格换行。原因很简单,中文按字切割不会破坏语义结构,而且表情包文案本来就是短句,逐字符是最通用、代码也最简单的方案。如果遇到中文夹杂英文的情况,也基本能正常切,顶多断在一个单词中间,但不影响表情包使用效果。

调用也很简单,max_width一般设为图片宽度的八成左右,留出左右边距:

max_width = int(img.width * 0.8) lines = wrap_text(draw, text, font, max_width) # 先算出总高度,决定第一行文字的起始 y 坐标 line_height = int(font.size * 1.3) total_height = line_height * len(lines) y = img.height - total_height - 20 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) w = bbox[2] - bbox[0] x = (img.width - w) / 2 draw.text((x, y), line, font=font, fill=(255, 255, 255)) y += line_height

这里的line_height用font.size * 1.3,是因为汉字字形本身有一定内边距,如果行距恰好等于字号,会显得非常拥挤。1.3 倍这个值是我试过多个选项后觉得视觉最舒服的,你也可以根据喜好调成 1.2 到 1.5 之间。

3.4 随机表情包生成器:多模板、多文案、批量输出

把上面的功能封装成函数,再引入随机模块,一个真正的“表情包生产工具”就完成了。

import os import random from PIL import Image, ImageDraw, ImageFont FONT_PATH = "font.ttf" FONT_SIZE = 48 OUTPUT_DIR = "output" TEMPLATE_DIR = "templates" # 内置文案列表,你也可以扩成外部文件 phrases = [ "今天也要加油鸭", "我太难了", "确认过眼神", "你不对劲", "懂的都懂", "这就是你找的理由?", ] def wrap_text(draw, text, font, max_width): lines = [] current = "" for char in text: test = current + char bbox = draw.textbbox((0, 0), test, font=font) w = bbox[2] - bbox[0] if w <= max_width: current = test else: lines.append(current) current = char if current: lines.append(current) return lines def make_meme(template_path, text, output_path): img = Image.open(template_path).convert("RGB") width = 600 height = int(img.height * width / img.width) img = img.resize((width, height)) draw = ImageDraw.Draw(img) font = ImageFont.truetype(FONT_PATH, FONT_SIZE) max_width = int(img.width * 0.8) lines = wrap_text(draw, text, font, max_width) line_height = int(font.size * 1.3) total_height = line_height * len(lines) y = img.height - total_height - 20 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) w = bbox[2] - bbox[0] x = (img.width - w) / 2 draw.text((x, y), line, font=font, fill=(255, 255, 255)) y += line_height img.save(output_path) if __name__ == "__main__": os.makedirs(OUTPUT_DIR, exist_ok=True) template_files = [f for f in os.listdir(TEMPLATE_DIR) if f.lower().endswith((".jpg", ".jpeg", ".png"))] if not template_files: raise FileNotFoundError("templates 文件夹里没有找到图片模板,请先放几张底图!") for i in range(10): tpl = random.choice(template_files) phrase = random.choice(phrases) output = os.path.join(OUTPUT_DIR, f"meme_{i:02d}.jpg") make_meme(os.path.join(TEMPLATE_DIR, tpl), phrase, output) print(f"已生成: {output}")

核心逻辑都在make_meme这个函数里,这也是热词里“python定义函数”最好的实践场景。把重复代码抽成函数之后,每个生成流程只需要一行调用,主程序的可读性大幅提升。

主程序最后那段if __name__ == "__main__":,是一个值得养成的习惯。它保证脚本被直接运行时才执行循环,而作为模块被别的代码导入时不会自动跑,方便后续你把生成器封装给其他程序调用。

运行之后,output文件夹里会多出十张随机组合的表情包。我实测过,从读取图片到保存成品,平均一张只需要零点几秒,十张完全是一瞬间的事。这个批量能力,是手动作图完全无法比拟的优势。

4. 进阶玩法:打磨效果与批量生产

4.1 文字描边与阴影,让字在任何底图上都看得清

表情包底图往往颜色复杂,白色文字压上去,遇到浅色背景部分很容易看不清。最简单有效的解决方案是给文字加黑色描边。Pillow 的draw.text方法自带描边参数,不需要额外计算,直接加两个参数即可:

draw.text( (x, y), line, font=font, fill=(255, 255, 255), # 白色文字 stroke_width=2, # 描边宽度,单位是像素 stroke_fill=(0, 0, 0), # 黑色描边 )

stroke_width设为 2 或 3 就已经很醒目了,太大字会显得臃肿。如果想让文字更有层次感,还可以用“阴影法”:先把文字用深灰色在原始坐标偏移 3 到 5 个像素的位置画一遍,再在原始坐标画一遍白色文字,视觉上就是一层浅浅的投影。这个办法在表情包上尤其好用,因为黑边白字虽然是标配,但加一点阴影会更像正经设计软件做出来的效果。

4.2 文案库外置:用 CSV 批量导入文字

内置文案列表适合演示,但真正高频用的话,每次都要改代码里的列表,很蠢。更合理的做法是把文案放在外部文本文件里,一个 CSV 文件,每行一条文案,程序读取后随机选。

import csv def load_phrases(csv_path): with open(csv_path, encoding="utf-8") as f: reader = csv.reader(f) return [row[0].strip() for row in reader if row and row[0].strip()]

这里必须写encoding="utf-8",否则在 Windows 上默认用 GBK 编码读取文件,遇到 CSV 里的中文会直接乱码或者报UnicodeDecodeError。这也顺带解答了一个很多新手困惑的问题:为什么别人代码里打开文件博文都要加 encoding 参数。

如果你不想用 CSV,用纯文本文件按行存储更简单,readlines()直接得到列表。我推荐 CSV 是因为它天然兼容 Excel,你可以在表格里编辑文案、排序、去重,操作效率远高于直接在代码里改。

4.3 让程序接收外部参数:命令行直接指定文字

随机模式适合批量刷表情包,但更多时候你心里已经有一句特定的话。为此可以在脚本里加个小功能:运行时传入文字参数,传了就生成指定文字的表情包,没传就进入随机模式。

import sys if __name__ == "__main__": os.makedirs(OUTPUT_DIR, exist_ok=True) if len(sys.argv) > 1: text = " ".join(sys.argv[1:]) template = "templates/白底模板.jpg" make_meme(template, text, "output/custom.jpg") print("已生成自定义表情包: output/custom.jpg") else: # 原有随机批量逻辑 pass

这样你就可以在命令行里直接写:

python meme_generator.py 老板明天不上班

一句话对应一张图,特别适合临下班前给同事群里发一张。sys.argv是入门阶段一个很好玩的变量,它是个列表,存放命令行里除脚本本身外的所有参数。" ".join(sys.argv[1:])是为了把多个词重新拼成一句完整的话,避免文件名空格被拆开。

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

5.1 运行报错:由于找不到 msvcp140.dll,无法继续执行代码

这个问题在 Windows 上太经典了,也是搜索热词里常年出现的一条。简单说,msvcp140.dll是微软 Visual C++ 运行库的一部分,很多 Python 包在安装时依赖这个运行库,缺少它,Python 环境或者某些第三方库就无法启动。

解决办法有两个,任选其一:

  • 去微软官网下载 Visual C++ Redistributable 最新版,安装重启;
  • 重新运行 Python 安装包,选择 Repair(修复)模式,安装器通常会帮你补齐缺失的组件。

我遇到过的情况是,装了一些科学计算库之后突然报这个错,修复 VC++ 运行库之后就好了。这类报错跟你的代码没有任何关系,是系统环境层面的问题,不用慌。

5.2 文字变成方框或者乱码,到底哪里出了问题

这是写中文文字时最容易遇到的问题。现象是字确实“画”上去了,但显示出来全是空心的方块,一个汉字都认不出来。原因百分之百是字体文件的问题:要么你没指定字体,用了 Pillow 的默认字体;要么指定的字体路径不对,Pillow 加载失败后悄悄回退到了默认字体。

排查顺序:

  1. 确认字体文件存在,且路径正确;
  2. 确认传入的是完整的绝对路径或相对路径,不是C:/Windows/Fonts/msyh.ttc写成了反斜杠\;
  3. 确认你的字体文件本身支持中文,有些从网上下载的精简字体只包含英文字母,这种也会显示方块;
  4. 调用ImageFont.truetype(font_path, 48)时,检查是否因为路径错误抛出了异常。如果代码没报错但字是方块,八成是路径指向了一个不存在的文件,而旧版 Pillow 在某些场景下会静默回退。

5.3 文字跑出图片边界,压缩字体也无济于事

有朋友问:我把字号调小了,为什么长文字还是超出图片?原因很简单,你只调了字号,但可能没有真正按像素宽度去做换行。如果代码里还是按固定字数切行,比如每 5 个字一行,一旦文案里有全角标点或者英文字母,行的实际像素宽度就会忽宽忽窄,很容易冲出去。

正确做法就是上面wrap_text那种“动态测量、按宽度断行”的逻辑。记住一个原则:图片处理里,凡是跟布局有关的事,都要用像素去度量,不要用字符数去估算。字体、字号、图片尺寸不同,同一个字数的宽度完全不同。

5.4 保存图片时报 cannot write mode RGBA as JPEG

这个报错出现率非常高,尤其是你的模板是透明背景的 PNG 图片时。JPG 格式不支持透明度,而 Pillow 拿到 RGBA 模式的图片,不知道怎么把透明信息“压”进去,就会直接拒绝保存。

解决方式就是那句看似不起眼的代码:

img = Image.open(template_path).convert("RGB")

.convert("RGB")会把 RGBA 图片里的透明区域全部填充成白色,然后变成普通的 RGB 三通道图像,保存成 JPG 完全没有问题。如果你希望透明区域是黑色或者其他颜色,可以先用Image.new()建一张纯色底图,再把 PNG 贴上去。

5.5 VS Code 里能跑,双击.py文件却一闪而过

脚本写好后,双击.py文件通常会弹出一个黑色命令窗口,然后立即关闭,什么都看不到。这其实不是 bug,而是脚本执行得太快,执行完窗口就被系统回收了。

想看清输出结果,有两个办法。一是改在终端里运行,先打开 CMD 或 PowerShell,cd到项目目录,再执行python meme_generator.py。二是在脚本末尾加一行input("按回车键退出..."),这样窗口会一直停住等你按键。我平时调试小工具时从来不加这个,因为始终用终端运行,早就习惯了,但给完全零基础的人演示时,加一行倒是能避免很多尴尬。

写在最后:我的一点实操体会

这个项目做完之后,我自己反而收获很大。最初只是想教朋友一门课,结果发现他学会之后,没过两周就用同样的思路给自己做了个一个批量生成博客配图的脚本。在我看来,这恰恰是入门阶段最该建立的能力:不是记住某个函数,而是看到重复性工作的时候,能顺其自然地想到“能不能写个代码帮我处理掉”。表情包生成器足够简单,简单到不会把你劝退,又足够有用,有用到能让你每天主动打开命令行去运行它。如果你刚学完 Python 基础,我真心建议你照着上面的代码完整敲一遍,哪怕一开始只是修改文案列表,也能清晰感受到代码对日常生活的渗透力。等你把这个流程跑顺了,再去碰爬虫也好、数据处理也好,心里会踏实很多。

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

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

立即咨询