测量每个目录特定文件类型的磁盘使用情况(递归地,作为“du --include”的演示)

测量每个目录特定文件类型的磁盘使用情况(递归地,作为“du --include”的演示)

这是我的工作代码,但我相信它没有优化 - 必须有一种方法可以比这更快地完成工作:

find . -type f -iname '*.py' -printf '%h\0' |
  sort -z -u |
  xargs -r -0 -I{} sh -c '
    find "{}" -maxdepth 1 -type f -iname "*.py" -print0 |
      xargs -r -0 du -sch |
      tail -1 |
      cut -f1 |
      tr "\n" " "
    echo -e "{}"' |
  sort -k1 -hr |
  head -50

目标是递归搜索包含的所有目录,*.py然后按每个目录的名称打印所有文件的总大小*.py,按大小逆序对它们进行排序,并仅显示前 50 个。

有什么想法可以改进此代码(性能方面)但保持相同的输出吗?

编辑:

我在以下示例中测试了您的建议:47GB total: 5805 files 不幸的是,我无法将其从头到尾进行比较,因为并非所有建议都遵循相同的准则:总大小应该是磁盘使用情况,分隔符应该只是一个空格。格式应如下:numfmt --to=iec-i --suffix=B

以下 4 个是排序输出,但 David 显示的是文件的累积大小,而不是真实的磁盘使用情况。然而,他的改进是显着的:速度提高了 9.5 倍以上。 Stéphane 和 Isaac 的代码是非常接近的赢家,因为他们的代码比参考代码快大约 32 倍。

$ time madjoe.sh
real    0m2,752s
user    0m3,022s
sys     0m0,785s

$ time david.sh 
real    0m0,289s
user    0m0,206s
sys     0m0,131s

$ time isaac.sh 
real    0m0,087s
user    0m0,032s
sys     0m0,032s

$ time stephane.sh 
real    0m0,086s
user    0m0,013s
sys     0m0,047s

遗憾的是,以下代码既不排序也不显示最大 50 个结果(此外,在之前与 Isaac 的代码进行比较时,以下代码比 Isaac 的改进慢约 6 倍):

$ time hauke.sh 
real    0m0,567s
user    0m0,609s
sys     0m0,122s

答案1

通过将所有目录总和收集在一个数组中并在最后全部打印(使用 GNU awk)来简化 @HaukeLaging 的解决方案。此外,只需要一次调用numfmt(在最后)。

#!/bin/sh

find . -type f -iname '*.py' -printf '%s %h\0' |
    awk 'BEGIN { RS="\0"; };

         { gsub(/\\/,"&&"); gsub(/\n/,"\\n");
           size=$1; sub("[^ ]* ",""); dirsize[$0]+=size }

         END {   PROCINFO["sorted_in"] = "@val_num_desc";
                 i=0;
                 for ( dir in dirsize ) { if(++i<=50) 
                     { print dirsize[dir], dir; }else{ exit } 
                 }
             }        ' | numfmt --to=iec-i --suffix=B

这会生成 py 文件的累积表观大小(而不是其磁盘使用情况),并避免对目录的子目录中的文件进行求和。

答案2

要计算磁盘使用情况而不是表观大小的总和,您需要使用%b¹ 而不是%s并确保每个文件仅计算一次,因此类似于:

LC_ALL=C find . -iname '*.py' -type f -printf '%D:%i\0%b\0%h\0' |
  gawk -v 'RS=\0' -v OFS='\t' -v max=50 '
    {
      inum = $0
      getline du
      getline dir
    }
    ! seen[inum]++ {
      gsub(/\\/, "&&", dir)
      gsub(/\n/, "\\n", dir)
      sum[dir] += du
    }
    END {
      n = 0
      PROCINFO["sorted_in"] = "@val_num_desc"
      for (dir in sum) {
        print sum[dir] * 512, dir
        if (++n >= max) break
      }
    }' | numfmt --to=iec-i --suffix=B --delimiter=$'\t'

目录名称中的换行符呈现为\n,反斜杠(至少在当前语言环境中解码的反斜杠)呈现为\\

如果在多个目录中找到一个文件,则该文件将根据第一个找到的目录进行计数(顺序不确定)。

POSIXLY_CORRECT它假设环境中没有变量(如果有,设置PROCINFO["sorted_in"]无效,gawk因此列表不会被排序)。如果您不能保证这一点,您始终可以gawkenv -u POSIXLY_CORRECT gawk ...(假设 GNUenv或兼容;或(unset -v POSIXLT_CORRECT; gawk ...))开始。

您的方法还存在一些其他问题:

  • 如果没有LC_ALL=C,GNUfind不会报告其名称在语言环境中不构成有效字符的文件,因此您可能会错过一些文件。
  • 嵌入{}代码中sh构成任意代码注入漏洞。例如,考虑一个名为$(reboot).py.您永远不应该这样做,文件的路径应作为额外参数传递,并使用位置参数在代码中引用。
  • echo不能用于显示任意数据(尤其是-e在这里没有意义的数据)。代替使用printf
  • 如果文件列表很大,则可能会多次调用,在这种情况下,最后一行将仅包含上次运行的总数xargs -r0 du -schdu

1%b以 512 字节为单位报告磁盘使用情况。 512 字节是磁盘分配的最小粒度,因为这是传统扇区的大小。还有%kwhich is int(%b / 2),但这会在具有 512 字节块的文件系统上给出不正确的结果(文件系统块通常是 2 的幂且至少为 512 字节大)

²LC_ALL=C也用于 gawk 会使其效率更高一些,但可能会破坏使用 BIG5 或 GB18030 字符集的语言环境中的输出(并且文件名也以该字符集进行编码),因为反斜杠的编码也在编码中找到那里还有其他一些角色。

³ 请注意,如果您的shis bash,在脚本中POSIXLY_CORRECT设置为,并且如果以或开头,则将其导出到环境中,因此该变量也可能会无意中潜入。yshsh-a-o allexport

答案3

我怀疑你需要写自己的 du。

目前,您使用两个 find 和一个 du 对层次结构进行三次递归。

我建议从 perl 的File::Find包开始。

或者,您的第一个查找可能会输出类似的内容-printf '%k %h\n',然后您可以按目录排序,使用 perl 或 awk (甚至 bash)来总计目录并转换为“人类”可读,最后排序和头。

无论哪种方式,您都应该 A) 仅遍历目录树一次,B) 创建尽可能少的进程。

编辑:示例实现

#!/bin/bash

find . -type f -iname '*.py' -printf '%k %h\n' | sort -k2 | (
    at=
    bt=
    output() {
        if [[ -n "$at" ]]
        then
            printf '%s\t%s\n' "$at" "$bt"
        fi
    }
    while read a b
    do
        if [[ "$b" != "$bt" ]]
        then
            output
            bt="$b"
            at=0
        fi
        at=$(( $at + $a ))
    done
    output
) | sort -hr | head -50 | numfmt -d'   ' --field=1 --from-unit=Ki --to=iec-i

注意:%k 很重要。 %s 报告表观大小,而 %k(和du)报告磁盘大小。它们对于稀疏文件和大文件有所不同。 (如果你愿意du --apparent-size,那就这样吧。)

注意:numfmt 应该放在最后,因此它会运行一次。使用“%k”时,需要指定起始单位。

注意:numfmt 的 -d 参数应包含单个选项卡。我无法在这里输入,并且 numfmt 不会接受-d'\t'。如果分隔符不是制表符,则间距会变得混乱。因此我在主体中使用了 printf 而不是 echo 。 (另一种方法是使用 echo 和最后的 sed 将第一个空格更改为制表符。

注意:我最初错过了第一次排序,并在重新测试中得到了某些目录的重复条目。

注意:numfmt 是最近才出现的。

答案4

这可能会快很多,但它不完全等同于你的方法吗?它不会对子目录文件进行两次计数:

find . -type f -iname '*.py' -printf '%s %h\0' |
    awk 'BEGIN { RS="\0"; }; '\
'{ pos=index($0," "); size=substr($0,1,(pos-1)); dir=substr($0,pos+1); gsub("\n","\\n",dir); '\
'if(dir!=lastdir) { if(NR>1) { "numfmt --to=iec-i --suffix=B " sizesum " | tr -d \"\n\"" | getline fsize; print fsize " " lastdir; } '\
'sizesum=size; lastdir=dir; } '\
'else sizesum=sizesum+size; }; '\
'END { "numfmt --to=iec-i --suffix=B " sizesum " | tr -d \"\n\"" | getline fsize; print fsize " " lastdir; }'

3,2KiB ./dir1
1,1MiB ./dir2

除了速度更快之外,它还用文字替换换行符\n。如果您期望目录名称带有换行符,那么您必须处理它们,直到管道结束,这是您的代码不执行的操作。

相关内容