一、任务
董付国老师的教材《Python程序设计基础与应用》当中有一道课后习题是这样的:
编写程序,检查D:\文件夹及其子文件夹中是否存在一个名为temp.txt的文件。
为了在讲完这部分内容之后给小盆友讲讲这个例子,就想先自己做一下,没想到这个程序要求不复杂,写起代码来感觉坑还是不少的。这里把写程序的过程记录一下,供有可能踩坑的同志们参考。
一共写了六个程序,前五个程序是一个系列,程序代码渐进由浅入深慢慢改进,用的是队列这种数据结构。第六个程序用的是递归的思想,写了一个递归函数。
二、环境
Win7中文旗舰版64位 + Python 3.64 64位
三、采用队列法写的五个程序
考虑到D:\可能包含的文件和文件夹太多,我们一开始可以用一个具体的文件夹做实验,如果程序没问题,再考虑推广到D:\这个文件夹。
【参考代码1】
import os
def mysearch(d, fn):
dlist = [d] # 将查询目录d加入队列
while dlist: # 当队列不空,从队列头部取一个目录,查询该目录
tmpd = dlist.pop(0)
for f in os.listdir(tmpd): # 查询tmpd下每个文件或目录f
if f == fn:
return True
ffull = os.path.join(tmpd, f) # 构造f全路径名ffull
if os.path.isdir(ffull): # 若ffull是目录,则加入队列
dlist.append(ffull)
return False
if __name__ == "__main__":
print(mysearch(r'D:\test', 'temp.txt'))
【说明】
这段代码没考虑到这么一个特殊情况:如果在d:\test文件夹下有一个名叫temp.txt的文件夹,我们会发现,它居然能返回True。
这是因为,我们只判断了f == fn是否为True,却没有判定f是不是文件。所以,我们要改一下代码。
【参考代码2】
import os
def mysearch(d, fn):
dlist = [d] # 将查询目录d加入队列
while dlist: # 当队列不空,从队列头部取一个目录,查询该目录
tmpd = dlist.pop(0)
for f in os.listdir(tmpd): # 查询tmpd下每个文件或目录f
ffull = os.path.join(tmpd, f) # 构造f全路径名ffull
if os.path.isfile(ffull):
if f == fn:
return True
else: # 若ffull是目录,则加入队列
dlist.append(ffull)
return False
if __name__ == "__main__":
print(mysearch(r'D:\test', 'temp.txt'))
【说明】
上述代码解决了文件夹的名字是temp.txt的问题。但运行时,偶尔也会发生文件存在但返回False的情况。原因是如果一个文件的名字是“temp.TXT”,在Windows系统看来,“temp.TXT”跟“temp.txt”是同一个文件,如果程序返回False,肯定是功能不完善。我们继续修改代码。
【参考代码3】
import os
def mysearch(d, fn):
dlist = [d] # 将查询目录d加入队列
fn = fn.lower() # 将fn变成小写
while dlist: # 当队列不空,从队列头部取一个目录,查询该目录
tmpd = dlist.pop(0)
for f in os.listdir(tmpd): # 查询tmpd下每个文件或目录f
ffull = os.path.join(tmpd, f) # 构造f全路径名ffull
if os.path.isfile(ffull):
if f.lower() == fn: # f变小写之后是否等于fn
return True
else: # 若ffull是目录,则加入队列
dlist.append(ffull)
return False
if __name__ == "__main__":
print(mysearch(r'D:\test', 'temp.txt'))
【说明】
上述代码解决了因为文件名大小写导致找不到文件的问题。现在看起来很完美,让我们把实验对象扩展到D:\,到这个包含更多文件和文件夹的目录下去查找文件。
我们发现,程序居然出错了,原因是目录d:\System Volume Information禁止访问,但上述代码会把它加入到队列中,然后取出来遍历它里面的文件时,就报错。
PermissionError: [WinError 5] 拒绝访问。: 'D:\\\\System Volume Information'
我们要在程序中禁止加入这样的目录。类似的还有回收站文件夹,我们都要禁止这些文件夹加入队列。
所以,有必要继续修改文件。
【参考代码4】
import os
def mysearch(d, fn):
# 禁止遍历的特定文件夹列表,比如:系统回收站什么的
# 有的时候遍历这些目录会被系统拒绝访问
uselesspathlist = ["$RECYCLE.BIN", "Recycled",
"System Volume Information"]
dlist = [d] # 将查询目录d加入队列
fn = fn.lower() # 将fn变成小写
while dlist: # 当队列不空,从队列头部取一个目录,查询该目录
tmpd = dlist.pop(0)
for f in os.listdir(tmpd): # 查询tmpd下每个文件或目录f
ffull = os.path.join(tmpd, f) # 构造f全路径名ffull
if os.path.isfile(ffull):
if f.lower() == fn:
print(ffull)
return True
else: # 若ffull是目录,则加入队列
# 硬盘根目录下的三个禁止遍历文件夹不能加入
if not (f in uselesspathlist and \
tmpd.rstrip("\\")[-1]==":"):
dlist.append(ffull)
return False
if __name__ == "__main__":
print(mysearch('D:\\', 'temp.txt'))
【说明】
上面的代码避开了Windows下的禁止访问文件夹出错问题,应该是很完美了吧?但一运行,很快又发现一个BUG:如果最初传递的参数不是一个存在的目录,则程序会报错。
我们要在函数mysearch()开头加上判定文件夹是否存在的代码。
【特别注意】
'D:\\'不要写成r'D:\',这会报告语法错误。这跟Python处理字符串的机制有关,Python认为反斜杠后面的字符是字符串的一部分,当字符串的内容以偶数个反斜杠结尾时,没问题,这些反斜杠两两组合,构成了若干个普通字符反斜杠自身。如果是字符串的内容以偶数个反斜杠结尾,则反斜杠两两组合后,还剩下一个位于最后面的反斜杠,Python会认为这最后的反斜杠后面的字符串界定符是字符串内容的一部分,从而认为缺少字符串界定符,因此报错。哪怕字符串前面加了r,Python处理字符串的机制也一样,所以r'D:\'构成语法错误。
因此,碰到像D:\这样表示硬盘根目录的字符串,直接在字符串里面写两个反斜杠算完,所以'D:\\'是允许的。如果非要在前面加r变成r'D:\\',那就变成了四个字符组成的字符串:D:\\,这个字符串作为目录虽然不会找不到,但还是感觉怪怪的。
【参考代码5】
import os
def mysearch(d, fn):
if not os.path.isdir(d):
return False
# 禁止遍历的特定文件夹列表,比如:系统回收站什么的
# 有的时候遍历这些目录会被系统拒绝访问
uselesspathlist = ["$RECYCLE.BIN", "Recycled",
"System Volume Information"]
dlist = [d] # 将查询目录d加入队列
fn = fn.lower() # 将fn变成小写
while dlist: # 当队列不空,从队列头部取一个目录,查询该目录
tmpd = dlist.pop(0)
for f in os.listdir(tmpd): # 查询tmpd下每个文件或目录f
ffull = os.path.join(tmpd, f) # 构造f全路径名ffull
if os.path.isfile(ffull):
if f.lower() == fn:
print(ffull)
return True
else: # 若ffull是目录,则加入队列
# 硬盘根目录下的三个禁止遍历文件夹不能加入
if not (f in uselesspathlist and \
tmpd.rstrip("\\")[-1]==":"):
dlist.append(ffull)
return False
if __name__ == "__main__":
print(mysearch('D:\\111', 'readdatefromexcel.py'))
【说明】
至此,经过5次修改,逐步解决了遇到的各种问题,代码总算比较完善了。但这也不能保证以后查找文件时不出差错。
代码中有BUG太正常了,据说没有BUG的代码是不存在的。所以,代码没有BUG可能就是最大的BUG。通常情况下,我们之所以认为我们编写的程序没有BUG,可能是因为在写代码的过程中已经对可能发生的情况考虑的足够周全但不是完美无遗漏,一旦出现一个意料之外的情况,那么整个程序代码可能就会因此崩溃。
所以,大家写代码的时候,不要害怕BUG,因为这是正常的。
四、采用递归法写的一个程序
前面几个代码用的是队列这种数据结构,按照先进先出的原则顺次处理各个文件夹,下面用递归的思想再写一个递归程序。
【参考代码6】
import os
uselesspathlist = ["$RECYCLE.BIN", "Recycled",
"System Volume Information"]
def mysearch_recursive(d, fn):
if not os.path.isdir(d):
return False
for f in os.listdir(d): # 查询tmpd下每个文件或目录f
ffull = os.path.join(d, f) # 构造f全路径名ffull
if os.path.isfile(ffull):
if f.lower() == fn.lower():
print(ffull)
return True
else:
if not (f in uselesspathlist and \
d.rstrip("\\")[-1]==":"):
r = mysearch_recursive(ffull, fn)
if r:
return True
return False
if __name__ == "__main__":
print(mysearch_recursive('D:\\ftp', 'readdatefromexcel.py'))
五、引申
实现本文功能的方法还有很多,比如os.walk,glob.glob,这里不再一一举例,感兴趣的读者朋友可以试试。
如果是查找某种类型的文件,比如查找所有的PDF文件*.pdf,程序该如何改动呢?
又如果查询某个目录及其子目录下的多种指定类型的文件,像WORD文件就有两种,*.doc和*.docx,这又该如何修改程序代码?
好了,感兴趣的小朋友可以动手干活喽。




