用于“cat”文件中所有行的成对扩展的命令行工具

Tom*_*den 13 shell awk shell-script text-processing

假设我有一个如下所示的文件(称为 sample.txt):

Row1,10
Row2,20
Row3,30
Row4,40
Run Code Online (Sandbox Code Playgroud)

我希望能够处理来自这个文件的流,它本质上是所有四行的成对组合(所以我们最终应该总共有 16 行)。例如,我正在寻找输出为的流(即高效)命令:

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row1,20 Row2,20
...
Row4,40 Row4,40
Run Code Online (Sandbox Code Playgroud)

我的用例是我想将此输出流式传输到另一个命令(如 awk)中,以计算有关此成对组合的一些指标。

我有一种方法可以在 awk 中做到这一点,但我担心的是我使用 END{} 块意味着我在输出之前基本上将整个文件存储在内存中。示例代码:

awk '{arr[$1]=$1} END{for (a in arr){ for (a2 in arr) { print arr[a] " " arr[a2]}}}' samples/rows.txt 
Row3,30 Row3,30
Row3,30 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row1,10 Row1,10
Row1,10 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Run Code Online (Sandbox Code Playgroud)

有没有一种有效的流媒体方式来做到这一点,而不必将文件本质上存储在内存中,然后在 END 块中输出?

PM *_*ing 12

以下是如何在 awk 中执行此操作,以便它不必将整个文件存储在数组中。这与terdon的算法基本相同。

如果你愿意,你甚至可以在命令行上给它多个文件名,它会独立处理每个文件,将结果连接在一起。

#!/usr/bin/awk -f

#Cartesian product of records

{
    file = FILENAME
    while ((getline line <file) > 0)
        print $0, line
    close(file)
}
Run Code Online (Sandbox Code Playgroud)

在我的系统上,这大约是 terdon 的 perl 解决方案时间的 2/3。


mik*_*erv 7

我不知道这是不是内存做得更好,但有一个sedrEADS出其INFILE在其INFILE每一行和另一对管道的另一侧交流H与输入线老空间...

cat <<\IN >/tmp/tmp
Row1,10
Row2,20
Row3,30
Row4,40
IN

</tmp/tmp sed -e 'i\
' -e 'r /tmp/tmp' | 
sed -n '/./!n;h;N;/\n$/D;G;s/\n/ /;P;D'
Run Code Online (Sandbox Code Playgroud)

输出

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40
Run Code Online (Sandbox Code Playgroud)

我用另一种方式做到了这一点。它确实在内存中存储了一些- 它存储了一个字符串,如:

"$1" -
Run Code Online (Sandbox Code Playgroud)

...对于文件中的每一行。

pairs(){ [ -e "$1" ] || return
    set -- "$1" "$(IFS=0 n=
        case "${0%sh*}" in (ya|*s) n=-1;; (mk|po) n=+1;;esac
        printf '"$1" - %s' $(printf "%.$(($(wc -l <"$1")$n))d" 0))"
    eval "cat -- $2 </dev/null | paste -d ' \n' -- $2"
}
Run Code Online (Sandbox Code Playgroud)

它非常快。它cat的的文件,因为有文件的行中多次|pipe。在管道的另一侧,输入与文件本身合并的次数与文件中的行数相同。

case东西就是便携性-yashzsh两个加一个元素拆分,同时mkshposh两赔一。ksh, dash, busybox, 和bash所有字段都拆分为与 打印的零一样多的字段printf。正如上面所写的那样,对于我机器上的每一个上述外壳,都呈现相同的结果。

如果文件长,则可能存在$ARGMAX参数过多的问题,在这种情况下,您还需要引入xargs或类似。

给定我在输出相同之前使用的相同输入。但是,如果我要变大...

seq 10 10 10000 | nl -s, >/tmp/tmp
Run Code Online (Sandbox Code Playgroud)

这会生成一个与我之前使用的几乎相同的文件(无“行”) - 但有 1000 行。你可以亲眼看看它有多快:

time pairs /tmp/tmp |wc -l

1000000
pairs /tmp/tmp  0.20s user 0.07s system 110% cpu 0.239 total
wc -l  0.05s user 0.03s system 32% cpu 0.238 total
Run Code Online (Sandbox Code Playgroud)

在 1000 行时,shell 之间的性能略有不同 -bash总是最慢的 - 但因为无论如何它们所做的唯一工作是生成 arg 字符串(1000 个副本filename -),所以影响很小。两者之间的性能差异zsh- 如上所述 -bash在这里是 100 秒。

这是另一个适用于任何长度文件的版本:

pairs2()( [ -e "$1" ] || exit
    rpt() until [ "$((n+=1))" -gt "$1" ]
          do printf %s\\n "$2"
          done
    [ -n "${1##*/*}" ] || cd -P -- "${1%/*}" || exit
    : & set -- "$1" "/tmp/pairs$!.ln" "$(wc -l <"$1")"
    ln -s "$PWD/${1##*/}" "$2" || exit
    n=0 rpt "$3" "$2" | xargs cat | { exec 3<&0
    n=0 rpt "$3" p | sed -nf - "$2" | paste - /dev/fd/3
    }; rm "$2"
)
Run Code Online (Sandbox Code Playgroud)

/tmp使用半随机名称创建到其第一个参数的软链接,以便它不会挂在奇怪的文件名上。这很重要,因为cat的 args 通过管道通过xargs. cat的输出被保存到<&3while sed prints 第一个 arg 中的每一行与该文件中的行一样多 - 并且它的脚本也通过管道提供给它。再次paste合并它的输入,但这次-它的标准输入和链接名称再次只需要两个参数/dev/fd/3

最后一个 -/dev/fd/[num]链接 - 应该适用于任何 linux 系统以及更多其他系统,但如果它没有创建命名管道mkfifo并使用它,则应该也可以使用。

它所做的最后一件事是rm在退出之前创建的软链接。

这个版本在我的系统上实际上仍然更快。我想这是因为虽然它执行了更多的应用程序,但它会立即开始向它们传递参数——而在它之前先将它们全部堆叠起来。

time pairs2 /tmp/tmp | wc -l

1000000
pairs2 /tmp/tmp  0.30s user 0.09s system 178% cpu 0.218 total
wc -l  0.03s user 0.02s system 26% cpu 0.218 total
Run Code Online (Sandbox Code Playgroud)


ter*_*don 5

好吧,你总是可以在你的 shell 中做到这一点:

while read i; do 
    while read k; do echo "$i $k"; done < sample.txt 
done < sample.txt 
Run Code Online (Sandbox Code Playgroud)

它比您的awk解决方案慢很多(在我的机器上,1000 行需要约 11 秒,而 1000 行需要约 0.3 秒awk),但至少它在内存中永远不会超过几行。

上面的循环适用于您在示例中拥有的非常简单的数据。它会被反斜杠阻塞,并且会吃掉尾随和前导空格。同样的事情的一个更强大的版本是:

while IFS= read -r i; do 
    while IFS= read -r k; do printf "%s %s\n" "$i" "$k"; done < sample.txt 
done < sample.txt 
Run Code Online (Sandbox Code Playgroud)

另一种选择是使用perl

perl -lne '$line1=$_; open(A,"sample.txt"); 
           while($line2=<A>){printf "$line1 $line2"} close(A)' sample.txt
Run Code Online (Sandbox Code Playgroud)

上面的脚本将读取输入文件 ( -ln) 的每一行,将其另存为$lsample.txt再次打开,并将每一行与$l. 结果是所有成对组合,而只有 2 行存储在内存中。在我的系统上,这0.6在 1000 行上只需要大约几秒钟。

  • 不得不对你的 while 循环投反对票。其中有 4 种不同的不良做法。你比较清楚。 (2认同)
  • @StéphaneChazelas 好吧,根据你的回答[此处](http://unix.stackexchange.com/q/65803/22222),我想不出“echo”可能成为问题的任何情况。我写的内容(我现在添加了“printf”)应该适用于所有这些,对吧?至于“while”循环,为什么?`while read f; 有什么问题吗?做 ..; 完成&lt;文件`? 当然,您并不是在建议“for”循环!还有什么选择? (2认同)
  • @cuonglm,那只是暗示了一个人应该避免它的可能原因。在 _conceptual_、_reliability_、_legibility_、_performance_ 和 _security_ 方面,仅涵盖 _reliability_。 (2认同)