按行号过滤文件


17

给定一个文件L,每行一个非负整数,并且文本文件F,那么仅将行号出现在文件L中的那些行保留在F中的快速方法是什么?

例:

$ cat L.txt
1
3

$ cat F.txt
Hello World
Hallo Welt
Hola mundo

$ command-in-question -x L.txt F.txt
Hello World
Hola mundo

我正在寻找一个可以处理具有5亿或更多条目的文件L的命令;文件L按数字排序。

注意:我已经完成了的实现的一半,command-in-question但是我只是想知道,这里是否也可以使用某些Unix工具。


更新:感谢您提供所有答案,我今天学到了很多东西!我想接受一个以上的答案,但这是不可能的。


文件F中有几行?
Peter.O 2015年

1
至少有那么多的行的F类属
三苦

Answers:


8

随着C省略有意义的错误信息:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {

    FILE *L;
    FILE *F;

    unsigned int to_print;
    unsigned int current = 0;
    char *line = NULL;
    size_t len = 0;

    if ((L = fopen(argv[1], "r")) == NULL) {
        return 1;
    } else if ((F = fopen(argv[2], "r")) == NULL) {
        fclose(L);
        return 1;
    } else {

        while (fscanf(L, "%u", &to_print) > 0) {
            while (getline(&line, &len, F) != -1 && ++current != to_print);
            if (current == to_print) {
                printf("%s", line);
            }
        }

        free(line);
        fclose(L);
        fclose(F);
        return 0;
    }
}

2
这是最有效的答案。至少根据我的测试是这样。如果有人感兴趣,我将其编译为:xsel -bo | cc -xc - -o cselect。它只是有效-它只需要两个库。
mikeserv

1
谢谢,太好了!希望您不要介意,但我将您的代码打包成一个小工具
miku

1
@miku前进,很高兴能为您提供帮助。我注意到您LINE_MAX的版本有所增加,因此您可能在文件中使用很大的行。我已经使用getline()删除行大小限制的版本更新了A。
FloHimself

@FloHimself,好,再次感谢:)的确,某些输入行可能会超出LINE_MAX,因此getline似乎是正确的。
2015年

10

我会使用awk,但不会将的全部内容存储L.txt在内存中,并进行不必要的哈希查找;-)。

list=L.txt file=F.txt
LIST="$list" awk '
  function nextline() {
    if ((getline n < list) <=0) exit
  }
  BEGIN{
    list = ENVIRON["LIST"]
    nextline()
  }
  NR == n {
    print
    nextline()
  }' < "$file"

确实,我尝试使用哈希映射,它们会超出内存;比特集将为您提供更多的净空;但是通过使用输入已排序的事实,您可以完全摆脱此(空间)问题。
miku 2015年

1
@Janis; 不仅仅是标准的良好编码实践的一种情况:不要硬编码文字-改用变量...(更灵活,更不易出错,更易于维护)
Peter.O 2015年

1
@StéphaneChazelas:它需要的环预初始化n,否则(不变化)它错过1L.txt
Peter.O

1
@ Peter.O,糟糕,这是我尝试通过NR> = n解决的问题,但这是错误的。现在应该更好。
斯特凡Chazelas

1
@Janis,想法是,如果该代码要嵌入到command-in-question脚本中,那么您就不能在文件中嵌入文件名。-v list="$opt_x"也不是因为awk对它执行反斜杠处理而导致的。这就是为什么我在这里改用ENVIRON的原因。
斯特凡Chazelas

10

grep -n | sort | sed | cut

(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F

无论输入什么大小,它都应该可以很快地工作(下面包括一些定时测试)。有关如何的一些注意事项:

  • export LC_ALL=C
    • 因为以下操作的重点是使整个文件./F与其./Llineno的文件内联,所以我们真正需要担心的唯一字符是ASCII [0-9]数字和:冒号。
    • 因此,担心是否要在一组128种可能的字符中找到这11个字符比使用UTF-8时要容易得多。
  • grep -n ''
    • 这会将字符串LINENO:插入到stdin-或中的每一行的开头<./F
  • sort -t: -nmk1,1 ./L -
    • sort根本不考虑对其输入文件进行排序,而是(正确地)假定它们已-m预先-numerically排序并按排序顺序合并它们,而基本上忽略了任何可能-k1,1出现的-t:冒号字符。
    • 尽管这可能需要一些临时空间(取决于某些序列可能发生的间隔),但与适当的排序相比并不需要太多,并且它非常快,因为它涉及零回溯。
    • sort将输出单个流,其中任何lineno都./L将紧接在中的相应行之前./F./L的行始终排在最前面,因为它们较短。
  • sed /:/d\;n
    • 如果当前行与/:/冒号匹配,d则将其从输出中删除。否则,自动打印当前n行和外部行。
    • 因此,将sedprunes sort的输出输出到与冒号和下一行不匹配的顺序行对-或输出到./L然后是下一行的行。
  • cut -sd: -f2-
    • cut -s从输出中压制不包含其至少一个-d:分隔符字符串之一的输入行-从而./L完全修剪了的行。
    • 对于这些行,它们的第一个:冒号分隔的字段-fcut消失了-所有grep插入的行号也都消失了。

小输入测试

seq 5 | sed -ne'2,3!w /tmp/L
        s/.*/a-z &\& 0-9/p' >/tmp/F

...生成5行样本输入。然后...

(   export LC_ALL=C; </tmp/F \
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)|  head - /tmp[FL]

...印刷品...

==> standard input <==
a-z 1& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/F <==
a-z 1& 0-9
a-z 2& 0-9
a-z 3& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/L <==
1
4
5

更大的定时测试

我创建了几个非常大的文件:

seq 5000000 | tee /tmp/F |
sort -R | head -n1500000 |
sort -n >/tmp/L

...将500万行放入其中,并将150万行/tmp/F随机选择放入/tmp/L。然后我做了:

time \
(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F |wc - l

它打印:

1500000
grep -n '' \
    0.82s user 0.05s system 73% cpu 1.185 total
sort -t: -nmk1,1 /tmp/L - \
    0.92s user 0.11s system 86% cpu 1.185 total
sed /:/d\;n \
    1.02s user 0.14s system 98% cpu 1.185 total
cut -sd: -f2- \
    0.79s user 0.17s system 80% cpu 1.184 total
wc -l \
    0.05s user 0.07s system 10% cpu 1.183 total

(我在那里添加了反斜杠)

在这里当前提供的解决方案中,这是所有解决方案中最快的,但是与我机器上上面生成的数据集相提并论。在其他人中,只有一个接近争夺第二名,那就是perl 在这里

这绝不是原始的解决方案-由于其他人的建议/启发,它减少了执行时间的三分之一。有关较慢的解决方案,请参见发布历史记录(但是为什么?)

另外,值得注意的是,如果不是针对我系统的多CPU体系结构以及该管道中每个进程的并发执行,则其他答案可能会更好。它们都在同一时间工作-每个都在自己的处理器核心上-传递数据并处理整个数据的一小部分。它太酷了。

但是最快的解决方案是...

但这不是最快的解决方案。这里提供的最快的解决方案是C程序。我叫它cselect。将其复制到X剪贴板后,我将其编译为:

xsel -bo | cc -xc - -o cselect

然后我做了:

time \
    ./cselect /tmp/L /tmp/F |
wc -l

...结果是...

1500000
./cselect /tmp/L /tmp/F  \
    0.50s user 0.05s system 99% cpu 0.551 total
wc -l \
    0.05s user 0.05s system 19% cpu 0.551 total

1
您可以使用– sed -ne'/:/!{n;p;}' | cut -d: -f2-而不是sed -ne'/:/!N;/\n/s/[^:]*://p'
–StéphaneChazelas

@StéphaneChazelas-如果切换seds 可能会得到更好的结果- sed我正在使用的是传家宝sed-您可以aliastime结果中看到值。顺便说一下,我的传家宝包是针对Musl libc静态编译的-regex实现基于TRE。当我将其切换到GNU sed并在不使用GNU的情况下运行时,它cut使完成时间增加了整整一秒(2.8秒),复合时间超过了三分之一。这仅比您的系统快0.3秒。
mikeserv

1
sort -mn相对于sort -nmk1,1可能会更好,你不需要做在这里(未测试)做分裂
斯特凡Chazelas

@StéphaneChazelas-是的,我也这么认为,并且尝试了所有方法。-n规范只是在一行上做第一个数字字符串,所以我想出了“好” -mn或“ -nm并且”,无论出于什么原因,它在完成时间中浸入2秒以下的唯一时间就是我按原样添加了所有选项。这很奇怪-这就是昨天我首先没有解决的原因-m-我知道我的意思,但它似乎只是某种自动优化的东西。有趣的是,传家宝sort有一个-z字符串长度的选项,仅适用于-[cm]....
mikeserv

-n不是该行的第一个数字字符串。它只是认为该行的号码,以便abc 123将是0,所以它不能比效率-t: -k1,1
斯特凡Chazelas

9

我会用awk

awk 'NR==FNR {a[$1]; next}; FNR in a' L.txt F.txt

更新:我已经做了绩效评估;似乎此版本可在非常大的数据集(如所述要求的情况)下更好地扩展,因为比较非常快并且过度补偿了构建哈希表所需的工作。


1
@miku; 是的,这是一个不错的紧凑型解决方案。但是要注意:并非所有人都awk能够处理如此庞大的数据集。-我正在使用GNU awk,没有问题;5亿行数据的测试需要7分钟。
贾尼斯(Janis)2015年

1
这相当慢(相比之下) real 16m3.468s- - 。user 15m48.447s sys 0m10.725s它使用的RAM测试1 / 10'th尺寸3.3 GB L50000000线; 并F5亿线- VS时间斯特凡Chazelas' AWK雁:real 2m11.637s- user 2m2.748s- sys 0m6.424s-我不使用快速盒,但比较有趣。
Peter.O 2015年

@ Peter.O; 谢谢你的数据!考虑到(在我自己的测试案例中)有十亿行存储在关联数组中,因此预期速度会降低。(这就是为什么我在Stephane的建议中评论了上面的“(+1)”。)-尽管我惊讶的是,这个简洁的解决方案仍在每秒处理100万行!我认为这使此代码模式(因为它很简单!)是一个可行的选择,尤其是在极端数据量较小的情况下。
Janis 2015年

这绝对是一个可行的解决方案。根据我使用的测试数据(500万行/ 150万升),您的数据在4秒钟多的时间内完成了-仅比斯蒂芬的回答差一秒钟。用于生成测试集的代码在我的答案中,但是它大部分只是seq输出,然后是L中较小的随机选择的子集。
mikeserv

1
我只是做了一些性能测试,数据文件大小为5亿行,关键文件大小为5000万行。5亿行,值得关注。使用较小的密钥文件,时间为4分钟(Stephane)对8分钟(Janis),而对于较大的密钥文件,时间为19分钟(Stephane)对12分钟(Janis)。
Janis 2015年

3

仅出于完整性考虑:我们可以将StéphaneChazelas的答案中优秀的awk脚本与kos的答案中的perl脚本合并,但不将整个列表都存储在内存中,希望perl可能比awk更快。(我已更改了args的顺序以匹配原始问题)。

#!/usr/bin/env perl
use strict;

die "Usage: $0 l f\n" if $#ARGV+1 != 2;
open(L,$ARGV[0]) or die "$ARGV[0]: $!";
open(F,$ARGV[1]) or die "$ARGV[1]: $!";

while(my $number = <L>){
    #chop $number;
    while (<F>) {
        if($. == $number){
            print;
            last;
        }
    }
}

这比awk。它的速度几乎和我的一样快-我刚才进行了两次测试,每次我在1.8 ...秒内处理您的500万行测试集,每次都使用1.9 ...秒。如果愿意的话,testset gen代码在我的答案中,但是重点是它非常好。更重要的是,输出是正确的-我仍然无法完成awk工作...尽管如此,我们两个答案都因FloHimself的使用而蒙羞。
mikeserv

@mikeserv,我们必须有不同awk的。在您的样本中,gawk的值为1.4s(Janis'的值为4s),mawk的值为0.9s,此perl解决方案的值为1.7s,kos'值为2.3s,您的(GNU sed)值为4.5s,您的值为1.4s( GNU sed)和我建议的改进(对于C解决方案为0.5秒)。
斯特凡Chazelas

@mikeserv,啊!当然,根据您的方法,地区会有所不同。从UFT-8切换到C时,从4.5s降至
2.3s。

3

我写了一个简单的Perl脚本来做到这一点:

Usage: script.pl inputfile_f inputfile_f

#!/usr/bin/env perl

$number_arguments = $#ARGV + 1;
if ($number_arguments != 2) {
    die "Usage: script.pl inputfile_f inputfile_l\n";
}

open($f, '<', $ARGV[0])
    or die "$ARGV[0]: Not found\n";
open($l, '<', $ARGV[1])
    or die "$ARGV[1]: Not found\n";

@line_numbers = <$l>;

while ($line = <$f>) {
    $count_f ++;
    if ($count_f == @line_numbers[$count_l]) {
        print $line;
        $count_l ++;
    }
}
  • 负荷 F.txt
  • 负荷 L.txt
  • 将每一行存储L.txt到一个数组中
  • F.txt逐行读取,跟踪其当前行号和当前数组索引;增加F.txt当前行号;如果F.txt当前行号与当前数组索引处的数组内容匹配,它将打印当前行并增加索引

成本和复杂性考虑

考虑到进行分配的成本,进行比较的成本以及打印行的成本,给定N 1作为in中的行数,F.txtN 2作为in中的行数L.txtwhile循环最多运行N 1次,导致2N 1 + N 2分配(显然假设N 1 > N 2),2N 1比较和N 2打印;给定与每个操作相同的成本,运行while循环的总成本为4N 1 + 2N 2,这导致O(N)脚本的复杂性。

测试一千万行的输入文件

使用一个1000万行的F.txt文件,其中包含随机的50个字符长的行,以及一个1000万行的L.txt文件,其中包含从1到10000000的数字(最坏的情况):

~/tmp$ for ((i=0; i<3; i++)); do time ./script.pl F.txt L.txt > output; done

real    0m15.628s
user    0m13.396s
sys 0m2.180s

real    0m16.001s
user    0m13.376s
sys 0m2.436s

real    0m16.153s
user    0m13.564s
sys 0m2.304s

2

该perl解决方案比其他awk或perl解决方案快20%左右,但显然不及C中的解决方案快。

perl -e '
  open L, shift or die $!;
  open F, shift or die $!;
  exit if ! ($n = <L>);
  while (1) {
    $_ = <F>;
    next if $. != $n;
    print;
    exit if ! ($n = <L>);
  }
' -- L F

0
cat <<! >L.txt
1
3
!

cat <<! >F.txt
Hello World
Hallo Welt
Hola mundo
!

cmd(){
 L=$1 F=$2
 cat -n $F |
 join $L - |
 sed 's/[^ ]* //'
}

cmd L.txt F.txt
Hello World
Hola mundo

由于L.txt已排序,因此您可以使用join。只需为F.txt中的每一行编号,将两个文件合并,然后删除行号即可。不需要大的中间文件。

实际上,以上内容将通过用一个空格替换所有空格来破坏您的数据线。为了使行保持完整,您需要选择一些未出现在数据中的字符作为分隔符,例如“ |”。然后是

cmd(){
 L=$1 F=$2
 cat -n $F |
 sed 's/^ *//;s/\t/|/' |
 join -t'|' $L - |
 sed 's/[^|]*|//'
}

第一个sed从“ cat -n”输出中删除前导空格并替换选项卡。第二个sed删除行号和“ |”。


恐怕这不适用于较大的文件。需要少于10行。我有相同的想法,并尝试过,join L.txt <(nl F.txt )但不适用于大文件。顺便说一句,欢迎您访问该网站,我们从新用户那里得到的答案不那么清晰,格式正确!
terdon

@terdon,是的,很遗憾join/ comm无法使用数字排序的输入。
斯特凡Chazelas

@terdon:我跟进了您的线索(现已删除),并尝试了join -t' ' <(<L.txt awk '{printf("%010s\n",$0)}') <(<F.txt awk '{printf("%010s %s\n",NR,$0)}') | cut -d' ' -f2--很慢!-甚至当我使用适当的0填充键输入准备好的文件时join -t' ' L.txt F.txt | cut -d' ' -f2- ,它仍然很慢(不包括准备时间)-比awk@Janis 的答案要慢(我在这里发表了评论,指出了两者的实际时间他和@StéphaneChazelas的答案
Peter.O 2015年

@ Peter.O是的。我尝试了一种类似的方法来避免一次麻烦,但是我找不到一种使它既可行又值得的方法。
terdon

@terdon和其他人:join+ awk printf 流程替换的实际时间是real 20m11.663s user 19m35.093s sys 0m10.513s 对StéphaneChazelas real 2m11.637s user 2m2.748s sys 0m6.424s 使用L5000万行,F5亿行的时间。
Peter.O 2015年

0

为了完整性,join解决方案的另一种尝试:

sed -r 's/^/00000000000000/;s/[0-9]*([0-9]{15})/\1/' /tmp/L | join <( nl -w15 -nrz /tmp/F ) - | cut -d' ' -f2-

这是通过将连接的行号列格式化为固定长度并以前导零表示的,从而使数字始终为15位数字。这避免了联接不喜欢常规数值排序顺序的问题,因为该列现在已被有效地强制为字典排序。 nl用于将这种格式的行号添加到F.txt。不幸sed需要用于重新格式化L.txt中的编号。

这种方法对于使用@mikeserv方法生成的测试数据似乎可以正常工作。但是它仍然非常慢-C解决方案在我的机器上快60倍。大约2/3的时间用在了sed1/3英寸中join。也许有更好的sed表达...


好的-但是为什么我们要把所有零都放在前面呢?我试图对此有所了解。另外,nl的功能超酷,但您不能在未经测试的输入上可靠地使用它。使它如此酷的一件事是其逻辑页面 -d分隔符。默认情况下,如果输入中有任何行仅连续1次,2次,3次或3次仅由字符串组成:\` (但没有尾随的坟墓),那么您的计数就会有些疯狂。进行实验-非常简洁。尤其要看一下当nl`读取包含1个定界符字符串的行,然后再读取另一个带3或2的
行时

0

由于接受的答案在C中,因此我认为可以在此处抛出python解决方案:

# Read mask
with open('L.txt', 'r') as f:
    mask = [int(line_num) for line_num in f.read().splitlines()]

# Filter input file
filtered_lines = []
with open('F.txt', 'r') as f:
    for i, line in enumerate(f.read().splitlines()):
        if (i+1) in mask:
            filtered_lines.append(line)

# Write newly filtered file
with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)

如果使用像numpy这样的外部库,则解决方案看起来会更加精致:

import numpy as np

with open('L.txt', 'r') as f:
    mask = np.array([int(line_num)-1 for line_num in f.read().splitlines()])

with open('F.txt', 'r') as f:
    lines = np.array(f.read().splitlines())
filtered_lines = lines[mask]

with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)
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.