【问题标题】:Split large directory into subdirectories将大目录拆分为子目录
【发布时间】:2016-07-30 09:30:08
【问题描述】:

我有一个包含大约 250 万个文件的目录,超过 70 GB。

我想把它分成子目录,每个子目录有 1000 个文件。

这是我尝试使用的命令:

i=0; for f in *; do d=dir_$(printf %03d $((i/1000+1))); mkdir -p $d; mv "$f" $d; let i++; done

该命令在小范围内对我有用,但我可以让它在这个目录上运行几个小时,但它似乎什么也没做。

我愿意通过命令行以任何方式执行此操作:perl、python 等。无论以最快的方式完成此操作...

【问题讨论】:

  • 您的* 需要一段时间才能扩展。也许从一个更有针对性的文件名子集开始,比如a*,看看它是否会在更合理的时间内返回。您可能还会考虑使用“查找”而不是 for 循环。另外我会担心在这个已经太大的目录中创建子目录。您是否考虑过在其他地方创建它们?
  • 我建议处理find的结果。
  • 只有写得很好的 shell 才能处理多 MB 大小范围内的命令或通配符扩展产生的字符串。在程序中读取目录既快速又容易(参见@ikegami)。 - 如果您想使用 shell 脚本:通过管道将ls 细分为while read ... done 以逐个获取文件。
  • @laune: 让*for 循环中扩展为 250 万个文件名不是问题本身 - 事实上, for f in *; do ...ls | while read ... 快很多。问题是对多个外部实用程序的 250 万次调用。
  • @mklement0 您不希望每个文件都有一个 mv。 1000 倍将其缩小到可管理的大小。 -- 但是最大命令长度为 2.088.198(根据 xargs --show-limits),我认为将 * 扩展为 250 万是行不通的。 ——所有这些只是表明这个任务对于 shell 脚本来说有点太多了。 Perl 已经足够好了,我们都知道它对 C(甚至 Java)来说是小菜一碟。

标签: python bash perl shell command-line


【解决方案1】:

我怀疑如果您检查过,您会注意到您的程序实际上正在移动文件,尽管速度非常慢。启动一个程序是相当昂贵的(至少与进行系统调用相比),并且每个文件要执行三到四次!因此,以下应该更快:

perl -e'
   my $base_dir_qfn = ".";
   my $i = 0;
   my $dir;
   opendir(my $dh, $base_dir_qfn)
      or die("Can'\''t open dir \"$base_dir_qfn\": $!\n");

   while (defined( my $fn = readdir($dh) )) {
      next if $fn =~ /^(?:\.\.?|dir_\d+)\z/;

      my $qfn = "$base_dir_qfn/$fn";

      if ($i % 1000 == 0) {
         $dir_qfn = sprintf("%s/dir_%03d", $base_dir_qfn, int($i/1000)+1);
         mkdir($dir_qfn)
            or die("Can'\''t make directory \"$dir_qfn\": $!\n");
      }

      rename($qfn, "$dir_qfn/$fn")
         or do {
            warn("Can'\''t move \"$qfn\" into \"$dir_qfn\": $!\n");
            next;
         };

      ++$i;
   }
'

【讨论】:

  • 优化和修复错误。
  • 添加说明。
  • 问题在于 250 万次迭代调用多个外部实用程序,而不是预先通过* 建立所有文件名的内部数组。在我运行 Ubuntu 14.04 的笔记本电脑上,对 250 万个文件的 empty 循环只需要大约 25 秒:time for f in *; do :; done 只添加 一个 简单的外部实用程序 - 例如,@987654324 @ - 显着增加执行时间;大约 30 分钟后,我终止了该命令。您的 Perl 解决方案要快得多,因为一切都是在进程中完成的。
【解决方案2】:

如果目录没有被使用,我建议如下

find . -maxdepth 1 -type f | split -l 1000 -d -a 5 

这将创建 n 个名为 x00000 - x02500 的文件(只是为了确保 5 位数字,尽管 4 位也可以)。然后,您可以将每个文件中列出的 1000 个文件移动到相应的目录中。

也许set -o noclobber 以消除在名称冲突的情况下被覆盖的风险。

要移动文件,使用大括号扩展来遍历文件名会更容易

for c in x{00000..02500}; 
do d="d$c"; 
   mkdir $d; 
   cat $c | xargs -I f mv f $d; 
done 

【讨论】:

  • ++,但是 (a) 对于 GNU find,文件名的结果列表将不会被排序,这与 glob * 不同,(b) 假设OP的xargs支持-0,使用tr '\n' '\0' < "$c" | xargs -0 -J f mv f "$d"会快很多; (c) 正如我在 (b) 中的 sn-ps 中所做的那样,我建议双引号引用变量以促进良好的习惯,即使这里并非绝对必要。
  • 是的文件不会被排序,但我不确定这是一个要求。
  • 这不是要求,但考虑到 OP 基于他自己的方法 *,差异值得一提。我刚刚意识到 -J 是一个 BSD xargs 选项;因此,对于 GNU 实用程序,更有效的命令必须声明为 tr '\n' '\0' < "$c" | xargs -0 mv -t "$d";并且明确一点:您的 -I f 方法单独移动每个文件。
  • 有点讽刺的是,您目前使用双引号的唯一地方是从不需要它们(尽管它们不会造成伤害):d="d$c" never需要双引号,与 $c 的值无关。
【解决方案3】:

我会在命令行中使用以下内容:

find . -maxdepth 1 -type f |split -l 1000
for i in `ls x*`
do 
   mkdir dir$i
   mv `cat $i` dir$i& 2>/dev/null
done

关键是“&”,它穿出每个 mv 语句。

感谢 karakfa 的拆分想法。

【讨论】:

  • 虽然在ls x* 的情况下您可能会使用for,因为文件名碰巧没有嵌入空格或通配元字符。它是bad idea in general;此外,`cat $i` 使生成的单词受到单词拆分的影响,您一方面依赖它,但这会导致带有嵌入空格的文件名失败。此外,使用此技术生成的命令行最终可能会太长。鉴于您已经有一个文件 $i,您不能创建一个同名的目录
  • 谢谢,很好的反馈。
【解决方案4】:

注意:ikegami's helpful Perl-based answer 是可行的方法 - 它在一个单个 进程中执行整个操作,因此比下面的 Bash + 标准实用程序解决方案快得多.


基于bash 的解决方案需要避免调用外部实用程序的循环才能合理执行。
您自己的解决方案调用两个外部实用程序 em> 并在每次循环迭代中创建一个 subshel​​l,这意味着您最终将总共创建大约 750 万个进程(!)

以下解决方案避免了循环,但是,考虑到输入文件的绝对数量,仍然需要相当长的时间才能完成(您最终将为每 1000 个输入文件创建 4 个进程,即总共大约 10,000 个进程):

printf '%s\0' * | xargs -0 -n 1000 bash -O nullglob -c '
  dirs=( dir_*/ )
  dir=dir_$(printf %04s $(( 1 + ${#dirs[@]} )))
  mkdir "$dir"; mv "$@" "$dir"' -
  • printf '%s\0' * 打印目录中所有文件的 NUL 分隔列表。
    • 请注意,由于printf 是 Bash 内置 而不是外部实用程序,因此最大。 getconf ARG_MAX 报告的命令行长度适用。
  • xargs -0 -n 1000 调用具有 1000 个输入文件名块的指定命令。

    • 请注意,xargs -0 是非标准的,但在 Linux 和 BSD/OSX 上均受支持。
    • 使用 NUL 分隔的输入可以稳健地传递文件名,而不必担心无意中将它们拆分为多个部分,甚至可以使用嵌入了 换行符 的文件名(尽管这样的文件名非常少见)。
  • bash -O nullglob -c 打开选项nullglob 执行指定的命令字符串,这意味着不匹配任何内容的通配模式将扩展为空字符串。

    • 命令字符串统计到目前为止创建的输出目录,以确定下一个更高索引的下一个输出目录的名称,创建下一个输出目录,并将当前批次(最多)1000个文件移动到那里.

【讨论】:

  • 您可以在目录(但不是文件)上进行循环。
  • @laune:是的,但是没有简单的方法可以稳健地将 1000 个文件名的批次直接从 xargs 传递到 while 循环(您的方法不起作用带有嵌入空格的文件名和/或恰好是有效 glob 的文件名)。通过让 xargs 将每批 1000 个文件名作为单独的参数(不涉及 shell)传递给 bash,它们被正确地保留了。
【解决方案5】:

这可能比 Perl 程序慢(10.000 个文件需要 1 分钟),但它应该适用于任何 POSIX 兼容的 shell。

#! /bin/sh
nd=0
nf=0
/bin/ls | \
while read file;
do
  case $(expr $nf % 10) in
  0)
    nd=$(/usr/bin/expr $nd + 1)
    dir=$(printf "dir_%04d" $nd)
    mkdir $dir
    ;;
  esac
  mv "$file" "$dir/$file"
  nf=$(/usr/bin/expr $nf + 1)

完成

使用 bash,您可以使用算术扩展 $((...))。

当然,这个想法可以通过使用 xargs 来改进 - 对于 250 万个文件,花费的时间不应超过 ~ 45 秒。

nd=0
ls | xargs -L 1000 echo | \
while read cmd;
do
  nd=$((nd+1))
  dir=$(printf "dir_%04d" $nd)
  mkdir $dir
  mv $cmd $dir
done

【讨论】:

  • 请双引号引用您的变量引用,以保护它们免受意外的分词和通配。为了稳健,您的read 命令应以IFS= 为前缀并使用-r:IFS= read -r ...。虽然很少见,但可能存在带有前导或尾随空格或嵌入 ` \ ` 字符的文件名。
  • 您的前 2 个命令分别处理每个文件,这是 OP 自己尝试过的,但未成功。多个外部实用程序的 250 万次调用需要很长时间。
  • 您的 xargs 解决方案并不可靠:它会破坏带有嵌入空格的文件名。
猜你喜欢
  • 2015-10-03
  • 1970-01-01
  • 2014-10-23
  • 2017-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-21
  • 1970-01-01
相关资源
最近更新 更多