2

Unix command to find lines common in two files的问题有一个答案,建议使用该comm命令来完成任务:

comm -12 1.sorted.txt 2.sorted.txt

这显示了两个文件-1共有的行(抑制仅在第一个文件中-2的行,并抑制仅在第二个文件中的行,只留下两个文件共有的行作为输出)。正如文件名所暗示的,输入文件必须按排序顺序排列。

在对该问题的评论中, bapors问道:

如何将输出保存在不同的文件中?

为了澄清,我问:

如果您只想在一个文件中的 File1 中的行,在另一个文件中仅在 File2 中的行,以及在第三个文件中的行,那么(假设文件中的所有行都没有以选项卡开头)您可以使用sed拆分输出三个文件。

用户bapors确认:

这正是我要问的。你能举个例子吗?

答案相对冗长,并且会破坏另一个问题的答案的简单性(用大量信息淹没它),所以我在这里单独提出了这个问题 - 并且也提供了一个答案。

4

2 回答 2

4

使用的基本解决方案sed依赖于comm仅在第一个文件中找到的不带前缀的输出行;它使用单个选项卡输出仅在第二个文件中找到的行;它使用两个选项卡输出在两个文件中找到的行。

它还依赖sed'sw命令来写入文件。

给定文件1.sorted.txt包含:

1.line-1
1.line-2
1.line-4
1.line-6
2.line-2
3.line-5

和文件2.sorted.txt包含:

1.line-3
2.line-1
2.line-2
2.line-4
2.line-6
3.line-5

的基本输出comm 1.sorted.txt 2.sorted.txt是:

1.line-1
1.line-2
        1.line-3
1.line-4
1.line-6
        2.line-1
                2.line-2
        2.line-4
        2.line-6
                3.line-5

给定一个包含以下内容的文件script.sed

/^\t\t/ {
    s///
    w file.3
    d
}
/^\t/ {
    s///
    w file.2
    d
}
/^[^\t]/ {
    w file.1
    d
}

您可以运行如下所示的命令并获得所需的输出,如下所示:

$ comm 1.sorted.txt 2.sorted.txt | sed -f script.sed
$ cat file.1
1.line-1
1.line-2
1.line-4
1.line-6
$ cat file.2
1.line-3
2.line-1
2.line-4
2.line-6
$ cat file.3
2.line-2
3.line-5
$

该脚本通过以下方式工作:

  1. 匹配以 2 个选项卡开头的行,删除选项卡,将行写入file.3,然后删除该行(因此脚本的其余部分被忽略),
  2. 匹配以 1 个选项卡开头的行,删除选项卡,将行写入file.2,然后删除该行(因此脚本的其余部分被忽略),
  3. 匹配不以制表符开头的行,将该行写入file.1,然后删除该行。

第 3 步中的匹配和删除操作更多是为了对称;它们可以被省略(只留下w file.1),这个脚本也可以工作。但是,请参见script3.sed下文了解保持对称性的进一步理由。

正如所写,这需要 GNU sed;BSDsed不识别\t转义符。显然,可以用实际的制表符代替\t符号来编写文件,然后 BSDsed就可以使用脚本了。

可以让它在命令行上运行,但它很繁琐(这是礼貌的)。使用 Bash 的ANSI C Quoting,您可以编写:

$ comm 1.sorted.txt 2.sorted.txt |
> sed -e $'/^\t\t/  { s///\n w file.3\n d\n }' \
>     -e $'/^\t/    { s///\n w file.2\n d\n }' \
>     -e $'/^[^\t]/ {        w file.1\n d\n }'
$

它将三个“段落”中的每一个都写script.sed在一个单独的-e选项中。w命令很繁琐;它需要文件名,并且只有文件名,在脚本的同一行上,因此在脚本中\n的文件名之后使用。有很多可以消除的空间,但是显示的布局对称性更加清晰。而且使用该-f script.sed文件可能更简单——这当然是一种值得了解的技术,因为它可以避免sed脚本必须在单引号、双引号和反引号上操作时出现问题,这使得在 Bash 命令行上编写脚本变得困难。

最后,如果这两个文件可以包含以制表符开头的行,则此技术需要更多的蛮力才能使其工作。一种变体解决方案利用 Bash 的进程替换在文件中的行之前添加前缀,然后后处理sed脚本在写入输出文件之前删除前缀。

script3.sed(用最多 8 个空格替换制表符)- 请注意,这次s///在第三段中需要一个替代项(d仍然是可选的,但也可以包括在内):

/^              X/ {
    s///
    w file.3
    d
}
/^      X/ {
    s///
    w file.2
    d
}
/^X/ {
    s///
    w file.1
    d
}

和命令行:

$ comm <(sed 's/^/X/' 1.sorted.txt) <(sed 's/^/X/' 2.sorted.txt) |
> sed -f script3.sed
$

对于相同的输入文件,这会产生相同的输出,但是通过在每行的开头添加然后删除X,代码不会更改数据的排序顺序,并且会处理前导制表符(如果存在)。

您还可以轻松编写使用 Perl 或 Awk 的解决方案,甚至不必使用这些解决方案comm(并且可以使用未排序的文件,只要文件适合内存)。

于 2017-09-21T05:55:11.543 回答
0

comm + awk解决方案:

复杂的示例文件:

1.txt

1. line-1 with spaces (                 |   | here
1.line-2
1.line-4    with tabs > 
 1.line-6
2.line-2
        3.line-5 (tabs)

2.txt

1.line-3
  2.line-1 with spaces
2.line-2
2.line-4
    2.line-6 with tabs
        3.line-5 (tabs)

工作:

comm -12 1.txt 2.txt > file-common 
awk 'NR==FNR{ a[$0];next }!($0 in a){ print $0 > "file"ARGIND-1 }' file-common 1.txt 2.txt
  • comm -12 1.txt 2.txt > file-common- 将常用行保存到file-common文件

  • awk ...- 将分别打印文件1.txt和文件中唯一的行2.txtfile1file2


查看结果:

head file*
==> file1 <==
1. line-1 with spaces (                 |   | here
1.line-2
1.line-4    with tabs > 
 1.line-6

==> file2 <==
1.line-3
  2.line-1 with spaces
2.line-4
    2.line-6 with tabs

==> file-common <==
2.line-2
        3.line-5 (tabs)
于 2017-09-21T10:30:50.377 回答