如何从 0 到 1 写一个 Python 重命名脚本?
大家好呀~
今天就来给大家讲讲,我是怎么实现“批量获取多个目录下的多张图片尺寸信息并重命名到每张图片的文件名上”这个需求的 Python 自动化脚本的。
为了帮助大家理解我整个思考、搜索、编写代码和测试的过程,我会讲得非常细,希望大家不要嫌我啰嗦,哈哈。
需求背景
先交代一下这个需求的背景。由于我平时工作中经常需要用到一些高清图片作为报告的背景图,因此我经常会到国外的无版权图片站下载一些图片,这些网站一般都会有不同的尺寸可供下载,为了适用不同场合的需求,我一般小、中、大、原始四种尺寸都会下载下来。
但有个问题是,这个网站下载下来的不同尺寸的图片文件名是一样的,而且是一串很长的数字字母组合,下载到本地之后就会变成:
一眼看过去好像四张完全一样的图片。
当然,我也可以把文件查看方式改成“详细信息”:
再在上面的详细信息栏中点选像素信息,也可以看到图片大小:
但是这种方法并不能一劳永逸,当我在PPT里插入图片需要点选时,要看到像素大小信息,还是要进行一遍相同的操作。
除了这个问题,文件名的修改也非常有必要。
虽然我每次下载的时候都会把不同内容主题的图片分别放在不同的文件夹下,以方便查找,但是同一个文件夹下有多张名称一样的图片毕竟不好区分。
一级目录“配图”下分不同场景文件夹 二级目录“城市”下分不同主题文件夹 三级目录“海边城市背景图”对应该图片多个尺寸文件再加上我最近装了一个查找本地文件的神器——everything,如果能通过关键词直接搜索到相应内容的图片,那找起来就非常高效了。
于是我决定对每张图片进行重命名,新的文件名为:每个图所在的目录名(代表图片主题内容)+作者信息(这个可以不要)+该图片像素大小。
需求分析
需求明确了以后,就要分析和拆解需求了。
每个图所在的目录名已经有了,只要读取一下赋给一个变量,在重命名的时候新文件名以这个变量开头即可。
作者信息就是原文件名开头的一个或两个单词,重命名时保留就行,我打算只保留第一个小破折号之前的名字。当然这个可以不要,我留下来主要是日后万一需要标作者信息,至少有个出处。
最难的是第三点——获取该图片的像素大小并添加到新文件名上。那么如何获取一个图片的像素大小信息呢?
于是我先从这个难点入手,开始百度。
获取图片大小
由于我的目标不是为了成为 Python 大神,只是为了解决当下的问题,所以求助于百度是效率最高的办法。毕竟对于我来说,重复造轮子既低效,又没有意义。如果有别人已经写好的,在能看懂的前提下直接拿来用,岂不快哉。
用关键词“python读取图片大小”进行搜索,我找到了这样一篇分享:
python获取图片大小的两种方法对比了一下,下面的方法代码更简洁,于是直接装库搬过来用。
新建一个脚本文件 getimgsize.py
:
import cv2
file_path = 'E:/study/CS/Python/配图/城市/海边城市背景图/pedro-lastra-Nyvq2juw4_o-unsplash.jpg'
img = cv2.imread(file_path) # 读取图片信息
#sp = img.shape[0:2] # 截取长宽啊
sp = img.shape # [高|宽|像素值由三种原色构成]
print(sp)
把原代码中的路径换成自己的,一运行,结果报错了。
Traceback (most recent call last):
File "getimgsize.py", line 7, in <module>
sp = img.shape # [高|宽|像素值由三种原色构成]
AttributeError: 'NoneType' object has no attribute 'shape'
莫慌,让我们来看看错误信息。
第7行报出了属性错误,说 ‘NoneType’ 对象没有 shape 这个属性。
啥意思?NoneType 是 Python 中 None 的类型,也就是说我们读取图片信息的 img = cv2.imread(file_path)
读了个寂寞(None)。
那么没读取到可能有哪些原因呢?我的路径中含有中文,会不会是无法识别中文路径?那我们就来验证一下看是不是中文路径的问题。
我们把一张图片复制出来,放到E盘下面,修改代码中的 file_path 为 ‘E:/pedro-lastra-Nyvq2juw4_o-unsplash.jpg’,然后再次运行脚本,发现成功运行了,输出结果如下:
> python getimgsize.py
(394, 640, 3)
原代码中已经给我们标注出来了,这三个数字分别是图片的高、宽、像素值。由于我只需要用到宽和高两个值,且需要分别获取到,所以我们对代码稍作修改,先截取这个元组的前两位,再通过index分别取出长和宽。
注意我们通常所说的“长”对应着前面“高、宽、像素值”中的“宽”,而我们通常所说的“宽”则对应“高、宽、像素值”中的“高”,所以“长”对应第1位,“宽”对应第0位。
import cv2
file_path = 'E:/study/CS/Python/配图/城市/海边城市背景图/pedro-lastra-Nyvq2juw4_o-unsplash.jpg'
img = cv2.imread(file_path) # 读取图片信息
sp = img.shape[0:2] # 截取长宽
print(sp[1]) # 打印长
print(sp[0]) # 打印宽
再运行一下,我们就得到了长和宽的值:
> python getimgsize.py
640
394
由于我们在重命名的时候是需要写成长x宽的形式,所以我们把最后两行代码换成:
print(str(sp[1])+'x'+str(sp[0]))
输出结果就变为:
> python getimgsize.py
640x394
获取和显示的问题搞定了,让我们回过头来解决一下中文路径识别的问题,毕竟我所有的目录都是中文命名。
解决中文路径识别问题
还是找度娘,最后在知乎看到一个这样的回答:
这么多赞,下面评论一众感谢大佬、大佬np,肯定靠谱没跑了。搬过来用!
函数什么的,我最喜欢了。这个函数相当于是把这个库原来的 imread
方法重新定义了一下,增加了对中文路径的支持。
修改后的代码如下,记得把 cv2.imread
改成 cv_imread
:
import cv2
def cv_imread(file_path):
cv_img = cv2.imdecode(np.fromfile(file_path,dtype=np.uint8),-1)
return cv_img
file_path = 'E:/study/CS/Python/配图/城市/海边城市背景图/pedro-lastra-Nyvq2juw4_o-unsplash.jpg'
img = cv_imread(file_path) # 读取图片信息
sp = img.shape[0:2] # 截取长宽
print(str(sp[1])+'x'+str(sp[0]))
跑一下试试:
> python getimgsize.py
Traceback (most recent call last):
File "getimgsize.py", line 8, in <module>
img = cv_imread(file_path) # 读取图片信息
File "getimgsize.py", line 4, in cv_imread
cv_img = cv2.imdecode(np.fromfile(file_path,dtype=np.uint8),-1)
NameError: name 'np' is not defined
又报错了。。。莫慌,让我们来看看错误信息:
NameError: name 'np' is not defined
这才注意到这个空降函数中调用了 np
这个库。还好我学过数据分析,这 np
不就是大名鼎鼎的 numpy
嘛,引用一下不就完了。
import numpy as np
这次终于成功了:
> python getimgsize.py
640x394
获取图片大小的问题解决了,我们就要回到我们的整体需求了,毕竟我要处理的不是一张图片,而是一个文件夹下面层层包裹着的多张图片。
读取目录和文件名
由于我的图片文件夹有三级目录,脚本需要进到每一个二级目录里,再从每个二级目录进到三级目录中,在三级目录下获取到每张图片的文件名,以及其尺寸信息,再用之前获取到的三级目录名+尺寸信息为每张图片进行重命名,所以我们需要先解决一个问题——如何读取文件名。
正当我准备搜索“如何用python读取一个目录下的文件名”时,我突然如梦初醒。
我到底在干什么?!明明是要做鱼香肉丝,为什么还要搜索“如何调制鱼香肉丝拌料”,直接搜“鱼香肉丝教程”不就行了!哈哈哈哈,真是聪明反被聪明误!
于是我把关键词改成“如何用python批量修改文件名”开始搜索。接着便看到这样一篇分享:
这位老兄的需求似乎跟我有异曲同工之处,他是要把“电影天堂”四个字去掉,我是要把第一个横杠后面的内容删掉换成别的,这个别的我已经搞定了,所以学习一下他是怎么获取文件名、怎么修改的就行了。
先研究一下这几行核心代码:
百度了一下,这个 os
库是 Python 的一个标准库,里面包含了很多处理目录和文件的函数。
这篇文章的开头有讲到:
chdir
这个方法是改变当前工作路径到指定路径,就是说我们要在这个目录下进行一系列操作。那在我的需求中,这里就应该是
import os
os.chdir('E:/study/CS/Python/配图/')
而下面这个 for 循环中的 listdir()
就是用于返回指定文件夹所包含的文件或文件夹的名称列表,这不就是我们一开始想找到“鱼香肉丝拌料”嘛~
最后一个核心方法就是 rename
了,这个方法很简单,括号中第一个参数是原文件名,第二个参数是新文件名,最后调用一下就可以执行重命名操作。
为了避免混淆,我们新建一个脚本文件 rename.py
,先读取一下一级目录下面的文件夹试试:
import os
os.chdir('E:/study/CS/Python/配图/')
for dirs in os.listdir():
print(dirs)
输出成功:
> python rename.py
光效
几何
城市
星空
森林
水墨
海洋
清新
渐变
自然
接下来我们读取二级目录下的文件夹,写个双重 for 循环就行。但是这里有个问题,我们要读取一个目录下的文件夹,需要先把工作路径切换到这个目录下,所以我们在进行第二层 for 循环之前,需要先切换工作路径:
import os
os.chdir('E:/study/CS/Python/配图')
for dirs in os.listdir():
os.chdir('E:/study/CS/Python/配图'+'/'+dirs)
for subdirs in os.listdir():
print(subdirs)
为了保证代码简洁,我们把最开始的路径赋给一个变量 op_path
:
import os
op_path = 'E:/study/CS/Python/配图'
os.chdir(op_path)
for dirs in os.listdir():
os.chdir(op_path+'/'+dirs)
for subdirs in os.listdir():
print(subdirs)
输出结果:
> python rename.py
光影交错深蓝背景图
光影交错紫色背景图
动感线条背景图
淡紫光圈背景图
深紫光点背景图
烟花散落背景图
结果太多,就保留几条意思一下。
现在我们已经成功获取到了二级目录下的文件夹名称。最后,让我们攻入核心地带——三级目录下的图片文件,继续给 for 循环套娃:
import os
op_path = 'E:/study/CS/Python/配图'
os.chdir(op_path)
for dirs in os.listdir():
os.chdir(op_path+'/'+dirs)
for subdirs in os.listdir():
os.chdir(op_path+'/'+dirs+'/'+subdirs)
for imgs in os.listdir():
print(imgs)
输出结果:
> python rename.py
jr-korpa-stwHyPWNtbI-unsplash (1).jpg
jr-korpa-stwHyPWNtbI-unsplash (2).jpg
jr-korpa-stwHyPWNtbI-unsplash (3).jpg
jr-korpa-stwHyPWNtbI-unsplash.jpg
jr-korpa-9XngoIpxcEo-unsplash (1).jpg
jr-korpa-9XngoIpxcEo-unsplash (2).jpg
jr-korpa-9XngoIpxcEo-unsplash (3).jpg
jr-korpa-9XngoIpxcEo-unsplash.jpg
zak-7wBFsHWQDlk-unsplash (1).jpg
zak-7wBFsHWQDlk-unsplash (2).jpg
zak-7wBFsHWQDlk-unsplash (3).jpg
zak-7wBFsHWQDlk-unsplash.jpg
至此我们总算完成了获取目录和文件名这一重要任务。
搞定重命名
接下来,我们来看重命名代码。回顾一下参考代码:
人家 find “电”,我们 find “-”,人家用 index('堂')
,我们用 `index('-'),这样就可以定位到 “-” 在文件名中的位置了。虽然图片文件名中不只有一个 “-”,但是我们只需要找到第一个即可,后面的不用管。
接着看,人家 newname 取 names[x+1:]
是删掉“堂”之前的所有内容,而我们需要删掉 “-” 后面的所有内容,那就是 names[:x+1]
所以,先不考虑文件大小信息,我们的重命名代码就应该是:
if imgs.find('-') >= 0:
x = imgs.index('-')
# 新文件名为“-”之前的内容加上文件大小信息,最后加上后缀名
newname = subdir+'-'+imgs[0:x+1]+sizeinfo+'.jpg'
os.rename(imgs, newname)
这里的 sizeinfo 是文件大小信息,我先占个位置,马上就来搞定它。
双剑合璧
终于到了激动人心的最后一步,我们只需要把之前写的 getimgsize.py
脚本融合到 rename.py
脚本中即可。
回到最开始的 getimgsize.py
脚本:
import cv2
import numpy as np
def cv_imread(file_path):
cv_img = cv2.imdecode(np.fromfile(file_path,dtype=np.uint8),-1)
return cv_img
file_path = 'E:/study/CS/Python/配图/城市/海边城市背景图/pedro-lastra-Nyvq2juw4_o-unsplash.jpg'
img = cv_imread(file_path) # 读取图片信息
sp = img.shape[0:2] # 截取长宽
print(str(sp[1])+'x'+str(sp[0]))
现在我们已经能够获取到每张图片的路径和文件名了,也就是说上面这个脚本中的 file_path
变量在我们之前 for 循环的最里层就能得到它的值了,那我们就把这个获取图片大小的代码放到最里层的 for 循环中。由于我们要先获取文件大小信息,才能进行重命名,所以这段放在重命名代码之前:
for imgs in os.listdir():
file_path = op_path+'/'+dirs+'/'+subdir+'/'+imgs
img = cv_imread(file_path) # 读取图片信息
sp = img.shape[0:2] # 截取长宽
# 拼接图片大小信息
sizeinfo = str(sp[1])+'x'+str(sp[0])
这里我们就用 sizeinfo 这个变量来接收图片大小信息。
最后完整的代码如下:
import os
import cv2
import numpy as np
# 定义cv_imread函数,解决imread无法识别中文路径的问题
def cv_imread(file_path):
cv_img = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), -1)
return cv_img
op_path = 'E:/study/CS/Python/配图' # 设置指定路径
os.chdir(op_path) # 改变当前工作目录到指定路径
# 循环读取目录及文件
for dirs in os.listdir():
os.chdir(op_path+'/'+dirs)
for subdir in os.listdir():
os.chdir(op_path+'/'+dirs+'/'+subdir)
for imgs in os.listdir():
file_path = op_path+'/'+dirs+'/'+subdir+'/'+imgs
img = cv_imread(file_path) # 读取图片信息
sp = img.shape[0:2] # 截取长宽
# 拼接图片大小信息
sizeinfo = str(sp[1])+'x'+str(sp[0])
# 通过定位文件名中的“-”来确定要保留的内容,要求是保留“-”之前的内容
if imgs.find('-') >= 0:
x = imgs.index('-')
# 新文件名为“-”之前的内容加上文件大小信息,最后加上后缀名
newname = subdir+'-'+imgs[0:x+1]+sizeinfo+'.jpg'
os.rename(imgs, newname)
print('所有文件均已完成重命名')
为了显示进度,我在末尾加上了一句提示信息。
好了,让我们来测试一下。
见证奇迹
为了以防万一,我们可以先给整个文件夹备份一个副本,然后运行。
十几秒后,我收到了如下信息:
python rename.py
Traceback (most recent call last):
File "rename.py", line 27, in <module>
os.rename(imgs, newname)
FileExistsError: [WinError 183] 当文件已存在时,无法创建该文件。: 'yuriy-kovalev-nN1HSDtKdlw-unsplash (3).jpg' -> 'yuriy-3097x4645'
文件名已存在?难道我下载的时候点重了?根据文件名搜索了一下,果不其然,这张图片统一尺寸我下了两次。把重复图片删掉之后,我又执行了一遍代码。
终于:
> python rename.py
所有文件均已完成重命名
回到文件夹下检查一下,所有图片都已经成功完成重命名:
总结复盘
这次写脚本全程花了3个多小时时间,虽然代码都是拼凑来的,但是最终能把问题成功解决,还是很令人激动的。
当然这个脚本还不完美,还存在很大的优化空间,比如:
- 如何解决下载图片尺寸重复,导致脚本执行过程中因为存在相同文件名而报错中断的问题
- 假如文件夹下存在其他格式的文件,如何绕过的问题
- 如何在执行过程中显示一个处理进度条
等等等等。
不过无伤大雅,先解决当前的问题是最重要的。之后如果遇到新的问题,再去改进和完善这个脚本。
我记得之前学《Learn Python 3 The Hard Way》的时候,老肖说过,写一个程序之前,要先写一个大纲,明确一下大致的步骤,包括书中写的几个文字游戏,涉及到类和函数,也都是按照步骤先把框架列出来。
但是我这次写脚本并没有严格按照这个方法来。我觉得这种思考方式适合有一定经验的程序员,不适合我们这种小白。
因为当我想要解决一个问题的时候,我并不能清晰地写出程序解决这个问题可能需要的步骤。这个重命名的脚本尚且容易理解,跟我们手动操作的过程差不多。但是像爬虫、像OCR这种,你让我凭空想步骤,我脑子里是不太有概念的。
所以我觉得对于小白来说,先拿着你的核心问题去百度,如果能找到几个参考性很强的文章和代码,那么直接研究代码即可,看明白之后,结合自己的需求改一改。或者半懂不懂也行,试着改改运行一下,也就明白了。
这个方法比我们凭空去想步骤、凭空去写代码要高效得多。而且随着我们“借鉴”的代码越来越多,我们就会逐渐形成一种关于用程序解决问题的 sense,等到我们对工具的使用驾轻就熟之后,我们再直接上手写就行。
我一直相信,大多数学习过程都始于模仿。就像画画一样,不经过长时间的临摹,直接给你一张白纸,是画不出什么像样的东西的。
所以,要想用好 Python 这个工具,就要多发现问题、思考问题,然后搜索+复制粘贴+修改,最后解决问题,才能在不断的实践中历练出真本事。