【问题标题】:How to efficiently loop through the lines of a file in Bash?如何有效地循环遍历 Bash 中的文件行?
【发布时间】:2018-08-25 18:56:24
【问题描述】:

我有一个文件example.txt,大约有 3000 行,每行都有一个字符串。一个小文件的例子是:

>cat example.txt
saudifh
sometestPOIFJEJ
sometextASLKJND
saudifh
sometextASLKJND
IHFEW
foo
bar

我想检查这个文件中所有重复的行并输出它们。期望的输出是:

>checkRepetitions.sh
found two equal lines: index1=1 , index2=4 , value=saudifh
found two equal lines: index1=3 , index2=5 , value=sometextASLKJND

我做了一个脚本checkRepetions.sh

#!bin/bash
size=$(cat example.txt | wc -l)
for i in $(seq 1 $size); do
i_next=$((i+1))
line1=$(cat example.txt | head -n$i | tail -n1)
for j in $(seq $i_next $size); do
line2=$(cat example.txt | head -n$j | tail -n1)
if [ "$line1" = "$line2" ]; then
echo "found two equal lines: index1=$i , index2=$j , value=$line1"
fi
done
done

但是这个脚本很慢,运行需要10多分钟。在 python 中,它需要不到 5 秒...我尝试通过执行 lines=$(cat example.txt) 和执行 line1=$(cat $lines | cut -d',' -f$i) 将文件存储在内存中,但这仍然很慢...

【问题讨论】:

  • Bash FAQ 001
  • @EdMorton 我编辑了它,我想现在很清楚了
  • 听起来你应该看看 sort 和 uniq 命令。
  • @EdMorton 我想要一个高效的解决方案,如果文件有十行,我可以用我的代码轻松完成,但可以编辑它。
  • @Pedro, ...如果您想要一个最有效的解决方案,bash 是不适合这项工作的工具。尽管所有的命令替换,外部命令调用,&c。使你的代码比它可能的效率低许多 base-10 数量级(也就是说,是的,数百或数千倍) - 如果你不是,一个 BashFAQ #1 while read 循环通常是正确的事情对性能非常敏感,因此完全优选使用不同的语言。

标签: linux bash shell scripting


【解决方案1】:

当你不想使用awk(工作的好工具,只解析一次输入)时, 您可以多次运行这些线路。排序很昂贵,但此解决方案避免了您尝试过的循环。

grep -Fnxf <(uniq -d <(sort example.txt)) example.txt

使用uniq -d &lt;(sort example.txt),您可以找到所有出现多次的行。接下来grep 将搜索这些(选项-f)完整的(-x)行,没有正则表达式(-F)并显示它出现的行(-n)。

【讨论】:

    【解决方案2】:

    为了演示一种相对有效的(在语言和运行时的限制内)native-bash 方法,您可以在https://ideone.com/iFpJr7 的在线解释器中看到它运行:

    #!/bin/bash
    case $BASH_VERSION in ''|[123].*) echo "ERROR: bash 4.0 required" >&2; exit 1;; esac
    
    # initialize an associative array, mapping each string to the last line it was seen on
    declare -A lines=( )
    lineNum=0
    
    while IFS= read -r line; do
      lineNum=$(( lineNum + 1 ))
      if [[ ${lines[$line]} ]]; then
         printf 'found two equal lines: index1=%s, index2=%s, value=%s\n' \
           "${lines[$line]}" "$lineNum" "$line"
      fi
      lines[$line]=$lineNum
    done <example.txt
    

    注意使用while read 逐行迭代,如BashFAQ #1 中所述:如何逐行(或逐字段)读取文件?时间>;这允许我们只打开文件一次并通读它,而不需要任何命令替换(分叉子shell)或外部命令(每次调用它们时都需要由操作系统单独启动,并且同样昂贵)。

    这里改进的另一部分是我们只读取整个文件一次 - 实现 O(n) 算法 - 而不是像原始代码那样运行 O(n^2) 比较。

    【讨论】:

    • 要明确 - “相对高效”仍然意味着比等效的 awk 脚本慢几个数量级:-)。我以为是这种情况,但只是测试了一下,如果你在乎的话,请查看my answer 的底部...
    • 当然。 100,000 行的 15 秒比 OP 的原始代码快得多,很多 - 非常合理,快到足以适合他们的实际用例。
    • @EdMorton,...顺便说一句,您是否有兴趣对 ksh93 进行基准测试,因为您正在进行测量?一些细微的语法差异,是的,但这是一种缩小与 awk 差距的方法,同时仍然是 POSIX 超集 shell 语言。
    • 不是特别重要,但更重要的是我现在没有可用的 ksh93(我在笔记本电脑上运行 cygwin)。你认为 ksh93 和 bash 的时间安排会有很大的不同吗?
    • 是的——ksh 通常比 bash 性能好得多;我见过编写良好的 ksh93 代码通常在 awk 的一个 base-10 数量级内。
    【解决方案3】:

    请参阅why-is-using-a-shell-loop-to-process-text-considered-bad-practice,了解您的脚本如此缓慢的一些原因。

    $ cat tst.awk
    { val2hits[$0] = val2hits[$0] FS NR }
    END {
        for (val in val2hits) {
            numHits = split(val2hits[val],hits)
            if ( numHits > 1 ) {
                printf "found %d equal lines:", numHits
                for ( hitNr=1; hitNr<=numHits; hitNr++ ) {
                    printf " index%d=%d ,", hitNr, hits[hitNr]
                }
                print " value=" val
            }
        }
    }
    
    $ awk -f tst.awk file
    found 2 equal lines: index1=1 , index2=4 , value=saudifh
    found 2 equal lines: index1=3 , index2=5 , value=sometextASLKJND
    

    为了让您了解使用编写得尽可能高效的 bash 脚本和等效的 awk 脚本的性能差异:

    bash:

    $ cat tst.sh
    #!/bin/bash
    case $BASH_VERSION in ''|[123].*) echo "ERROR: bash 4.0 required" >&2; exit 1;; esac
    
    # initialize an associative array, mapping each string to the last line it was seen on
    declare -A lines=( )
    lineNum=0
    
    while IFS= read -r line; do
      (( ++lineNum ))
      if [[ ${lines[$line]} ]]; then
         printf 'Content previously seen on line %s also seen on line %s: %s\n' \
           "${lines[$line]}" "$lineNum" "$line"
      fi
      lines[$line]=$lineNum
    done < "$1"
    
    $ time ./tst.sh file100k > ou.sh
    real    0m15.631s
    user    0m13.806s
    sys     0m1.029s
    

    awk:

    $ cat tst.awk
    lines[$0] {
        printf "Content previously seen on line %s also seen on line %s: %s\n", \
           lines[$0], NR, $0
    }
    { lines[$0]=NR }
    
    $ time awk -f tst.awk file100k > ou.awk
    real    0m0.234s
    user    0m0.218s
    sys     0m0.016s
    

    两个脚本的输出没有区别:

    $ diff ou.sh ou.awk
    $
    

    以上是使用第 3 次运行时间来避免缓存问题并针对由以下 awk 脚本生成的文件进行测试:

    awk 'BEGIN{for (i=1; i<=10000; i++) for (j=1; j<=10; j++) print j}' > file100k
    

    当输入文件的重复行为零(由seq 100000 &gt; nodups100k 生成)时,bash 脚本的执行时间与上面大致相同,而 awk 脚本的执行速度比上面快得多:

    $ time ./tst.sh nodups100k > ou.sh
    real    0m15.179s
    user    0m13.322s
    sys     0m1.278s
    
    $ time awk -f tst.awk nodups100k > ou.awk
    real    0m0.078s
    user    0m0.046s
    sys     0m0.015s
    

    【讨论】:

    • 强烈不同意您的链接。 OP 使用的做法比链接的答案批评的要慢几个数量级;用被描述为缓慢或高效的代码替换他们的代码将使其比现在快数百倍。
    • fork()ing 子shell 和exec()ing 单独catheadtail 每行输入的可执行文件的成本相比,该链接中描述的原因完全无关紧要. OP 的代码进行逐字节读取会比现在快得多。
    • 链接的文章涵盖了这一点,并用通俗的语言解释了为什么会出现问题,例如:Invoking a tool though has a cost ...A process has to be created, the tool has to be loaded, initialised, then cleaned-up, process destroyed and waited for. Invoking cut is like opening the kitchen drawer, take the knife, use it, wash it, dry it, put it back in the drawer
    • 在 Mac 上,我看到 bash 版本的第三次运行时间为 7.079 秒,而 awk 为 0.328 秒,ksh93 为 2.0 秒(小于 3.2 秒, 在一个以 10 为底的数量级内)。对 bash 代码的唯一更改是取消版本检查并使用 typeset -A 而不是 declare -A
    猜你喜欢
    • 2013-04-08
    • 1970-01-01
    • 2014-11-19
    • 2012-07-25
    • 1970-01-01
    • 1970-01-01
    • 2020-09-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多