如何有效循环遍历Bash中的文件行?

时间:2018-08-25 18:56:24

标签: linux bash shell scripting

我有一个文件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)将文件存储在内存中,但这仍然很慢...

3 个答案:

答案 0 :(得分:4)

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

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

使用uniq -d <(sort example.txt),您会发现所有重复出现的行。接下来的grep将搜索不带正则表达式(-f)的这些(选项-x)完整(-F)行,并显示其发生的行(-n

答案 1 :(得分: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脚本,让您了解性能差异:

重击:

$ 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

确认:

$ 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 > 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

答案 2 :(得分:3)

为了演示一种相对有效的(在语言和运行时的限制范围内)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

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

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