资讯动态

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

发布时间:2026/10/18 8:16:59 来源:尧图企业网站定制
看到“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, quality80, optimizeTrue) 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, quality85): 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, qualityquality, optimizeTrue) print(fprocessed: {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_okTrue) 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 坑一误以为 quality95 就是“高清无损”第18天全量测试时我拿着一批高质量的相机原图做压缩实验发现一个反直觉的现象把quality设成95输出文件居然比原图还大。我当时第一反应是脚本写错了反复查了半天确认save的参数没写反optimize也没问题。后来又用同一张图分别用95、90、85、80的quality跑了一遍才彻底明白原因。quality在JPEG编码里的作用是控制量化表的缩放系数它和最终文件大小不是线性关系。很多相机原图本身的压缩率已经很高把quality设为95甚至会推高输出体积。更关键的是我已经在前面做了img.convert(RGB)这张图如果本身没有额外的元数据负担压缩收益就非常有限。真正影响体积的往往不是单一的quality值而是原图的分辨率。比如一张4000x3000的照片位深度和量化表才是体积大头简单调quality根本压不动。后来我给自己补了一组直观数据就拿一张典型照片做测试方便以后参考参数输出大小相对原图原图2.41 MB100%quality952.52 MB104%quality851.86 MB77%quality751.41 MB58%quality85 尺寸缩到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, encodingutf-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, encodingutf-8) as f: f.write(item \n) except Exception as e: print(ffailed: {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”计划不管做什么方向这点是我最想让你提前知道的事。

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

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

免费获取报价 →
↑