功能要求
假设一个测试目录test,有若干子目录A/B/C。 每个子目录(例如A)中都拥有一个requires文件,requires里每行是子目录名字(例如B),代表此目录的依赖项。编写一个脚本check.py,测试指定子目录中的依赖项是否合理,即是否存在缺失或者循环的依赖关系等(例如A->B->A)。若不合理则需要进行详细分析并打印原因。
执行方式:python check.py -c (folder_name)
test目录内容如下图所示:
子目录内容及requests文件内容(以G目录举例),如下图所示:
问题分析
- 首先,涉及到遍历目录,查找依赖,依赖关系是一个比较复杂的网,具体来看,A目录依赖于B目录,这是一个单向关系,所以关系具有方向性,结合有向图考虑。
- 其次,依赖是否存缺失或者循环,是否缺失容易判断,但是在遍历过程中,先要排除环的存在,避免遍历陷入死循环。所以,先判断是否有环,后判断是否有缺失。
- 然后,在判断依赖是否有环。可采用图的深度优先搜索、拓扑排序算法,以及结合图的矩阵存储来判断。本次用图的深度优先搜索算法进行实现。
- 再者,判断依赖是否存在缺失。分为两方面,一方面是依赖文件requests的缺失,另一方面是依赖目录的缺失,这一点在实现时,先将所有子目录名存名放在一个列表中,而后在遍历依赖时*判断依赖的目录是否存在于列表,若不存在则存在缺失*
- 最后,命令行方式执行。第一步,需要对命令行进行解析;第二步,对输入不正确的命令要捕获异常并给出提示;第三步,根据参数,执行检测功能。
关于环的类型
- 第一种,顶点本身构成环:
- 第二种,所有顶点构成环:
- 第三种,部分顶点构成环:
- 特别要注意,下面(图8)这个有向图,不构成环,虽然C、D被访问了多次,很多示例都未解决这个问题。
具体实现
- 需要的库:os ,argparse
其中,os库用于遍历目录,argparse库用于解析命令行输入的命令,并执行。 - 在check.py文件中的代码如下:
注: 我将依赖看做一条路径。N的前驱顶点为:在从X->N->Y的路径上,自X开始访问到该目录N之前,已经发现的依赖目录。这些目录已经形成了一条路径。
import os
import argparse
# 格式化子目录名,用作键
def file(folder):
return folder.split('/')[-1]
# 获取上级目录名称
def father_file(input_dir, pardir):
return os.path.abspath(os.path.join(input_dir, pardir))
# 通过requires文件获取依赖关系,建立从A——>B的部分
def build_dependency_graph(folder):
path = os.path.join(folder, 'requires')
if not os.path.exists(folder):
print(f"Error:{file(folder)}目录未建立")
return None
elif os.path.exists(path):
with open(os.path.join(folder, 'requires'), 'r') as f:
lines = list(map(lambda line: line.strip(), f.readlines())) # 读取直接依赖目录 去除字符末尾的空格,将结果置于列表中
return lines
else:
print("folder:",folder)
l = folder.split('/')
if l[-1] != '':
print(f"Error:{file(folder)}目录下未找到requires文件")
return []
# 统计有多少个同级子目录
def get_parent_directory(input_dir):
dir_num = 0
parent_dir = father_file(input_dir, os.pardir) # 获取上级目录的路径
for dir_name in os.listdir(parent_dir): # 统计有多少个子目录
if os.path.isdir(os.path.join(parent_dir, dir_name)):
dir_num += 1
return dir_num
def undependent_or_norequires(fname, get_res):
if get_res == []:
print(f"{fname}目录没有依赖")
return True
# 找前驱顶点
def get_before(g: list, dependency_dice):
before = []
for key, value in dependency_dice.items():
for _g in g:
if _g in value: # 找到直接前驱顶点
before.append(key)
return before
# 对g进行遍历,找指向前驱顶点的路径
def has_path_before(g: list, dependency_dice, parent_dir):
"""1.遍历g,得到邻接顶点
2.要知到前驱顶点有哪些,倒找
3.从前驱顶点中找该邻接顶点
找到,就是环;找不到,就是需要多次被访问的"""
before = set() # 用于记录前驱顶点
# 1.遍历g,得到邻接顶点
neighbor = build_dependency_graph(os.path.join(parent_dir, g[0]))
# 2.找到前驱顶点有哪些
for i in range(len(dependency_dice)):
res = get_before(g, dependency_dice)
if len(res): # 找到前驱顶点
for r in res:
before.add(r)
g = res # 继续找前驱的前驱
# 3.从前驱顶点中找该邻接顶点
for n in neighbor:
if n in before:
return True
return False # 前驱顶点中未找到该邻接顶点
# 检查目录
def check_dependency(folder):
parent_dir = father_file(folder, os.pardir) # 记录上级目录名称
allfile = set()
for dir_name in os.listdir(parent_dir): # 子目录列表
if os.path.isdir(os.path.join(parent_dir, dir_name)):
allfile.add(dir_name) # 获取同级子目录名称
visited = set() # 用以记录访问过的目录
dependency_dice = {} # 用以记录依赖关系
fname = file(folder) # 目录名
folder_num = get_parent_directory(folder) # 获取同级子目录总数
get_res = build_dependency_graph(folder) # 获取依赖关系
visited.add(fname) # 添加已访问过的目录
dependency_dice[fname] = get_res # 记录目录间的依赖关系
stack = [f for f in get_res]
if undependent_or_norequires(fname, get_res):
return False
while stack and folder_num >= 0: # 遍历未访问过的依赖目录
cur_dir = stack.pop()
if cur_dir not in visited:
visited.add(cur_dir)
get_res = build_dependency_graph(os.path.join(parent_dir, cur_dir))
dependency_dice[cur_dir] = get_res # 继续记录目录间的依赖关系
if get_res == None:
return False
elif get_res == [] and cur_dir != '':
print(f"{cur_dir}目录没有依赖")
for i in get_res:
if i not in allfile and i != '':
print(f'{cur_dir}目录所需的{i}目录,缺失')
return False
if i not in visited:
stack.append(i)
# 依赖顶点存在于被访问过的顶点中,且依赖顶点有指向前驱顶点的路径
elif has_path_before(list(cur_dir), dependency_dice, parent_dir):
print(f"{fname}目录——>{cur_dir}目录,存在重复")
return False
else: # 为多次访问的依赖目录
continue
folder_num -= 1
elif has_path_before(list(cur_dir), dependency_dice, parent_dir):
# 依赖顶点存在于被访问过的顶点中,且依赖顶点有指向前驱顶点的路径(如A->B->C->D,C的前驱顶点是A,B),则有环
print(f"{fname}目录——>{cur_dir}目录,存在重复")
return False
else: # 否则则为多次访问的依赖目录
continue
return True
def main():
try:
parser = argparse.ArgumentParser(description='检查文件依赖的合理性') # 定义命令行解析器对象
parser.add_argument('-c', help='指定要检查的目录') # 添加命令行参数,-c
args = parser.parse_args() # 获取结构化解析参数
except BaseException:
print("Error: -c 未指定目录的具体位置,请正确使用命令\nusage: python check.py -c (folder_name)")
return
else:
folder = args.c # 获得检查对象的路径
if not args.c:
print('请指定要检查的目录 \nusage: python check.py -c (folder_name)')
return
if not os.path.exists(folder) or not os.path.isdir(folder): # 判断待检对象是否为已存在的目录
print(f"Error: {folder} 不存在或不是一个目录")
return
if not check_dependency(folder):
print(f"Error: 检验未通过,{folder} 依赖不合理")
return
print(f"检验通过,{folder} 依赖合理")
if __name__ == '__main__':
main()