[英]Joining multiple fields in text files on Unix
我該怎么做?
File1看起來像這樣:
foo 1 scaf 3
bar 2 scaf 3.3
File2看起來像這樣:
foo 1 scaf 4.5
foo 1 boo 2.3
bar 2 scaf 1.00
我想要做的是在字段1,2 和 3相同時找到在File1和File2中共同出現的行。
有沒有辦法做到這一點?
這是正確的答案(就使用標准GNU coreutils工具而言,而不是在perl/awk 中編寫自定義腳本)。
$ join -j1 -o1.2,1.3,1.4,1.5,2.5 <(<file1 awk '{print $1"-"$2"-"$3" "$0}' | sort -k1,1) <(<file2 awk '{print $1"-"$2"-"$3" "$0}' | sort -k1,1)
bar 2 scaf 3.3 1.00
foo 1 scaf 3 4.5
好的,它是如何工作的:
首先,我們將使用一個很好的join
工具,它可以合並兩條線。 join
有兩個要求:
我們需要在輸入文件中生成密鑰,為此我們使用一個簡單的awk
腳本:
$ cat file1 foo 1 scaf 3 bar 2 scaf 3.3 $ <file1 awk '{print $1"-"$2"-"$3" "$0}' foo-1-scaf foo 1 scaf 3 bar-2-scaf bar 2 scaf 3.3
你看,我們添加了第一列,其中有一些鍵,比如“ foo-1-scaf ”。 我們對file2做同樣的事情。 順便提一句。 <file awk
,只是寫awk file
或cat file | awk
奇特方式cat file | awk
cat file | awk
。
我們還應該按關鍵字對文件進行排序,在我們的例子中,這是第 1 列,因此我們在命令的末尾添加| sort -k1,1
| sort -k1,1
(按從第 1 列到第 1 列的文本排序)
此時我們可以只生成文件file1.with.key和file2.with.key並加入它們,但假設這些文件很大,我們不想將它們復制到文件系統上。 相反,我們可以使用稱為bash
進程替換的東西將輸出生成到命名管道中(這將避免任何不必要的中間文件創建)。 有關更多信息,請閱讀提供的鏈接。
我們的目標語法是: join <( some command ) <(some other command)
最后一件事是解釋花式連接參數: -j1 -o1.2,1.3,1.4,1.5,2.5
-j1
- 在第一列中通過鍵加入(在兩個文件中) -o
- 僅輸出字段1.2
(第一個文件字段1.2
)、 1.3
(第一個文件第 3 列)等。
這樣我們連接了行,但join
只輸出必要的列。
從這篇文章中吸取的教訓應該是:
廣泛的實驗加上對手冊頁的仔細審查表明您不能直接連接多個列 - 有趣的是,我所有的連接工作示例都只使用一個連接列。
因此,任何解決方案都需要以某種方式將要連接的列連接成一列。 標准 join 命令還要求其輸入按正確的排序順序排列 - 在 GNU join (info coreutils join) 中有一條關於它並不總是需要排序數據的注釋:
但是,作為 GNU 擴展,如果輸入沒有不可配對的行,則排序順序可以是將兩個字段視為相等的任何順序,當且僅當上述排序比較認為它們相等時。
對給定文件執行此操作的一種可能方法是:
awk '{printf("%s:%s:%s %s %s %s %s\n", $1, $2, $3, $1, $2, $3, $4);}' file1 |
sort > sort1
awk '{printf("%s:%s:%s %s %s %s %s\n", $1, $2, $3, $1, $2, $3, $4);}' file2 |
sort > sort2
join -1 1 -2 1 -o 1.2,1.3,1.4,1.5,2.5 sort1 sort2
這會在開始時創建一個復合排序字段,使用 ':' 分隔子字段,然后對文件進行排序 - 對於兩個文件中的每一個。 然后 join 命令連接兩個復合字段,但僅打印出非復合(非聯接)字段。
輸出是:
bar 2 scaf 3.3 1.00
foo 1 scaf 3 4.5
加入 -1 1 -2 1 -1 2 -2 2 -1 3 -2 3 -o 1.1,1.2,1.3,1.4,2.4 file1 file2
在 MacOS X 10.6.3 上,這給出:
$ cat file1 foo 1 scaf 3 bar 2 scaf 3.3 $ cat file2 foo 1 scaf 4.5 foo 1 boo 2.3 bar 2 scaf 1.00 $ join -1 1 -2 1 -1 2 -2 2 -1 3 -2 3 -o 1.1,1.2,1.3,1.4,2.4 file1 file2 foo 1 scaf 3 4.5 bar 2 scaf 3.3 4.5 $
這是加入第 3 場(僅) - 這不是我們想要的。
您確實需要確保輸入文件的排序順序正確。
將前三個字段與 awk 結合起來可能是最簡單的:
awk '{print $1 "_" $2 "_" $3 " " $4}' filename
然后您可以在“字段1”上正常使用join
你可以試試這個
awk '{
o1=$1;o2=$2;o3=$3
$1=$2=$3="";gsub(" +","")
_[o1 FS o2 FS o3]=_[o1 FS o2 FS o3] FS $0
}
END{ for(i in _) print i,_[i] }' file1 file2
輸出
$ ./shell.sh
foo 1 scaf 3 4.5
bar 2 scaf 3.3 1.00
foo 1 boo 2.3
如果你想省略不常見的行
awk 'FNR==NR{
s=""
for(i=4;i<=NF;i++){ s=s FS $i }
_[$1$2$3] = s
next
}
{
printf $1 FS $2 FS $3 FS
for(o=4;o<NF;o++){
printf $i" "
}
printf $NF FS _[$1$2$3]"\n"
} ' file2 file1
輸出
$ ./shell.sh
foo 1 scaf 3 4.5
bar 2 scaf 3.3 1.00
怎么樣:
cat file1 file2
| awk '{print $1" "$2" "$3}'
| sort
| uniq -c
| grep -v '^ *1 '
| awk '{print $2" "$3" "$4}'
這是假設您不太擔心字段之間的空白(換句話說,三個制表符和一個空格與一個空格和 7 個制表符沒有區別)。 當您談論文本文件中的字段時,通常就是這種情況。
它的作用是輸出兩個文件,去掉最后一個字段(因為在比較方面你不關心那個)。 它排序,以便相似的行相鄰然后將它們統一(用一個副本和一個計數替換每組相鄰的相同行)。
然后它去掉所有那些有一次計數(沒有重復)的人,並打印出每一個都去掉計數。 這為您提供了重復行的“鍵”,然后如果您願意,您可以使用另一個 awk 迭代來在文件中定位這些鍵。
如果兩個相同的密鑰只在一個文件中,這將不會按預期工作,因為這些文件很早就被合並了。 換句話說,如果您在file1
有重復的鍵但在file2
沒有,那將是誤報。
然后,我能想到的唯一真正的解決方案是檢查file1
每一行的file2
的解決方案,盡管我相信其他人可能會想出更聰明的解決方案。
而且,對於那些享受一點施虐受虐狂的人來說,這里是上述不太有效的解決方案:
cat file1
| sed
-e 's/ [^ ]*$/ "/'
-e 's/ / */g'
-e 's/^/grep "^/'
-e 's/$/ file2 | awk "{print \\$1\\" \\"\\$2\\" \\"\\$3}"/'
>xx99
bash xx99
rm xx99
這將構造一個單獨的腳本文件來完成這項工作。 對於file1
每一行,它會在腳本中創建一行以在file2
查找該行。 如果您想了解它是如何工作的,請在刪除之前查看xx99
。
而且,在這個中,空格確實很重要,所以如果它對file1
和file2
之間的空格不同的行不起作用,請不要感到驚訝(盡管,與大多數“可怕的”腳本一樣,可以通過另一個管道中的鏈接)。 它更多地是作為您可以為快速不骯臟的工作創造的可怕事物的一個例子。
這不是我會為生產質量代碼做的事情,但一次性很好,前提是你在每日 WTF發現之前銷毀它的所有證據:-)
這是在 Perl 中執行此操作的一種方法:
#!/usr/local/bin/perl
use warnings;
use strict;
open my $file1, "<", "file1" or die $!;
my %file1keys;
while (<$file1>) {
my @keys = split /\s+/, $_;
next unless @keys;
$file1keys{$keys[0]}{$keys[1]}{$keys[2]} = [$., $_];
}
close $file1 or die $!;
open my $file2, "<", "file2" or die $!;
while (<$file2>) {
my @keys = split /\s+/, $_;
next unless @keys;
if (my $found = $file1keys{$keys[0]}{$keys[1]}{$keys[2]}) {
print "Keys occur at file1:$found->[0] and file2:$..\n";
}
}
close $file2 or die $!;
簡單的方法(沒有awk 、 join 、 sed或perl ),使用軟件工具cut
、 grep
和sort
:
cut -d ' ' -f1-3 File1 | grep -h -f - File1 File2 | sort -t ' ' -k 1,2g
輸出(不打印不匹配的行):
bar 2 scaf 1.00
bar 2 scaf 3.3
foo 1 scaf 3
foo 1 scaf 4.5
這個怎么運作...
cut
列出要搜索的所有行。grep
的-f -
開關輸入來自cut
的行並為它們搜索File1和File2 。sort
不是必需的,但可以使數據更易於閱讀。 使用datamash
濃縮結果:
cut -d ' ' -f1-3 File1 | grep -h -f - File1 File2 | \
datamash -t ' ' -s -g1,2,3 collapse 4
輸出:
bar 2 scaf 3.3,1.00
foo 1 scaf 3,4.5
如果File1很大並且有點多余,添加sort -u
應該會加快速度:
cut -d ' ' -f1-3 File1 | sort -u | grep -h -f - File1 File2 | sort -t ' ' -k 1,2g
我曾經與之共事的一位教授創建了一組 perl 腳本,這些腳本可以對面向列的純文本文件執行許多類似數據庫的操作。 它被稱為Fsdb 。 它絕對可以做到這一點,如果這不僅僅是一次性需求(因此您不會經常編寫自定義腳本),則特別值得研究。
與 Jonathan Leffler 提供的解決方案類似的解決方案。
使用不同的分隔符創建 2 個臨時排序文件,其中匹配的列組合在第一個字段中。 然后加入第一個字段上的臨時文件,並輸出第二個字段。
$ cat file1.txt |awk -F" " '{print $1"-"$2"-"$3";"$0}' |sort >file1.tmp
$ cat file2.txt |awk -F" " '{print $1"-"$2"-"$3";"$0}' |sort >file2.tmp
$ join -t; -o 1.2 file1.tmp file2.tmp >file1.same.txt
$ join -t; -o 2.2 file1.tmp file2.tmp >file2.same.txt
$ rm -f file1.tmp file2.tmp
$ cat file1.same.txt
bar 2 scaf 3.3
foo 1 scaf 3
$ cat file2.same.txt
bar 2 scaf 1.00
foo 1 scaf 4.5
使用datamash
的折疊操作,再加上一些裝飾性sort
和tr
ing:
cat File* | datamash -t ' ' -s -g1,2,3 collapse 4 |
sort -g -k2 | tr ',' ' '
輸出(常用行有第 5 個字段,不常用行沒有):
foo 1 boo 2.3
foo 1 scaf 3 4.5
bar 2 scaf 3.3 1.00
如果這正是所需的輸出,OP 不會顯示預期的輸出,但這是解決問題的方法:
$ awk '
{ key=$1 FS $2 FS $3 }
NR==FNR { val[key]=$4; next }
key in val {print $0, val[key] }
' file1 file2
foo 1 scaf 4.5 3
bar 2 scaf 1.00 3.3
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.