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


13

假设我有一个看起来像这样的文件(称为sample.txt):

Row1,10
Row2,20
Row3,30
Row4,40

我希望能够处理此文件中的流,该流本质上是所有四行的成对组合(因此我们应该最终得到总共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

我的用例是我想将此输出流式传输到另一个命令(如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

是否有一种有效的流式传输方法来执行此操作,而不必本质上将文件存储在内存中,然后在END块中输出?


1
您始终需要先读取一个文件,然后才能开始为另一文件的第二行生成输出。您可以流式传输的另一个文件。
reinierpost 2014年

Answers:


12

这是在awk中执行此操作的方法,这样它就不必将整个文件存储在数组中。这基本上与terdon算法相同。

如果愿意,您甚至可以在命令行上为它提供多个文件名,它将单独处理每个文件,并将结果串联在一起。

#!/usr/bin/awk -f

#Cartesian product of records

{
    file = FILENAME
    while ((getline line <file) > 0)
        print $0, line
    close(file)
}

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


1
谢谢!这个问题的所有解决方案都很棒,但是由于1)简单和2)呆在awk中,我最终选择了这个解决方案。谢谢!
汤姆·海登

1
汤姆,很高兴您喜欢它。这些天,我倾向于主要使用Python进行编程,但是我仍然喜欢awk用于逐行文本处理,因为它内置了行和文件循环。而且它通常比Python快。
下午14年

7

我不确定这是否比在内存中做的更好,但是它的作用是sed使rinfile中的每一行都减少其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'

输出值

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

我以另一种方式做到了。它确实将一些内容存储在内存中-它存储如下字符串:

"$1" -

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

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"
}

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

case东西就是便携性- yashzsh两个加一个元素拆分,同时mkshposh两赔一。kshdashbusybox,和bash所有拆分出到完全一样,因为有通过零作为印刷许多领域printf。如上所写,对于我的机器上的每个上述外壳,上述结果都是相同的。

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

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

seq 10 10 10000 | nl -s, >/tmp/tmp

生成的文件几乎与我以前使用的文件相同(没有“ Row”),但文件数为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

在1000行时,外壳之间的性能略有差异- 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"
)

/tmp使用半随机名称创建到其第一个arg的软链接,这样它就不会挂在奇怪的文件名上。这很重要,因为cat的args通过管道通过管道馈入xargscat的输出被保存到<&3同时sed p,因为有在该文件线路中的第一个参数的每一行rints多次-和它的脚本还经由配管供给到它。再次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

对函数是否应该位于文件中,如果没有,您将如何声明它?

@Jidder-我要怎么声明?您可以将其复制并粘贴到终端中,对吗?
mikeserv 2014年

1
声明功能。因此,您可以!我以为您可以转义换行符,但是,我很谨慎地只是粘贴代码,谢谢:)这也非常快,很好的答案!

@Jidder-我通常将它们写在一个活动的shell中,就像ctrl+v; ctrl+j我一样用来获取换行符。
mikeserv 2014年

@Jidder-非常感谢。保持警惕是明智的-对您有好处。它们在文件中也可以正常工作- . ./file; fn_name在这种情况下,您可以将其复制。
mikeserv

5

好吧,您总是可以在shell中执行此操作:

while read i; do 
    while read k; do echo "$i $k"; done < sample.txt 
done < sample.txt 

这比您的awk解决方案要慢很多(在我的机器上,花1000行花了〜11秒,awk而在中花了〜0.3秒),但是至少它在内存中保存的行数不超过几行。

上面的循环适用于示例中非常简单的数据。它会在反斜杠上窒息,并且会占用尾随和前导空格。同一件事的一个更强大的版本是:

while IFS= read -r i; do 
    while IFS= read -r k; do printf "%s %s\n" "$i" "$k"; done < sample.txt 
done < sample.txt 

另一种选择是改为使用perl

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

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


哇谢谢!我不知道为什么perl的解决方案是这样比bash的快得多while语句
汤姆·海登

@TomHayden基本上是因为perl像awk 一样快于bash。
terdon

1
必须为您的while循环投票。那里有4种不同的不良做法。你比较清楚。
斯特凡Chazelas

1
@StéphaneChazelas很好,根据您在此处的回答,我想不出任何echo可能造成问题的情况。我写的内容(我printf现在添加了)应该适合所有这些人?至于while循环,为什么呢?这有什么错while read f; do ..; done < file?当然,您不是在建议for循环!还有什么其他选择?
terdon

2
@cuonglm,这只暗示了一个人应该避免的可能原因。在概念可靠性易读性性能安全性方面,仅涵盖可靠性
斯特凡Chazelas

4

zsh

a=(
Row1,10
Row2,20
Row3,30
Row4,40
)
printf '%s\n' $^a' '$^a

$^a在数组上打开数组的类似括号的扩展(如中的{elt1,elt2})。


4

您可以编译此代码以获得快速结果。
在1000行文件中,它完成大约0.19-0.27秒。

它当前将10000行读入内存(以加快打印到屏幕的速度),如果1000每行有字符,将使用少于10mb内存的内存,我认为这不会成为问题。不过,您可以完全删除该部分,如果确实引起问题,则可以直接打印到屏幕上。

您可以使用g++ -o "NAME" "NAME.cpp"
Where NAME将其保存到的文件的名称以及NAME.cpp此代码保存到的文件的位置进行编译

CTEST.cpp:

#include <iostream>
#include <string>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include <sstream>
int main(int argc,char *argv[])
{

        if(argc != 2)
        {
                printf("You must provide at least one argument\n"); // Make                                                                                                                      sure only one arg
                exit(0);
   }
std::ifstream file(argv[1]),file2(argv[1]);
std::string line,line2;
std::stringstream ss;
int x=0;

while (file.good()){
    file2.clear();
    file2.seekg (0, file2.beg);
    getline(file, line);
    if(file.good()){
        while ( file2.good() ){
            getline(file2, line2);
            if(file2.good())
            ss << line <<" "<<line2 << "\n";
            x++;
            if(x==10000){
                    std::cout << ss.rdbuf();
                    ss.clear();
                    ss.str(std::string());
            }
    }
    }
}
std::cout << ss.rdbuf();
ss.clear();
ss.str(std::string());
}

示范

$ g++ -o "Stream.exe" "CTEST.cpp"
$ seq 10 10 10000 | nl -s, > testfile
$ time ./Stream.exe testfile | wc -l
1000000

real    0m0.243s
user    0m0.210s
sys     0m0.033s

3
join -j 2 file.txt file.txt | cut -c 2-
  • 通过不存在的字段加入并删除第一个空格

字段2为空,并且对于file.txt中的所有元素均相等,因此join会将每个元素与所有其他元素连接在一起:实际上,它在计算笛卡尔积。


2

Python的一种选择是对文件进行内存映射,并充分利用Python正则表达式库可以直接与内存映射文件一起使用的事实。尽管看起来像在文件上运行嵌套循环,但内存映射可确保操作系统以最佳方式发挥可用的物理RAM

import mmap
import re
with open('test.file', 'rt') as f1, open('test.file') as f2:
    with mmap.mmap(f1.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m1,\
        mmap.mmap(f2.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m2:
        for line1 in re.finditer(b'.*?\n', m1):
            for line2 in re.finditer(b'.*?\n', m2):
                print('{} {}'.format(line1.group().decode().rstrip(),
                    line2.group().decode().rstrip()))
            m2.seek(0)

可以选择使用Python快速解决方案,尽管可能仍然需要考虑内存效率

from itertools import product
with open('test.file') as f:
    for a, b  in product(f, repeat=2):
        print('{} {}'.format(a.rstrip(), b.rstrip()))
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

根据定义,那不是将整个文件保留在内存中吗?我不了解Python,但您的语言肯定会支持。
terdon

1
@terdon,如果您指的是内存映射解决方案,则操作系统将根据可用的物理RAM透明地仅将文件中尽可能多的文件保留在内存中。可用的物理RAM不必超过文件大小(尽管拥有额外的物理RAM显然是有利的情况)。在最坏的情况下,这可能会降低遍历磁盘上文件的速度,甚至更糟。这种方法的主要优势是透明使用可用的物理RAM,因为这种情况可能会随着时间而波动
iruvar

1

在bash中,仅使用shell内置功能,ksh也应该工作:

#!/bin/bash
# we require array support
d=( $(< sample.txt) )
# quote arguments and
# build up brace expansion string
d=$(printf -- '%q,' "${d[@]}")
d=$(printf -- '%s' "{${d%,}}' '{${d%,}}")
eval printf -- '%s\\n' "$d"

请注意,尽管这将整个文件保存在一个shell变量的内存中,但只需要对其进行一次读取访问。


1
我认为OP的重点是不要将文件保存在内存中。否则,他们目前GAWK方法既简单得多快。我猜想这需要处理大小为数GB的文本文件。
terdon

是的,这是完全正确的-我有几个庞大的数据文件需要执行,并且不想保留在内存中
Tom Hayden 2014年

如果您受到内存的限制,我建议使用@terdon的解决方案之一
Franki

0

sed 解。

line_num=$(wc -l < input.txt)
sed 'r input.txt' input.txt | sed -re "1~$((line_num + 1)){h;d}" -e 'G;s/(.*)\n(.*)/\2 \1/'

说明:

  • sed 'r file2' file1 -读取file1每一行的file2的所有文件内容。
  • 构造1~i表示第1条线,然后是1 + i线,1 + 2 * i,1 + 3 * i,等等。因此,1~$((line_num + 1)){h;d}意味着将h旧的指向缓冲区的线,d删除图案空间并开始新的循环。
  • 'G;s/(.*)\n(.*)/\2 \1/'-对于除上一步中选择的所有行,请执行下一步:G从保持缓冲区中移出一行并将其附加到当前行。然后交换行的位置。是current_line\nbuffer_line\n,成为buffer_line\ncurrent_line\n

输出量

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.