在多个文件上运行脚本的更快方法?

在多个文件上运行脚本的更快方法?

我有下面的 bash 脚本,是我用我非常差的 bash 知识创建的,用于将 JPG/PNG 文件批量转换为 JPEG XL,到目前为止,该脚本可以很好地满足我的需求,没有任何问题。

我唯一无法解决的问题是优化负责检查图像是否具有与“视觉无损”JPEG XL 不兼容的 ICC 配置文件的循环部分。

我最初的想法是将 Find 和 Parallel 与 IF 和 ELSE 结合在一起,但没有取得任何成功,只导致大量语法错误输出,因此作为替代方法,我选择使用循环,但在具有多个文件的文件夹中检查过程很慢,有时比转换本身花费的时间更长,所以我问,如何优化脚本的这一部分?

#!/bin/bash

# create a copy of all folders and subfolder inside a path called jxl #
find . -type d -not -path "./jxl/*" -exec mkdir -p ./jxl/{} \; -exec mkdir -p ./jxl/icc/{} \;
rmdir ./jxl/jxl
rmdir ./jxl/icc/jxl

# move images with a NOT compatible icc profile to a directory called icc inside jxl path #
dir="./jxl/icc"
icc1="Device Model                    : "
icc2="Device Model                    : NONE"
icc3="Device Model                    : MS30"
shopt -s globstar
for f in **/*.jpg **/*.jpeg **/*.jpe **/*.png
do
   check=$(exiftool -devicemodel "$f")
   if [ "$check" = "$icc1" ] || [ "$check" = "$icc2" ] || [ "$check" = "$icc3" ]; then      
      echo "$f = icc profile NOT compatible"
      mv "$f" "$dir/$f"
   else
      echo "$f = icc profile compatible"
   fi
done

# Run cjxl encoder e ignore all files inside the jxl folder
find ./ -type f \( -iname \*.jpg -o -iname \*.jpeg -o -iname \*.jpe -o -iname \*.png \) -not -path "./jxl/*" -print0 | parallel --jobs 8 -0 cjxl '{}' './jxl/{.}.jxl' -d 1 -e 7 -E 3 -I 1 --lossless_jpeg 0\;

# copy all files that are not a image to the jxl folder
find ./ -type f \( -iname \*.* ! -iname \*.jpg ! -iname \*.jpeg ! -iname \*.jpe ! -iname \*.png ! -iname \*.sh ! -iname \*.html \) -not -path "./jxl/*" -print0 | parallel --jobs 5 -0 mv '{}' './jxl/{}' \;

#delete all empty folders inside the jxl folder
find ./jxl -type d -empty -delete

答案1

我能看到的唯一真正明显的改进是避免多次分叉外部程序。这是否值得“修复”的麻烦取决于每个命令被分叉了多少次findbash……如果它只是少数或数十个文件,则可能不会。如果是数百或数千,那么肯定是的。

例如,在第一个find,你正在跑步mkdir 两次对于找到的每个文件。您可以通过将其编写为来优化它:

find . -type d -not -path "./jxl/*" \
  -exec bash -c 'for d; do
                   printf './jxl/%s\0./jxl/icc/%s\0' "$d" "$d";
                 done | xargs -0r mkdir -p ' bash {} +

这用于printf发送 NUL 分隔的目录列表以使用xargs -0r mkdir -p.

稍后在脚本中的for f in **/*.jpg **/*.jpeg **/*.jpe **/*.png循环中,您将为mv每个非 ICC 兼容文件执行一次。您可以通过构建一个包含要移动的文件的数组来优化它,并再次使用printf将 NUL 分隔的列表发送到xargs -0r mv -t "$dir/".

例如:

declare -a mvfiles=()

for f in **/*.jpg **/*.jpeg **/*.jpe **/*.png
do
   check=$(exiftool -devicemodel "$f")
   if [ "$check" = "$icc1" ] || [ "$check" = "$icc2" ] || [ "$check" = "$icc3" ]; then      
      echo "$f = icc profile NOT compatible"
      mvfiles+=("$f")
   else
      echo "$f = icc profile compatible"
   fi
done
printf '%s\0' "${mvfiles[@]}" | xargs -0r mv -t "$dir/"

请注意,这假设您使用的是mv带有-t允许您指定目标目录的选项的 GNU所有源路径名。不值得对其他版本执行此操作,mv因为您必须使用xargs'-I选项,并且这会为每个文件名运行一个选项mv,从而违背了这样做的目的。当然,除非您使用 FreeBSD 版本的xargswith-J选项(其工作方式类似于-I但允许每个命令使用多个参数)。

您可以不使用xargs,只使用 ,mv "${mvfiles[@]}" "$dir/"但是如果您有数千个文件,那么您将面临超过 ARG_MAX 的风险。使用 xargs 可以避免任何风险。

顺便说一句,我不确定如何parallel处理{}(我主要用于xargs -P并行任务),但您可能想研究一下。考虑到您要同时parallel运行 5 个进程,它可能会为每个参数运行一个命令。mv这可能正是您运行时所需要的cjxl(我没有安装它,也不知道它如何处理其文件名参数,所以我无法发表评论),但对于mv您来说,最好mv -t通过管道进入来使用xargs -0r mv -t ./jxl/


另外值得注意的是:使用parallel一次运行多个进程可能无法提供您期望的性能提升。这取决于进程是否受到 I/O 或可用 CPU 功率的限制。

如果他们是不是如果 I/O 带宽匮乏,那么并行运行它们将会带来巨大的性能提升。

如果进程 I/O 匮乏,那么它们在等待输入时大部分时间都会处于空闲状态,特别是当它们最终相互竞争 IO 时。所以,通过实验来找出最优数cjxl在不超过硬件可用 IO 带宽的情况下可以运行的作业数量 - 8可能是正确的数字,或者可能更少。如果你有超过 8 个核心,它甚至可能会更多,但这不太可能。

答案2

我强烈建议您更改脚本以包含从 GNU Parallel 调用的单个 bash 函数:

doit() {
   # Do all processing of a single file here
   # including if statements
}
export -f doit
find ... | parallel doit

这样做的好处是您可以一次在一个文件上测试您的函数。

另一个好处是您可以混合 CPU 和 I/O 密集型部件。因此,如果幸运的话,一项工作将使用您的 CPU,而另一项工作将使用您的磁盘。

相关内容