关于图的应用——循环依赖检测(DFS+Python实现)

功能要求

假设一个测试目录test,有若干子目录A/B/C。 每个子目录(例如A)中都拥有一个requires文件,requires里每行是子目录名字(例如B),代表此目录的依赖项。编写一个脚本check.py,测试指定子目录中的依赖项是否合理,即是否存在缺失或者循环的依赖关系等(例如A->B->A)。若不合理则需要进行详细分析并打印原因。
执行方式:python check.py -c (folder_name)
图1-功能要求
test目录内容如下图所示:图2-test目录
子目录内容及requests文件内容(以G目录举例),如下图所示:
图3-子目录G
图4-子目录G的requests目录内容

问题分析

  • 首先,涉及到遍历目录,查找依赖,依赖关系是一个比较复杂的,具体来看,A目录依赖于B目录,这是一个单向关系,所以关系具有方向性,结合有向图考虑。
  • 其次,依赖是否存缺失或者循环,是否缺失容易判断,但是在遍历过程中,先要排除环的存在,避免遍历陷入死循环。所以,先判断是否有环,后判断是否有缺失
  • 然后在判断依赖是否有环。可采用图的深度优先搜索拓扑排序算法,以及结合图的矩阵存储来判断。本次用图的深度优先搜索算法进行实现
  • 再者判断依赖是否存在缺失。分为两方面,一方面是依赖文件requests的缺失,另一方面是依赖目录的缺失,这一点在实现时,先将所有子目录名存名放在一个列表中,而后在遍历依赖时*判断依赖的目录是否存在于列表,若不存在则存在缺失*
  • 最后,命令行方式执行。第一步,需要对命令行进行解析;第二步,对输入不正确的命令要捕获异常并给出提示;第三步,根据参数,执行检测功能。

关于环的类型

  • 第一种,顶点本身构成环:
    图5-顶点本身构成环
  • 第二种,所有顶点构成环:图6-所有顶点构成环
  • 第三种,部分顶点构成环:
    图7-部分顶点构成环
  • 特别要注意,下面(图8)这个有向图,不构成环,虽然C、D被访问了多次,很多示例都未解决这个问题。图8-被多次访问但不构成环的有向图

具体实现

  • 需要的库: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()
  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值