从包含数百万个文件的目录 (bash/python/perl) 中通过精确匹配有效地查找数千个文件

我在 Linux 上,我试图从包含数百万个文件的目录 (SOURCE_DIR) 中查找数千个文件。我有一个我需要查找的文件名列表,存储在一个文本文件 (FILE_LIST) 中。该文件的每一行都包含一个名称,对应于 SOURCE_DIR 中的一个文件,文件中有数千行。


## FILE_LIST contain single word file names, each per line

#Name0001

#Name0002

#..

#Name9999

我想将文件复制到另一个目录 (DESTINATION_DIR)。我写了下面的循环,里面有个循环一个一个找。


#!/bin/bash

FILE_LIST='file.list'

## FILE_LIST contain single word file names, each per line

#Name0001

#Name0002

#..

#Name9999


SOURCE_DIR='/path/to/source/files' # Contain millions of files in sub-directories

DESTINATION_DIR='/path/to/destination/files' # Files will be copied to here



while read FILE_NAME

do

    echo $FILE_NAME

    for FILE_NAME_WITH_PATH in `find SOURCE_DIR -maxdepth 3 -name "$FILE_NAME*" -type f -exec readlink -f {} \;`; 

    do 

        echo $FILE

        cp -pv $FILE_NAME_WITH_PATH $DESTINATION_DIR; 

    done

done < $FILE_LIST

这个循环花费了很多时间,我想知道是否有更好的方法来实现我的目标。我进行了搜索,但没有找到解决我的问题的方法。如果已经存在,请直接告诉我解决方案,或者请建议对上述代码进行任何调整。如果有另一种方法甚至是 python/perl 解决方案,我也很好。感谢您的时间和帮助!


呼唤远方
浏览 86回答 4
4回答

泛舟湖上清波郎朗

注意&nbsp; &nbsp;下面添加的处理不同目录中相同名称的代码需要找到要复制的文件,因为它们没有给出路径(不知道它们在哪个目录中),但是重新搜索每个文件非常浪费,大大增加了复杂性。相反,首先为每个文件名构建一个具有完整路径名的散列。一种方法,使用 Perl,利用快速核心模块File::Finduse warnings;use strict;use feature 'say';use File::Find;use File::Copy qw(copy);my $source_dir = shift // '/path/to/source';&nbsp; # give at invocation or defaultmy $copy_to_dir = '/path/to/destination';my $file_list = 'file_list_to_copy.txt';&nbsp;&nbsp;open my $fh, '<', $file_list or die "Can't open $file_list: $!";my @files = <$fh>;chomp @files;my %fqn;&nbsp; &nbsp;&nbsp;find( sub { $fqn{$_} = $File::Find::name&nbsp; unless -d }, $source_dir );# Now copy the ones from the list to the given location&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;foreach my $fname (@files) {&nbsp;&nbsp; &nbsp; copy $fqn{$fname}, $copy_to_dir&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; or do {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; warn "Can't copy $fqn{$fname} to $copy_to_dir: $!";&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; next;&nbsp; &nbsp; &nbsp; &nbsp; };}剩下的问题是关于可能存在于多个目录中的文件名,但是我们需要得到一个规则来决定接下来要做什么。†我忽略了问题中使用的最大深度,因为它无法解释并且在我看来是与极端运行时相关的修复(?)。此外,文件被复制到一个“平面”结构中(不恢复其原始层次结构),从问题中得到提示。最后,我只跳过目录,而其他各种文件类型都有自己的问题(复制链接需要小心)。要仅接受普通文件,请更改unless -d 为if -f.†澄清说,确实,不同目录中可能存在同名文件。那些应该复制到相同的名称,在扩展名之前以序号为后缀。为此,我们需要检查一个名称是否已经存在,并在构建哈希时跟踪重复的名称,因此这将花费更长的时间。那么如何解释重名有一个小难题呢?我在 arrayrefs 中使用另一个哈希值,其中只保留了被欺骗的名称‡ ;这简化并加快了工作的两个部分。my (%fqn, %dupe_names);find( sub {&nbsp; &nbsp; return if -d;&nbsp; &nbsp; (exists $fqn{$_})&nbsp; &nbsp; &nbsp; &nbsp; ? push( @{ $dupe_names{$_} }, $File::Find::name )&nbsp; &nbsp; &nbsp; &nbsp; : ( $fqn{$_} = $File::Find::name );}, $source_dir );令我惊讶的是,即使现在对每个项目运行测试,它的运行速度也比不考虑重复名称的代码慢一点点,在 25 万个文件上分布在一个庞大的层次结构中。三元运算符中赋值周围的括号是必需的,因为运算符可能被赋值给(如果最后两个参数是有效的“左值”,就像它们在这里一样),因此需要小心分支内的赋值。然后在%fqn按照帖子的主要部分复制之后,还复制其他同名文件。我们需要分解文件名以便在之前添加枚举.ext;我使用核心File::Basenameuse File::Basename qw(fileparse);foreach my $fname (@files) {&nbsp;&nbsp; &nbsp; next if not exists $dupe_names{$fname};&nbsp; # no dupe (and copied already)&nbsp; &nbsp; my $cnt = 1;&nbsp; &nbsp; foreach my $fqn (@{$dupe_names{$fname}}) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; my ($name, $path, $ext) = fileparse($fqn, qr/\.[^.]*/);&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; copy $fqn, "$copy_to_dir/${name}_$cnt$ext";&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; or do {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; warn "Can't copy $fqn to $copy_to_dir: $!";&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; next;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };&nbsp; &nbsp; &nbsp; &nbsp; ++$cnt;&nbsp; &nbsp; }}(已完成基本测试,但仅此而已)我可能会使用undef而不是$path上面的方法来指示该路径未使用(同时这也避免了分配和填充标量),但为了那些不熟悉模块的子返回的内容的人清楚,我将其保留为这种方式。笔记。&nbsp; &nbsp;对于具有重复项的文件,将有副本fname.ext、fname_1.ext等。如果您希望将它们全部编入索引,则首先将fname.ext(在目标位置,它已通过 复制%fqn)重命名为fname_1.ext,并将计数器初始化更改为my $cnt = 2;。‡请注意,这些文件不一定是相同的文件。

智慧大石

我怀疑速度问题(至少部分)来自您的嵌套循环 - 对于每个FILE_NAME,您都在运行 afind并循环其结果。下面的 Perl 解决方案使用动态构建正则表达式的技术(适用于大型列表,我已经在 100k+ 单词的列表上进行了测试),这样你只需要遍历文件一次并让正则表达式引擎处理其余部分;这相当快。请注意,根据我对您的脚本的阅读,我做了几个假设:您希望模式在文件名的开头区分大小写,并且您希望在目标中重新创建与源相同的目录结构(设置$KEEP_DIR_STRUCT=0如果你不想要这个)。此外,我正在使用不完全是最佳实践的解决方案,find而不是使用 Perl 自己的解决方案,File::Find因为它可以更容易地实现您正在使用的相同选项(例如-maxdepth 3) - 但它应该可以正常工作,除非有名称中带有换行符的任何文件。该脚本仅使用核心模块,因此您应该已经安装了它们。#!/usr/bin/env perluse warnings;use strict;use File::Basename qw/fileparse/;use File::Spec::Functions qw/catfile abs2rel/;use File::Path qw/make_path/;use File::Copy qw/copy/;# user settingsmy $FILE_LIST='file.list';my $SOURCE_DIR='/tmp/source';my $DESTINATION_DIR='/tmp/dest';my $KEEP_DIR_STRUCT=1;my $DEBUG=1;# read the file listopen my $fh, '<', $FILE_LIST or die "$FILE_LIST: $!";chomp( my @files = <$fh> );close $fh;# build a regular expression from the list of filenames# explained at: https://www.perlmonks.org/?node_id=1179840my ($regex) = map { qr/^(?:$_)/ } join '|', map {quotemeta}&nbsp; &nbsp; sort { length $b <=> length $a or $a cmp $b } @files;# prep dest dirmake_path($DESTINATION_DIR, { verbose => $DEBUG } );# use external "find"my @cmd = ('find',$SOURCE_DIR,qw{ -maxdepth 3 -type f -exec readlink -f {} ; });open my $cmd, '-|', @cmd or die $!;while ( my $srcfile = <$cmd> ) {&nbsp; &nbsp; chomp($srcfile);&nbsp; &nbsp; my $basename = fileparse($srcfile);&nbsp; &nbsp; # only interested in files that match the pattern&nbsp; &nbsp; next unless $basename =~ /$regex/;&nbsp; &nbsp; my $newname;&nbsp; &nbsp; if ($KEEP_DIR_STRUCT) {&nbsp; &nbsp; &nbsp; &nbsp; # get filename relative to the source directory&nbsp; &nbsp; &nbsp; &nbsp; my $relname = abs2rel $srcfile, $SOURCE_DIR;&nbsp; &nbsp; &nbsp; &nbsp; # build new filename in destination directory&nbsp; &nbsp; &nbsp; &nbsp; $newname = catfile $DESTINATION_DIR, $relname;&nbsp; &nbsp; &nbsp; &nbsp; # create the directories in the destination (if necessary)&nbsp; &nbsp; &nbsp; &nbsp; my (undef, $dirs) = fileparse($newname);&nbsp; &nbsp; &nbsp; &nbsp; make_path($dirs, { verbose => $DEBUG } );&nbsp; &nbsp; }&nbsp; &nbsp; else {&nbsp; &nbsp; &nbsp; &nbsp; # flatten the directory structure&nbsp; &nbsp; &nbsp; &nbsp; $newname = catfile $DESTINATION_DIR, $basename;&nbsp; &nbsp; &nbsp; &nbsp; # warn about potential naming conflicts&nbsp; &nbsp; &nbsp; &nbsp; warn "overwriting $newname with $srcfile\n" if -e $newname;&nbsp; &nbsp; }&nbsp; &nbsp; # copy the file&nbsp; &nbsp; print STDERR "cp $srcfile $newname\n" if $DEBUG;&nbsp; &nbsp; copy($srcfile, $newname) or die "copy('$srcfile', '$newname'): $!";}close $cmd or die "external command failed: ".($!||$?);您可能还想考虑使用硬链接而不是复制文件。

暮色呼如

和rsync我不知道这对于数百万个文件会有多快,但这是一种使用rsync.按以下格式设置您的格式file.list(例如:如 with $ cat file.list | awk '{print "+ *" $0}')。+ *Name0001+ *Name0002...+ *Name9999在命令中file.list使用--include=from选项调用:rsync$ rsync -v -r --dry-run --filter="+ **/" --include-from=/tmp/file.list --filter="- *" /path/to/source/files /path/to/destination/files选项说明:-v&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : Show verbose info.-r&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : Traverse directories when searching for files to copy.--dry-run&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: Remove this if preview looks okay--filter="+ *./"&nbsp; &nbsp; : Pattern to include all directories in search--include-from=/tmp/file.list&nbsp; : Include patterns from file.--filter="- *"&nbsp; &nbsp; &nbsp; : Exclude everything that didn't match previous patterns.期权订单很重要。--dry-run如果详细信息看起来可以接受,请删除。测试rsync版本 3.1.3。

HUX布斯

这是带有 的 bashv4+ 解决方案find,但不确定速度。#!/usr/bin/env bashfiles=file.listsourcedir=/path/to/source/filesdestination=/path/to/destination/filesmapfile -t lists < "$files"total=${#lists[*]}while IFS= read -rd '' files; do&nbsp; counter=0&nbsp; while ((counter < total)); do&nbsp; &nbsp; if [[ $files == *"${lists[counter]}" ]]; then&nbsp; &nbsp; &nbsp; echo cp -v "$files" "$destination" && unset 'lists[counter]' && break&nbsp; &nbsp; fi&nbsp; &nbsp; ((counter++))&nbsp; done&nbsp; lists=("${lists[@]}")&nbsp; total=${#lists[*]}&nbsp; (( ! total )) && break&nbsp; ##: if the lists is already emtpy/zero, break.done < <(find "$sourcedir" -type f -print0)如果在 file.list 和 source_directory 中的文件中找到匹配项,则innerbreak将退出内部循环,因此它不会处理 file.list 直到最后,它会删除"${lists[@]}"(这是一个数组)中的条目,unset所以下一个内部循环将跳过已经匹配的文件。文件名冲突应该不是问题,unset并且内部break确保了这一点。不利的一面是,如果您在不同的子目录中有多个文件要匹配。如果速度是您所追求的,那么请使用通用脚本语言,例如python,perl和 friends循环内(极慢的)模式匹配的替代方法是grep#!/usr/bin/env bashfiles=file.listsource_dir=/path/to/source/filesdestination_dir=/path/to/destination/fileswhile IFS= read -rd '' file; do&nbsp; cp -v "$file" "$destination_dir"done < <(find "$source_dir" -type f -print0 | grep -Fzwf "$files")-zfromgrep是一个 GNU 扩展。echo如果您认为输出正确,请删除。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python