news 2026/10/18 8:16:52

Day20复盘:20天用Python+Pillow打造图片批量处理工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day20复盘:20天用Python+Pillow打造图片批量处理工具

看到“day20”这个标题,你可能觉得奇怪:不就一个天数吗?但如果你做过任何以“连续记录”为驱动的个人项目,就会懂第20天是个非常微妙的节点——新鲜感已经耗得差不多,离终点又还有一截,要么靠惯性滑过去,要么就此烂尾。这篇要聊的,就是我给自己定的一个20天小项目:用连续20天做一个图像批量处理的小工具,并在第20天完整复盘整个过程。内容包括为什么选20天、前10天怎么熬过迷茫期、后半程靠什么提速、收尾时踩过的三个典型坑,以及最后实际产出的数据。想尝试“用天数逼自己完成一件小事”的读者,尤其是第一次做长期个人项目的朋友,这篇文章应该能给你一套可以直接照搬的节奏和方法。

1. 为什么是“day20”:连续20天项目的起心动念

1.1 以天为单位做项目,治好了我的拖延症

事情的起因其实很俗:我电脑里攒了上千张图片,有的是截图,有的是相机导出来的原片,文件名乱得像一锅粥,想找一张图得翻半天。我一直想写个脚本批量改名、顺便压缩一下体积,但“有时间再做”这句话说了大半年,愣是没动过一行代码。

后来我意识到,问题不在于“没时间”,而在于任务太模糊。批量处理图片——听起来简单,但具体先做什么?用什么库?要不要做界面?这些都没想清楚,潜意识里就一直拖着。于是我想换个思路:不给自己规定“做完”,而是规定“做20天”。每天不要求有大进展,但必须打开编辑器、写点东西、留一条记录。第20天不管做成什么样,都要总结一次。这种做法不新鲜,很多人给自己定过“30天挑战”“100天打卡”,但我特意选20天,原因是这个长度刚刚好:三周左右,足够走完“立flag—踩坑—修正—收尾”的完整循环,又不会因为周期太长而在中途彻底放弃。

规则也很简单:每天至少投入30分钟;当天做了什么、卡在哪里、明天准备做什么,三行字写在日志里;不追求每天都有大版本更新,但第20天必须有一个能跑通的成品。听起来很宽松对吧?但真正执行起来,前三天我就差点破功。

1.2 从“day20”这个节点反推:目标拆解与里程碑

定完规则之后,我先做了另一件事:从第20天倒推,把这个模糊的“图片批量处理工具”拆成可以每天推进的里程碑。这个动作看起来平平无奇,但它是我觉得整个项目里最值的一步。

我的拆法是这样:第1到5天,先搞清楚需求边界和环境。说白了就是明确工具到底要解决什么问题——我的核心需求就两个:批量重命名(加前缀、按序号排列)和批量压缩(在画质可接受的前提下减小体积)。再确定技术路线,因为我是用Python顺手,图像处理就选了Pillow库,不碰OpenCV那些重型依赖。第6到10天把主路径跑通,先做一版只有一个入口和几个参数的脚本,宁可丑,但要能跑。第11到15天逐步加功能,比如递归遍历子目录、输出预览、跳过已处理文件。第16到19天集中测试和修bug,把真机上的上千张图分批跑,记录失败场景。第20天收尾复盘,整理文档、统计数据和写这篇东西。

这样拆完,每个阶段的任务都变得非常具体,而“day20”就不再只是一个天数,而是一个验收节点。我甚至给自己写了一句“验收定义”:第20天结束时,把任一文件夹丢进脚本,能自动递归找到所有图片、按规则改名、压缩输出,并且不会因为个别文件名异常而中途崩溃。

2. 前10天的真实节奏:从“不知道做什么”到最小原型跑通

2.1 头两天根本学不进去,后来是怎么爬出来的

说实话,前两天的记录拿出来看有点丢人。day01我只做了一件事:装环境、建文件夹、读了一小时Pillow的官方文档,没有写一行业务代码。更糟的是day02,我打开编辑器坐了一个小时,满脑子都是“这个工具到底要支持多少种情况”“要不要顺便做重命名格式校验”“图片旋转要不要考虑”,最后只写了个读取文件夹路径的骨架,然后默默关掉电脑。

那两天给我最大的教训是,小项目千万别在开头想架构,想多了唯一的结果就是什么都不做。我当时的解决办法很笨但很有效:把“做工具”暂时抛到脑后,改成“先写一个能跑的、只能处理单张图片的脚本”。不管丑不丑,不管功能全不全,只要我把一张JPG读进来、压缩、重新命名、写出去,就是一个最小闭环。于是我删掉了原计划里那些花里胡哨的念头,在day03写下了这段代码:

from PIL import Image import os src = "test.jpg" dst = "processed_test.jpg" img = Image.open(src) img.save(dst, quality=80, optimize=True) print("done:", dst)

就这么一小段,运行成功的瞬间,我的心态完全变了。因为“能不能做”这个问题已经不再是问题,剩下的全是“怎么做得更好”。所以如果你是第一次做这类项目,我强烈建议把“最小原型跑通”放在整个计划的最前面,哪怕原型蠢到只能处理一个文件,它给你带来的推进感也远比十页规划文档有用。

2.2 第3到第10天:选型与最小原型的实现

从day03开始,节奏明显就稳了。我先把脚本从单文件扩展成支持一个文件夹,再慢慢加上参数。技术选型在这个时候体现出它的价值:选Pillow而不是别的库,主要是因为它开箱即用,API简单,对JPEG、PNG、WebP这些常用格式都支持,而且用quality和optimize两个参数就能完成大部分压缩需求。我可以把精力全放在流程上,不用去碰像素级的图像编码逻辑。

到了day06,脚本已经长这样了:

from PIL import Image import os import sys def process_image(src_path, dst_path, prefix="", quality=85): img = Image.open(src_path) img = img.convert("RGB") filename = os.path.basename(src_path) name, ext = os.path.splitext(filename) new_name = f"{prefix}{name}{ext}" out_path = os.path.join(dst_path, new_name) img.save(out_path, quality=quality, optimize=True) print(f"processed: {src_path} -> {out_path}") if __name__ == "__main__": src_dir = sys.argv[1] dst_dir = sys.argv[2] prefix = sys.argv[3] if len(sys.argv) > 3 else "IMG_" os.makedirs(dst_dir, exist_ok=True) for item in os.listdir(src_dir): if item.lower().endswith((".jpg", ".jpeg", ".png")): process_image(os.path.join(src_dir, item), dst_dir, prefix)

这段代码现在看来当然很初级,但它在day06的时候已经解决了我80%的问题:能遍历单层目录、能改名、能压缩。第7到第10天我一直在做细节补充,比如对PNG图片的透明通道做特殊处理,压缩参数从写死改成可配置,还有把处理结果打印得更清楚。

说实话,前10天里最难的其实不是代码,而是“每天都要碰一下”的习惯养成。我给自己定的规矩是:哪怕当天很累,也要至少打开项目跑一次脚本,然后写三行日志。这个习惯本身比那几天写的代码珍贵得多,它能保证day11的时候你不会因为隔了两三天而完全接不上状态。

3. 后半程的提速方法:让第11到第19天不再原地打转

3.1 把“每天写代码”改成“每天完成一个微目标”

如果说前10天靠的是新鲜感和执行力,那后半段拼的就是节奏设计。day10结束后我回看自己的日志,发现一个规律:只要当天的任务是“想给脚本加个功能”,效率往往很一般,甚至会在若干个功能点之间来回横跳;但凡是当天的任务能被一句话讲清楚的,比如“让脚本能递归遍历子文件夹”“给压缩参数加上取值范围校验”,完成率几乎百分之百。

于是从day11开始,我调整了策略,不再用“今天写代码”这种空泛的说法当目标,而是每天晚上定好第二天的微目标,且保证这个目标能在30到60分钟内完成。后面几天的微目标长这样:

第11天:支持递归遍历子目录,遇到非图片文件自动跳过。第12天:输出处理前后的文件大小对比,让我直观看到压缩效果。第13天:加一个预览模式,先打印将发生的改名规则,不真正写入。第14天:把quality参数从命令行传进去,并限制在35到95之间,避免误传0导致报错。第15天:做一个“已处理文件列表”,重复运行同一个文件夹时跳过已完成的部分。第16天:针对文件名包含空格、中文、多个点的场景做兼容测试。第17天:处理PNG透明通道在压缩时变黑的问题。第18天:全量跑一次自己的图库,记录报错和异常样本。第19天:修复第18天发现的所有问题,跑第二遍完整测试。

这9天里真正写在代码里的东西,就一句话:把目标变具体。大脑是很吃“确定性”的,你说“今天增加递归遍历”,它就知道要做什么;你说“今天看点相关的资料”,它就不知道该往哪儿使劲。后者在长期项目里特别容易导致原地打转——你感觉自己忙了一晚上,实际什么都没产出。

3.2 验收清单和“日终回顾”的实操写法

另外一个在后半程帮了大忙的东西,是一张很简陋的验收清单。我把它直接写在项目目录下的一个文本文件里,每次改完代码就逐条勾:

  • 脚本是否能在命令行里用参数指定输入和输出路径?
  • 是否会自动创建不存在的输出目录?
  • 是否支持递归读取子目录?
  • 是否会在压缩前备份原图?
  • 是否能把“处理失败”的文件单独列出来而不是直接中断?
  • 重复运行同一目录,是否会重复处理?

这些问题的价值不在于答案本身,而在于它们逼着我把“感觉差不多了”变成“可验证的标准”。比如“支持递归读取子目录”看着简单,实际写的时候就得处理递归函数里的路径拼接,处理完还得用一个嵌套文件夹试一遍才知道有没有写对。

再说“日终回顾”。我自己的格式是每天固定三问:今天交付了什么?卡在哪儿?明天第一个动作是什么?前两问用来把当天的信息压缩成记忆点,第三问是关键——它让第二天的“开始成本”降到最低。比如我day17写着“明天第一个动作:跑一下带透明通道的PNG样本”,第二天我打开电脑根本不用想,直接找样本执行就行。如果没有这一句,第二天光“从哪儿开始”就能消耗十几分钟。

4. 第20天收尾踩过的三个典型坑

4.1 坑一:误以为 quality=95 就是“高清无损”

第18天全量测试时,我拿着一批高质量的相机原图做压缩实验,发现一个反直觉的现象:把quality设成95,输出文件居然比原图还大。我当时第一反应是脚本写错了,反复查了半天,确认save的参数没写反,optimize也没问题。后来又用同一张图分别用95、90、85、80的quality跑了一遍,才彻底明白原因。

quality在JPEG编码里的作用,是控制量化表的缩放系数,它和最终文件大小不是线性关系。很多相机原图本身的压缩率已经很高,把quality设为95甚至会推高输出体积。更关键的是,我已经在前面做了img.convert("RGB"),这张图如果本身没有额外的元数据负担,压缩收益就非常有限。真正影响体积的往往不是单一的quality值,而是原图的分辨率。比如一张4000x3000的照片,位深度和量化表才是体积大头,简单调quality根本压不动。

后来我给自己补了一组直观数据,就拿一张典型照片做测试,方便以后参考:

参数输出大小相对原图
原图2.41 MB100%
quality=952.52 MB104%
quality=851.86 MB77%
quality=751.41 MB58%
quality=85 + 尺寸缩到50%412 KB17%

结论很简单:想明显减小体积,要么接受一定画质损失,要么直接缩分辨率。只会调quality的小工具,面对“原图体积已经不小”的场景是失效的。我在day19给脚本加了一个可选的尺寸缩放比例参数,比如传入0.5就把图片宽高变成原来的50%,这是这次踩坑留下的最大变化。

4.2 坑二:文件名解析在批量场景下随机出错

第二个坑更隐蔽,发生在day16的兼容性测试阶段。我准备了一批带空格的、带中文的、带多个点的文件,比如“我的 截图 (1).final.jpg”,跑第一遍的时候部分文件处理成功,部分直接报错,而且报错的文件毫无规律。我第一反应是编码问题,因为用的是中文目录,于是又检查了路径编码,还是不对。

后来我给脚本加了一行异常打印,把出错的完整文件名打出来,才发现问题出在文件后缀解析上。最初的代码里我用了filename.split(".")[-1]去取后缀,这个写法在普通文件名上没问题,但遇到“我的 截图 (1).final.jpg”这种含多个点的名字,取到的后缀还是jpg,看起来正常,实际上问题出在拼接输出路径时我用的是name + ext,而name取的是split(".")[0]——也就是“我的 截图 (1)”,根本没把.final当名字的一部分。这就导致输出文件名和预期不一致,而且如果多个类似文件存在,还会生成重复的目标路径,触发覆盖冲突。

正确的做法是用os.path.splitext,它按最后一个点拆分:

name, ext = os.path.splitext("我的 截图 (1).final.jpg") # name = "我的 截图 (1).final" # ext = ".jpg"

这次排查给我留了个很深的印象:单文件测试能跑通,不代表批量场景能跑通。很多文件名问题在单张图上不会暴露,只有数据量大了之后才会随机出现。所以批量处理工具在开发阶段,一定要准备一组“脏数据”做测试,包括带空格、带中文、带多个点、后缀大小写混合的情况,不要只拿一份干净样本跑一遍就当完了。

4.3 坑三:进程中断后只能用零散样本重新调试

第18天的全量测试跑到一半,脚本身上的一个意外直接崩了——有一张损坏的图片让Pillow抛出异常,而当时脚本用的是单进程顺序处理,前面已经处理完的30多张图虽然写进了磁盘,但整个脚本直接退出,后续所有文件都没了着落。最烦的是,我找不到“哪些文件已经处理完”的记录,因为脚本根本没有写日志文件,我唯一的信息是运行窗口里刷屏的打印输出。

这个问题本质上是“小工具最容易被忽略的可靠性问题”:功能看着都正常,但一旦中断,就得从头再来。我在day19做了两个改动来解决它。第一,把所有处理结果追加写入一个processed.txt,每次成功处理完一个文件就记录一行。第二,下次启动时先读取processed.txt,把已经处理过的文件名放进一个集合,循环里遇到集合里的文件就跳过。核心代码大概长这样:

processed = set() if os.path.exists("processed.txt"): with open("processed.txt", "r", encoding="utf-8") as f: processed = set(line.strip() for line in f if line.strip()) for item in file_list: if item in processed: continue try: process_one_file(item) with open("processed.txt", "a", encoding="utf-8") as f: f.write(item + "\n") except Exception as e: print(f"failed: {item}, reason: {e}")

这段代码很朴素,但它把“跑全量”从高风险操作变成了可以随时中断、随时续跑的低风险操作。后来我把同样的思路用在了其他批处理脚本里,几乎成了我的默认模板。至于当时找不到已处理列表的教训,也让我养成了一个习惯:任何批量任务,第一步先考虑断点续跑,再考虑功能丰富度。

5. 20天结束时拿到的数据,和如果重来会调整的三件事

5.1 每日输出的复盘口径,以及计划与实际的差距

第20天做收尾统计时,我重新翻了一遍每天的日志,把数据整理成了下面这张表:

项目数据
实际写代码天数19天(其中1天只写了日志没碰代码)
累计有效编码时间约18小时
核心功能数量6项(递归处理、改名、压缩、预览、跳过已处理、日志断点)
全部测试图片数约1200张
处理失败并修复的问题4类(损坏图片、名称解析、透明通道变黑、体积不减反增)
第20天成品状态命令行工具可用,但无GUI、无打包程序、部分文档缺失

对比一下我最初在day01拟的“完美收尾”计划:计划里的第20天应该有一个像模像样的图形界面,所有参数都能可视化调整,还附带一份完善的配置文件。实际做出来的,只是一个个颇“寒酸”的命令行脚本。但如果你问我是否觉得这个项目失败了,我会说没有。因为这20天的价值不在于那个脚本本身有多完美,而在于我彻底跑通了一件从0到1的事。功能不够,下次迭代就行;文档缺失,后续补上就行。而“能不能把一个模糊想法变成一个能跑的东西”这种能力,恰好只能靠这种小项目一点点练出来。

我也统计了一下自己使用工具处理那1200张图片的实际效果:总占用空间从最初的1.8GB左右降到了约900MB,同时保留了相对可接受的画质。对于一个20天业余时间做出来的小工具,这个结果我认为完全达到了当初设定“批量改名和压缩”的目标。

5.2 下一步延伸:从脚本到工具,从“day20”到长期项目

第20天的日志里,我给自己列了三个后续方向。第一个是把脚本包装成真正的命令行工具,用参数解析库规范参数格式,让其他环境的人也能直接安装使用。第二个是给它加一个简单的图形界面,或者做成右键菜单集成到文件管理器里,这样不懂命令行的朋友也能用。第三个是把“已处理列表”升级成更完整的任务日志,记录每次处理的参数、时间、失败原因,方便日后追溯。

如果让我再做一轮类似的“day20”项目,有三件事我会在一开始就调整。第一,从第1天就写README,哪怕只有两行字,也要记录“这个项目要干什么、主要文件在哪”;第20天再补文档的痛苦远大于每天顺手写一句。第二,每三天做一次完整测试,而不是等到第16天才集中测,因为问题越早暴露,修复成本越低。第三,在计划里预留两天buffer,不把每个功能排得太满,给意外留出空间。

最后想分享一个我直到第15天才悟到的小技巧:把“计划中的当天任务”和“实际完成的当天内容”写在同一个文档里,不要分开记。一开始觉得分开没问题,后来发现“计划”和“实际”放在一起才能看到差距。翻日志的时候,计划写着“今天实现GUI”,实际写着“纠结了一天转码问题,只改了两行代码”,这种对比看着有点打脸,但恰恰是这些打脸记录让我逐渐摸清了自己的真实执行速度。你以为的自己和实际的自己差多远,只有这种连续记录能回答。如果你也想给自己定一个“day20”计划,不管做什么方向,这点是我最想让你提前知道的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 8:16:46

10/0.4kV配电工程电气设计要点详解:从负荷计算到运行复盘

做了不少10/0.4kV配电工程电气设计之后,我最大的感受是:这类项目看着入门门槛不高,电路似乎就是“进线—变压器—出线”三段,但真正能把图纸一次审过、送电后不出毛病的,拼的全是细节。变压器容量选大了,几…

作者头像 李华
网站建设 2026/10/18 8:15:42

基于YOLOv8/YOLO11的混凝土裂缝检测系统实战与OpenCV部署

混凝土表面的裂缝和坑洞,看着不起眼,但对结构安全来说,每一道裂缝都可能是内部损伤的警报信号。这个项目涉及用YOLOv8和YOLO11分别训练混凝土裂缝检测数据集,并构建一套基于深度学习的混凝土缺陷检测系统。我把自己从数据打标、模…

作者头像 李华
网站建设 2026/10/18 8:15:42

直接赋值、浅拷贝与深拷贝:内存共享、隔离与工程选型

写这篇文章的起因有点丢人——某次项目评审会上,我被当场问了一句"你这段代码为什么不用深拷贝",我愣了一下,然后发现自己在用一堆零散经验解释一个本来应该很基础的概念。回头把"直接赋值、浅拷贝、深拷贝"这一组操作彻…

作者头像 李华
网站建设 2026/10/18 8:14:56

GitHub 热门项目剖析:当“代码知识图谱”撞上 AI 编程助手,我们真的需要它吗?——用 TaoToken 统一 Key 实测 Cursor Base URL 与 Cline MCP 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 8:14:40

常驻Agent四层架构全解析:从会话管理到模型调度

2. 四层构造的全景拆解:从会话外壳到底层内核聊到常驻Agent的内部构造,大多数人第一反应是“不就是一个大模型套个Prompt吗”。但实际拆解下来你会发现,一个能稳定常驻运行、不外挂业务逻辑、能自己驱动工具链的Agent,它的内部结构…

作者头像 李华
网站建设 2026/10/18 8:14:15

手机与AI的双向改造:从端侧大模型到智能体生态

地铁上,旁边一位乘客把手机对着摊开的会议记录拍了张照,两秒后屏幕上弹出一版整理好的文字稿,标题、要点、待办事项分得清清楚楚。他把这段文字转发出去,整套动作行云流水,头都没抬。这一幕放在三年前不太敢想&#xf…

作者头像 李华