☰
用Python+Pillow自动生成表情包:零基础入门项目实战
2026/10/1 4:54:31 网站建设 项目流程

直接说个结论:Python入门项目里,自动生成表情包几乎是最容易出成果、又最能练到真东西的一个方向。标题看着像玩,实际上涉及了Python最常用的三大块:文件处理、图像操作、批处理逻辑。就算你是零基础,跟着把代码敲出来,也能真真切切看到自己的图片被程序改造成表情包,这份成就感比刷一百道语法题都来得实在。这篇内容围绕“代码自动生成表情包”这件事,把环境搭建、核心代码、常见坑一次讲透,适合刚学Python不久、想找个项目练手的人,也适合想给日常聊天做私藏表情包的老手。

1. 项目核心思路与方案选型

1.1 为什么选这个项目练手

很多入门教程喜欢用“爬虫”“数据分析”当第一个项目,但说实话,这两个方向对新手都不够友好。爬虫要面对反爬、编码、网页结构变化,数据分析则要处理大量的数学概念和pandas操作。表情包生成不一样,它的核心逻辑非常单纯:读入一张图片,在指定位置写上文字,保存出来。整个过程涉及的知识点全是Python基础层面的东西,比如函数定义、循环、文件路径、字符串处理。把这些基本功揉进一个看得见摸得着的项目里,学习效率比单纯看书高很多。

另外这个项目天然带有“分享欲”。你做出来的表情包可以直接发到聊天群里,朋友问你怎么做的,你顺理成章就讲了Python。这种正反馈是自学阶段最缺的东西。我自己带过不少人入门,凡是能拿自己写的工具给身边人用的,基本都坚持下来了。

1.2 技术方案不只一种,为什么选Pillow

生成表情包的技术路线其实有三条。第一条是用现成的在线网站,比如各种表情包生成器,填文字下载图片,好处是零门槛,坏处是完全没法批量处理、没法自动化;第二条是前端Canvas方案,用浏览器画,适合做Web服务,但对本地批量场景太重;第三条就是本文要讲的Python+Pillow方案。

Pillow是Python官方生态里最成熟的图像处理库,它的前身是PIL(Python Imaging Library),虽然PIL早期版本很久没更新,但Pillow作为继承者一直维护到现在。它支持JPEG、PNG、WEBP这些常见格式,能画文字、画矩形、做裁剪、调尺寸,而且API设计得相当人性化,新手照着文档就能写出来。

选Pillow还有一个实际考量:它不需要额外装很多依赖。对比一下,如果用OpenCV做这个项目,光安装和配置环境就够新手折腾一阵;Pillow一条pip命令就能搞定,适合把精力集中在逻辑而不是环境上。表情包本身是轻量图片,Pillow的速度也完全够用,几百张图批量处理不过是几秒钟的事。

1.3 功能边界:做到什么程度算合格

项目开始之前,先想清楚要做到什么程度,这个习惯比写代码本身更重要。我的建议是把目标定在“三步走”:

  • 第一步:能在一张图片上添加自定义文字,手动指定文字内容和保存路径;
  • 第二步:能处理中文不乱码、文字能自动换行、在图片里水平垂直居中;
  • 第三步:能批量处理多张图片,每张图配不同文案,一键输出整批表情包。

如果你还能顺手写个简单的命令行参数,那就更好了,等于学会了argparse这个标准库。别一上来就想做动态表情包、加滤镜、识别脸部做贴纸,那些属于进阶玩法,等基础版跑通后再慢慢加特效不迟。

2. 环境准备:从零装好Python和Pillow

2.1 Python安装:注意勾选PATH

这一步对新手的坑最多,但解决起来也最简单。打开Python官网下载页面,挑一个3.10或更高版本的安装包。安装的时候有两个地方必须注意:第一,安装向导第一页最下面有个“Add Python to PATH”复选框,一定勾上,否则后续在命令行敲python会提示找不到命令;第二,建议选择自定义安装,把“Install for all users”勾上,省得以后权限问题。

装好之后验证一下:Windows上打开cmd,macOS或Linux打开终端,输入python --version,如果输出了版本号,说明安装成功。这里多说一句,千万别装完就急着装库,先确认python命令能用,再继续下一步。

2.2 编辑器选VS Code就够用

编辑器这块不用纠结,Visual Studio Code是目前最稳妥的选项。装好VS Code之后,在扩展市场里搜索“Python”,安装官方发布的Python扩展插件。之后打开一个文件夹,新建一个.py文件,VS Code会自动帮你识别Python解释器,右下角会显示当前使用的Python版本。如果有多个Python版本,手动用Ctrl+Shift+P调出命令面板,输入“Python: Select Interpreter”,选择和你PATH里一致的那个。

新手初期用系统自带的IDLE其实也行,但IDLE的代码补全和调试体验比较弱。我个人建议直接用VS Code,被坑个几次、熟悉了它的一套流程之后,后面写任何Python项目都会顺手很多。

2.3 安装Pillow库:一条命令的事

打开命令行工具,输入:

pip install pillow

如果机器上有多个Python版本,可能要用pip3具体指定对应的pip。装完后同样做一次验证,在命令行输入python进入交互模式,再执行:

from PIL import Image print(Image.__version__)

能输出Pillow的版本号,就说明环境OK。国内下载慢的话,可以在pip命令后面加镜像源:

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

镜像源这个技巧以后装任何其他库都用得上,建议记下来。

3. 核心代码实现:从单张图片到批量流水线

3.1 最基础的版本:一张白纸先画起来

写代码之前,准备一张表情包底图,网上随便找一张搞笑图片就行,或者自己用手机拍一张、截图一块屏幕区域都行。为了测试方便,图片不要太复杂,主体内容居中那种效果最好。

先看第一版代码,功能很简单:打开一张图片,在正中间写上一行字,保存成新文件。

from PIL import Image, ImageDraw, ImageFont # 1. 打开底图 img = Image.open("base.jpg") draw = ImageDraw.Draw(img) # 2. 加载系统字体 font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 50) # 3. 在画布上写字 draw.text((100, 100), "Hello Meme", fill="red", font=font) # 4. 保存结果 img.save("output.jpg")

这段代码基本就是Pillow操作图片的四步曲:打开、生成画布对象、画东西、保存。重点在第3步的draw.text,它的前两个参数分别是文字左上角的坐标和内容,fill是颜色,font是字体文件。

你运行这段代码后,会看到图片左上角出现一行字。到此,最基础的功能已经跑通了,剩下的工作就是把它越做越顺手。

3.2 中文字体问题:为什么显示成方块

把上面代码里的文字换成“你好世界”,大概率会发现在生成的表情包里,中文变成了一堆方块。这个坑几乎人人都会遇到,原因很简单:Pillow只能使用指定的字体文件渲染文字,默认字体里没有中文字形,就会用替代样式,结果就是乱码。

解决办法是手动指定一个系统里带中文的字体文件。不同的操作系统,字体文件路径不一样,常见的有下面这些:

操作系统字体文件路径
WindowsC:/Windows/Fonts/simhei.ttf(黑体)、msyh.ttc(微软雅黑)
macOS/System/Library/Fonts/PingFang.ttc
Linux/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc(文泉驿正黑)

不同电脑字体路径有差异,所以更聪明的做法是写一个探测函数,自动去找系统里可能存在的中文字体:

import os from PIL import ImageFont def get_chinese_font(size): font_candidates = [ "C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyh.ttc", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", ] for path in font_candidates: if os.path.exists(path): return ImageFont.truetype(path, size) raise FileNotFoundError("未找到可用的中文字体文件")

把字体加载换成这个函数之后,中文就正常了。这块理解透了,后面换成其他字体也就是换个路径而已。

3.3 自动换行与文本尺寸计算

表情包的文字通常比较短,但万一你写了很长一句话,超出图片边缘就难看了。所以需要一个“自动换行”的逻辑:按顺序把文字里的每个字符往一行里塞,塞不下就换下一行。

Pillow里有一个方法叫textbbox,可以计算一段文字在给定字体下的实际尺寸。配合这个,换行函数可以这么写:

def wrap_text(draw, text, font, max_width): lines = [] for paragraph in text.split("\n"): line = "" for char in paragraph: if draw.textbbox((0, 0), line + char, font=font)[2] <= max_width: line += char else: lines.append(line) line = char lines.append(line) return lines

这里textbbox返回一个四元组,分别是(x0, y0, x1, y1),其中x1减去x0就是文字总宽度。逐字判断,比按单词换行更适合中英文混排的场景。拿到换行结果后,再计算这段文字的总体高度,就能实现水平垂直居中:

# 计算所有行拼起来的总高度 line_height = 0 total_height = 0 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) line_height = bbox[3] - bbox[1] total_height += line_height + 10 # 行距设为10 # 从图片中心反推起始坐标 y = (img.height - total_height) // 2 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) line_width = bbox[2] - bbox[0] x = (img.width - line_width) // 2 draw.text((x, y), line, font=font, fill="white", stroke_width=4, stroke_fill="black") y += line_height + 10

这里用到了stroke_width和stroke_fill参数,这是Pillow从5.3版本开始支持的功能,相当于给文字加了一圈描边。描边是表情包文字的灵魂,白字黑边,即使底图花里胡哨,文字也清晰可见。

3.4 底部黑条经典风格

除了自由排版,还有一种特别经典的样式:图片下方压一条黑色横条,白字写在横条上。这种风格最像早期聊天软件的表情包,而且实现起来也不复杂,本质就是在图片底部画一个矩形,然后把文字位置锁定在矩形区域内。

核心改动只有两个地方:先画矩形,再写字。矩形用draw.rectangle绘制,从图片底部往上数100像素的位置开始:

bar_height = 100 draw.rectangle( [(0, img.height - bar_height), (img.width, img.height)], fill="black" )

画完黑条之后,文字不需要再居中于整张图片,而是居中于黑条区域。把之前居中逻辑里的img.height换成bar_height,img.height - bar_height作为起点偏移就行了。这样做出来的表情包,观感非常接近微信里那些经典配字图。

3.5 批量生成:配置文件驱动多张底图

单张做完之后,批量处理才是自动化的真正意义。设想一个常见的场景:你手上有50张底图,想给每张图配一句专门的文案,然后一次生成50个表情包。这时候只要准备一个“图片路径 + 文字内容”的映射关系,循环调用处理函数就行了。

一个比较省事的做法是用Python列表直接写死对应关系。比如:

tasks = [ {"image": "meme01.jpg", "text": "我实在没法跟你解释"}, {"image": "meme02.jpg", "text": "因为我只是个表情包"}, {"image": "meme03.jpg", "text": "你说得对但我不听"}, ] for task in tasks: generate_meme(task["image"], task["text"])

用列表里嵌套字典的方式,可读性好,也方便以后扩展字段,比如加font_size、position这些参数。把生成逻辑封装成generate_meme函数后,主流程非常干净。

如果你想完全不碰代码就改文案,可以把任务配置放到一个JSON文件里,程序启动时读取。这个进阶小技巧适合已经跑通基础版的读者:

import json with open("tasks.json", "r", encoding="utf-8") as f: tasks = json.load(f)

这样做的好处是,以后你只需要编辑JSON文件里的文字,不需要再碰Python代码。给非技术朋友用你的工具时,这个体验会友好很多。

3.6 完整示例代码串起来

前几节的小片段拆得比较碎,这里把完整脚本整合出来,方便你复制运行。

import os import json from PIL import Image, ImageDraw, ImageFont FONT_CANDIDATES = [ "C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyh.ttc", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", ] def get_chinese_font(size): for path in FONT_CANDIDATES: if os.path.exists(path): return ImageFont.truetype(path, size) raise FileNotFoundError("未找到可用的中文字体") def wrap_text(draw, text, font, max_width): if not text: return [""] lines = [] for paragraph in text.split("\n"): line = "" for char in paragraph: if draw.textbbox((0, 0), line + char, font=font)[2] <= max_width: line += char else: lines.append(line) line = char if line: lines.append(line) return lines def make_meme(bg_path, out_path, text, font_size=55, bottom_bar=True, bar_height=110): img = Image.open(bg_path).convert("RGBA") draw = ImageDraw.Draw(img) font = get_chinese_font(font_size) if bottom_bar: draw.rectangle( [(0, img.height - bar_height), (img.width, img.height)], fill="black" ) area_height = bar_height y_start = img.height - bar_height else: area_height = img.height y_start = 0 max_width = img.width - 40 lines = wrap_text(draw, text, font, max_width) line_height = 0 total_height = 0 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) line_height = bbox[3] - bbox[1] + 10 total_height += line_height y = y_start + (area_height - total_height) // 2 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) line_width = bbox[2] - bbox[0] x = (img.width - line_width) // 2 draw.text( (x, y), line, font=font, fill="white", stroke_width=4, stroke_fill="black" ) y += line_height img.convert("RGB").save(out_path, quality=95) if __name__ == "__main__": if os.path.exists("tasks.json"): with open("tasks.json", "r", encoding="utf-8") as f: tasks = json.load(f) else: tasks = [ {"image": "meme01.jpg", "text": "我实在没法跟你解释", "out": "out01.jpg"}, {"image": "meme02.jpg", "text": "嘴在撒谎但眼睛不会", "out": "out02.jpg"}, ] os.makedirs("output", exist_ok=True) for task in tasks: out_file = task.get("out", "output/" + task["image"].replace(".", "_meme.")) make_meme(task["image"], out_file, task["text"]) print("已生成:", out_file)

这个脚本默认开启底部黑条模式,输出到当前目录下。重要的一点是,如果你自己准备了一堆底图,把tasks列表换成你自己的路径和文案就行。整个代码从头到尾没有用到任何高级特性,全部是入门级函数和循环,很适合一行一行读懂。

4. 常见问题排查与实操心得

4.1 Pillow安装报错或import失败

安装报错最常见的两个原因:一是pip版本太老,二是所用的Python环境不对。可以先升级pip试试:

python -m pip install --upgrade pip

如果import PIL时报错ModuleNotFoundError,基本可以确定你当前运行的Python和pip指向的Python不是同一个。这时候用python -m pip install pillow这种方式来安装,能保证装到当前解释器对应的环境里。这个坑在macOS里尤其多,因为系统自带Python和后来装的Python版本容易混。

4.2 中文字体还是显示成方块

如果确认字体路径存在,但仍然显示方块,检查一下路径里的.ttc格式。个别老版本Pillow对ttc集合字体的支持不够好,可以把微软雅黑的msyh.ttc替换成simhei.ttf试试。另外,用ImageFont.truetype加载字体时不要省略size参数,传一个具体数值进去,否则某些字体API调用会出现尺寸异常。

还有一个容易被忽视的点:如果你把文字画在了透明图层上,保存成JPG格式时会默认展开透明层,但PNG格式有时会保留带透明度的文字描边边缘,看起来像毛边。多用.convert("RGB")再保存,能避免很多颜色异常问题。

4.3 文字位置怎么调都不居中

居中计算最容易犯的错是直接用getsize这种旧API。Pillow的ImageDraw没有.getsize方法,早期教程里常用draw.textsize,但这个API在新版本里已经标记为过时。统一用textbbox来计算才是正确姿势,它的返回值是相对文字左上角的盒体坐标,别忘了坐标原点的偏移。

另外对多行文字计算总高度时,行距如果写死,不同字体渲染出的行高会有细微差异。更稳妥的办法是每行都实时取一次textbbox,用其中的y1-y0值来累加总高,而不是用一个固定常数乘以行数。上面完整代码里已经用了这个思路,改字体后不会出问题。

4.4 批量处理生成很慢

表情包本身尺寸不大,单张处理通常只需几十毫秒。如果感觉批量任务跑得慢,先检查源图是不是超大尺寸。有些手机照片是4000x3000像素级别,一次缩放和保存就耗掉不少时间。建议在读取后统一做一次缩略处理:

img.thumbnail((800, 800))

thumbnail旨在保持宽高比的前提下把图片缩到适合表情包使用的尺寸,500到800像素都比较合适,既能保证清晰度,又不会让文件体积大得离谱。

4.5 调试小技巧:多存中间结果

写图像处理代码时,经常出现“生成出来的图不是想要的”这种问题。别慌,也别一次性改一堆再运行。我的习惯是每写一个功能点,就立刻保存一张测试图:

img.save("debug_step1.png")

比如先存一张只画了黑条、没有文字的,确认黑条尺寸和位置没问题;再往黑条上写字,确认文字对齐。分段验证能把问题快速隔离出来,尤其适合新手排查自己的代码逻辑。这个习惯比写一百行注释都好用。

4.6 灵感与素材的收集思路

最后聊点轻松的。表情包这东西,技术和素材同样重要。底图素材可以用自己平时聊天的截图、影视剧经典桥段的截屏,需要提醒的是别拿别人的原创作品做商用。自己用的表情包无所谓,商用发布就有版权问题了。

文案方面可以学一个套路:热门表情包的文案只有两三种句式,一个是“当你____的时候”,一个是“我只是一个____罢了”,另一个是人称+状态+结果的短句。把这些句式套进脚本里,批量生成出来的表情包效果通常都不差。

5. 写在后面的一点经验

这个项目我在带朋友入门时跑过很多次,每次有人问“学Python能干嘛”,我就让他们先做这个表情包脚本。因为它真的能在一个晚上之内跑通,几乎不会中途放弃,而且做完之后马上能分享给朋友用,这种正向反馈对保持学习动力太重要了。

如果你已经跑通了基础的批量生成功能,后面可以自己往三个方向扩展:一是给程序加一个图形按钮界面,不用每次改代码和文件路径,点几下就能出图;二是去分析文字长度、字号和图片宽度的关系,让文字按视觉比例自动缩放;三是把你手头一批表情包底图结合每天的热点文案,做成一个每天自动更新一批“今日表情包”的小工具。代码拉下来跑一遍,再去改自己想改的细节,你会突然发现,自己不知不觉已经能看懂不少别人写的Python程序了。

最后再分享一个小习惯:所有代码文件里,图片路径和文字内容尽量放在文件顶部,或者用JSON单独管理,不要散落在函数中间。过两个星期再回来看这个项目时,你会非常感激当时那个能保持整洁的自己。

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

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

立即咨询