【问题标题】:How do I build an iterator for walking a file tree recursively?如何构建迭代器以递归地遍历文件树?
【发布时间】:2020-01-24 08:35:45
【问题描述】:

我想在对每个级别的兄弟姐妹进行排序时,一个一个地懒惰地消耗文件树的节点。

在 Python 中,我会使用同步生成器:

def traverse_dst(src_dir, dst_root, dst_step):
    """
    Recursively traverses the source directory and yields a sequence of (src, dst) pairs;

    """
    dirs, files = list_dir_groom(src_dir) # Getting immediate offspring.

    for d in dirs:
        step = list(dst_step)
        step.append(d.name)
        yield from traverse_dst(d, dst_root, step)

    for f in files:
        dst_path = dst_root.joinpath(step)
        yield f, dst_path

在 Elixir 中,一个(惰性)流:

def traverse_flat_dst(src_dir, dst_root, dst_step \\ []) do
  {dirs, files} = list_dir_groom(src_dir) # Getting immediate offspring.

  traverse = fn d ->
    step = dst_step ++ [Path.basename(d)]
    traverse_flat_dst(d, dst_root, step)
  end

  handle = fn f ->
    dst_path =
      Path.join(
        dst_root,
        dst_step
      )

    {f, dst_path}
  end

  Stream.flat_map(dirs, traverse)
  |> Stream.concat(Stream.map(files, handle))
end

可以看到一些解决递归的语言特性:Python 中的yield from,Elixir 中的flat_map;后者看起来像是一种经典的函数式方法。

看起来 Rust 中任何惰性的东西,它总是一个迭代器。我应该如何在 Rust 中或多或少地做同样的事情?

我想保留递归函数的结构,将 dirsfiles 作为路径向量(它们可以选择排序和过滤)。

获取dirsfiles 已经按照我的喜好实现了:

fn folders(dir: &Path, folder: bool) -> Result<Vec<PathBuf>, io::Error> {
    Ok(fs::read_dir(dir)?
        .into_iter()
        .filter(|r| r.is_ok())
        .map(|r| r.unwrap().path())
        .filter(|r| if folder { r.is_dir() } else { !r.is_dir() })
        .collect())
}

fn list_dir_groom(dir: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
    let mut dirs = folders(dir, true).unwrap();
    let mut files = folders(dir, false).unwrap();
    if flag("x") {
        dirs.sort_unstable();
        files.sort_unstable();
    } else {
        sort_path_slice(&mut dirs);
        sort_path_slice(&mut files);
    }
    if flag("r") {
        dirs.reverse();
        files.reverse();
    }
    (dirs, files)
}

Vec&lt;PathBuf 可以按原样迭代,有标准的flat_map 方法。应该可以实现 Elixir 风格,只是我还想不通。

这是我已经拥有的。真的工作 (traverse_flat_dst(&amp;SRC, [].to_vec());),我的意思是:

fn traverse_flat_dst(src_dir: &PathBuf, dst_step: Vec<PathBuf>) {
    let (dirs, files) = list_dir_groom(src_dir);

    for d in dirs.iter() {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        println!("d: {:?}; step: {:?}", d, step);
        traverse_flat_dst(d, step);
    }
    for f in files.iter() {
        println!("f: {:?}", f);
    }
}

我想要的(还没有工作!):

fn traverse_flat_dst_iter(src_dir: &PathBuf, dst_step: Vec<PathBuf>) {
    let (dirs, files) = list_dir_groom(src_dir);

    let traverse = |d| {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        traverse_flat_dst_iter(d, step);

    };
    // This is something that I just wish to be true!
    flat_map(dirs, traverse) + map(files)    
}

本着 Elixir 解决方案的精神,我希望这个函数能够提供一个长而扁平的文件迭代器。我还不能应付必要的返回类型和其他语法。我真的希望这次足够清楚。

我设法编译和运行的(没有意义,但签名是我真正想要的):

fn traverse_flat_dst_iter(
    src_dir: &PathBuf,
    dst_step: Vec<PathBuf>,
) -> impl Iterator<Item = (PathBuf, PathBuf)> {
    let (dirs, files) = list_dir_groom(src_dir);

    let _traverse = |d: &PathBuf| {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        traverse_flat_dst_iter(d, step)
    };
    files.into_iter().map(|f| (f, PathBuf::new()))
}

我还缺少什么:

fn traverse_flat_dst_iter(
    src_dir: &PathBuf,
    dst_step: Vec<PathBuf>,
) -> impl Iterator<Item = (PathBuf, PathBuf)> {
    let (dirs, files) = list_dir_groom(src_dir);

    let traverse = |d: &PathBuf| {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        traverse_flat_dst_iter(d, step)
    };
    // Here is a combination amounting to an iterator,
    // which delivers a (PathBuf, PathBuf) tuple on each step.
    // Flat mapping with traverse, of course (see Elixir solution).
    // Iterator must be as long as the number of files in the tree.
    // The lines below look very close, but every possible type is mismatched :(
    dirs.into_iter().flat_map(traverse)
        .chain(files.into_iter().map(|f| (f, PathBuf::new())))

}

【问题讨论】:

  • 请添加minimal reproducible example。没有任何 Rust 代码,很难回答。
  • 这个文件迭代器是开源的:docs.rs/walkdir/2.2.9/walkdir
  • This是源代码吗?本质淹没在选项中,就像现实生活中的代码一样。
  • 当堆栈如此易于使用且如此高效(并且还可以被线程池轻松消耗)时,为什么还要进行递归调用?
  • 小心这种说法,@AlexeyOrlov。递归在视觉上更容易,而不是在性能上,除非语言和/或平台明确提供。 Rust 没有。

标签: recursion rust tree iterator


【解决方案1】:

这是我寻求的确切解决方案。这不是我的成就;见here。欢迎评论。

fn traverse_flat_dst_iter(
    src_dir: &PathBuf,
    dst_step: Vec<PathBuf>,
) -> impl Iterator<Item = (PathBuf, PathBuf)> {
    let (dirs, files) = list_dir_groom(src_dir);

    let traverse = move |d: PathBuf| -> Box<dyn Iterator<Item = (PathBuf, PathBuf)>> {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        Box::new(traverse_flat_dst_iter(&d, step))
    };
    dirs.into_iter()
        .flat_map(traverse)
        .chain(files.into_iter().map(|f| (f, PathBuf::new())))
}

另一个更复杂的镜头。必须将东西装箱,克隆要在 lambda 之间共享的参数等,以满足编译器的要求。然而它确实有效。希望 on 能掌握窍门。

fn traverse_dir(
    src_dir: &PathBuf,
    dst_step: Vec<PathBuf>,
) -> Box<dyn Iterator<Item = (PathBuf, Vec<PathBuf>)>> {
    let (dirs, files) = groom(src_dir);
    let destination_step = dst_step.clone(); // A clone for handle.

    let traverse = move |d: PathBuf| {
        let mut step = dst_step.clone();
        step.push(PathBuf::from(d.file_name().unwrap()));
        traverse_dir(&d, step)
    };
    let handle = move |f: PathBuf| (f, destination_step.clone());
    if flag("r") {
        // Chaining backwards.
        Box::new(
            files
                .into_iter()
                .map(handle)
                .chain(dirs.into_iter().flat_map(traverse)),
        )
    } else {
        Box::new(
            dirs.into_iter()
                .flat_map(traverse)
                .chain(files.into_iter().map(handle)),
        )
    }
}

【讨论】:

    【解决方案2】:

    有两种方法:

    第一个是使用现有的 crate,例如 walkdir。好处是它经过了很好的测试并提供了许多选择。

    第二个是编写自己的迭代器实现。这是一个示例,也许是您自己的基础:

    struct FileIterator {
        dirs: Vec<PathBuf>, // the dirs waiting to be read
        files: Option<ReadDir>, // non recursive iterator over the currently read dir
    }
    
    impl From<&str> for FileIterator {
        fn from(path: &str) -> Self {
            FileIterator {
                dirs: vec![PathBuf::from(path)],
                files: None,
            }
        }
    }
    
    impl Iterator for FileIterator {
        type Item = PathBuf;
        fn next(&mut self) -> Option<PathBuf> {
            loop {
                while let Some(read_dir) = &mut self.files {
                    match read_dir.next() {
                        Some(Ok(entry)) => {
                            let path = entry.path();
                            if let Ok(md) = entry.metadata() {
                                if md.is_dir() {
                                    self.dirs.push(path.clone());
                                    continue;
                                }
                            }
                            return Some(path);
                        }
                        None => { // we consumed this directory
                            self.files = None;
                            break; 
                        }
                        _ => { }
                    }
                }
                while let Some(dir) = self.dirs.pop() {
                    let read_dir = fs::read_dir(&dir);
                    if let Ok(files) = read_dir {
                        self.files = Some(files);
                        return Some(dir);
                    }
                }
                break; // no more files, no more dirs
            }
            return None;
        }
    }
    

    playground

    编写自己的迭代器的优势在于,您可以根据您的精确需求(排序、过滤、错误处理等)对其进行调整。但是您必须处理自己的错误。

    【讨论】:

    • 谢谢!我会花一些时间研究它。 “为什么递归”是不言自明的:这看起来不像 Python/Elixir 解决方案那么好和可读。
    • @AlexeyOrlov 使用 walk_dir 将是两行。请注意,在现实生活中您必须处理意外问题,例如权限、无效链接、inode 等。作为奖励,这是我如何(并行)计算 dir 大小的总和:github.com/Canop/broot/blob/master/src/file_sizes/…(我基本上使用一个通道代替堆栈)
    • 最后一件事:我刚刚为您的问题编写了 FileIterator。它没有经过很好的测试,可能会更优雅
    • 再次感谢您!我的任务比较具体。权限问题不太可能出现,但排序、过滤和向前/向后移动应该可以顺利进行。
    • @AlexeyOrlov 为了排序,您可以使用替换 files: Option&lt;DirEntry&gt;files: Option&lt;std::vec::IntoIter&lt;PathBuf&gt;&gt; 并通过首先将条目存储到向量中然后对其进行排序来构建它。没有直接允许排序的read_dir
    猜你喜欢
    • 2019-12-01
    • 2014-03-29
    • 2011-11-24
    • 2018-07-12
    • 2014-03-11
    • 1970-01-01
    • 2014-08-21
    • 1970-01-01
    • 2016-12-15
    相关资源
    最近更新 更多