LINUX.ORG.RU

Объединить 2 таблицы: как сделать FULL OUTER JOIN стандартными средствами

 , ,


0

1

Добрый день! Есть 2 таблицы.

c:\nncron>cat A.txt
Lubos	John	Linda	Rares	Rick
	A	B	C	E
4	1	2	3
c:\nncron>cat B.txt
Anna	Linda	Rares	John	Max
	F	G	E	I
4	2	3	1	
Таблицы не отсортированы. Нужно объединить их так, чтобы получить результирующую таблицу:
c:\nncron>cat C.txt
Lubos	John	Linda	Rares	Rick	Anna	Max
	A	B	C	E		
4	1	2	3			
	E	F	G			I
	1	2	3		4	
Насколько я понимаю, эта операция в терминологии баз данных называется FULL OUTER JOIN (полным объединением). Вот хотелось бы узнать, как его эффективно реализовать в bash стандартными утилитами unixutils - awk sed, sort, grep. Anonymous уже рекомендовал мне утилиту join, но попользовавшись ей, я сделал вывод, что у нее есть 2 существенных недостатка. Во первых, сравниваемые столбцы таблиц должны быть отсортированы по возрастанию плюс выполняется объединение строк, а не полей. То есть, для того, чтобы воспользоваться данной утилитой, эти таблицы нужно транспонировать, добавить недостающие TAB справа и отсортировать по сравниваемому полю, пусть это будет поле 1:
c:\nncron>gawk -F"\t" -v OFS="\t" "NF=5"
c:\nncron>gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%s\x09\",$3 }; END { print \"\" }" | cut -f 1-3 | sort -k1"
Получим:
c:\nncron>cat A_transp.txt
John	A	1
Linda	B	2
Lubos		4
Rares	C	3
Rick	E	
c:\nncron>cat B_transp.txt
Anna		4
John	E	1
Linda	F	2
Max	I	
Rares	G	3
В винде я столкнулся еще с одной промежуточной проблемой - невозможность использования TAB в качестве выходного разделителя (ключ \t), но это так, мелочи, опустим это.

Вторым существенным недостатком данной утилиты является то, что при использовании ключа -a2 непарные строки таблицы 2 в выводятся без отступов слева, то есть ячейки непарных строк таблицы 2 фактически находятся в полях таблицы 1, что усложняет дальнейшее построение объединенной таблицы. Кроме того, если в исходной таблице идут 2 символа разделителя подряд, например 2 TAB, то в объединенной таблице будет один TAB. Опция -e, замещающая входные пустые ячейки, работает некорректно, заменяются не все пустые ячейки. Поэтому пустые ячейки исходной таблицы приходится заменять с помощью sed. Допустим, я делаю это и использую join:

c:\nncron>sed -i "s/\t\(\t\|$\)/\tNULL0\1/g" A_transp.txt
John	A	1
Linda	B	2
Lubos	NULL0	4
Rares	C	3
Rick	E	NULL0
c:\nncron>sed -i "s/\t\(\t\|$\)/\tNULL0\1/g" B_transp.txt
Anna	NULL0	4
John	E	1
Linda	F	2
Max	I	NULL0
Rares	G	3
Вот такая невыровненная хрень получается:
c:\nncron>join -j 1 -a1 -a2 A_transp.txt B_transp.txt
Anna NULL0 4
John A 1 E 1
Linda B 2 F 2
Lubos NULL0 4
Max I NULL0
Rares C 3 G 3
Rick E NULL0
Или если удалить пустые ячейки и заменить пробелы TAB:
c:\nncron>join -j 1 -a1 -a2 A_transp.txt B_transp.txt | sed -e "s/ /\t/g;s/NULL0//g"
Anna		4
John	A	1	E	1
Linda	B	2	F	2
Lubos		4
Max	I	
Rares	C	3	G	3
Rick	E	
Сравните это с транспонированной матрицей C.txt (как должно быть в идеале):
c:\nncron>cat C.txt | gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%s\x09\",$3 }; END { print \"\" }" | cut -f 1-5
Lubos		4		
John	A	1	E	1
Linda	B	2	F	2
Rares	C	3	G	3
Rick	E			
Anna				4
Max			I	
Поскольку исходные таблицы были отсортированы, чтобы привести объединенную таблицу в начальный, не сортированный вид, строки транспонированных таблиц нужно было еще и пронумеровать до сортировки, чтобы потом их восстановить...

Вот собственно я хотел бы спросить форумчан, может быть кто из вас сталкивался с такой задачей и каким способом вы ее решали. Может быть, существует более легкий и просто способ. Желательно с помощью утилит unixutils, не прибегая к сторонним ЯП.


Вот такая невыровненная хрень получается:

Проблему с «невыравненностью» решил, использовал дополнительный ключ -o команды join и ключ -e для заполнения пустых ячеек:

c:\nncron>join -j 1 -a1 -a2 -o 0,1.2,1.3,2.2,2.3 -e NULL0 A.txt B.txt | sed -e "s/ /\t/g"
Anna	NULL0	NULL0	NULL0	4
John	A	1	E	1
Linda	B	2	F	2
Lubos	NULL0	4	NULL0	NULL0
Max	NULL0	NULL0	I	NULL0
Rares	C	3	G	3
Rick	E	NULL0	NULL0	NULL0

Осталось прикрутить нумерацию к входным таблицами и выполнить обратную сортировку, чтобы получившаяся таблица совпала с C_transp.txt. Пока думаю как это сделать.

И еще: не знаю как сгенерировать ключ -o для скрипта nncron, если допустим на стеке будут 2 значения ширины обеих таблиц, в данном случае W1 W2, тогда нужно еще сделать генерацию строки вида 0,1.2,1.3,...,1.W1,2.2,2.3,...,2.W2, геморрой еще тот. Может кто-нибудь знает более «элегантный» способ написания ключа -o для таблиц большой ширины. Пробовал так:

c:\nncron>join -j 1 -a1 -a2 -o 0,1.2-1.3,2.2-2.3 -e NULL0 A.txt B.txt | sed -e "s/ /\t/g" | clip
join: invalid field number: `2-1.3'

c:\nncron>join -j 1 -a1 -a2 -o 0,1.2-3,2-2.3 -e NULL0 A.txt B.txt | sed -e "s/ /\t/g" | clip
join: invalid field number: `2-3'
облом-с.

xseed
() автор топика
Ответ на: комментарий от xseed

[code]join -j 1 -a1 -a2 -o 0,1.2-3,2-2.3 -e NULL0 A.txt B.txt[/code]

join -j 1 -a1 -a2 -o 0,1.2-3,2.2-3 -e NULL0 A.txt B.txt
xseed
() автор топика
Ответ на: комментарий от mos

Может кому понадобится, crontab:

#( Join2Tables
SingleInstance
NoActive
Action:
    \ в комментариях приведены состояния стека таблиц A размером 3x5 и B размером 4xY
    \ sed ver. 4.0.7, sed_ ver. 4.2.1, gawk, join, cat, sed из unixutils, paste_.exe с c3scripts.com
    SWHide
    QUERY: "Обнулить общую таблицу?"
    IF S" B.txt" FDELETE THEN
    START-APPW: "paste_.exe | sed -e "${/^$/d};s/[^\t]//g" | sed_ -z "$s/\n$//" | gawk "{print length ($0)}" | sort -nr | sed -e "$=;1!d" | sed_ -z "s/\r/\x0D/" > tab_row_counts.txt"
    READ-BY-LINE: tab_row_counts.txt
        FOUND-LINE S>NUM 1+
    ;READ-BY-LINE \ 4 5
    1- 2DUP 2DUP DROP \ 5 3 5 3 5
    \ 5 3 5 прибавляем недостающие табы справа, транспонируем и нумеруем общую таблицу
    START-APPW: "paste_ | sed -e "${/^$/d}" | gawk -F"\t" -v OFS="\t" "NF=%0 esPICK%" | gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%PERCENT%s\x09\",$3 }; END { print \"\" }" | cut -f 1-%1 esPICK% | cat -n - B.txt | sed -e "s/\t$/\t987879798897/;s/\t\t/\t987879798897\t/g;s/\t\t/\t987879798897\t/g;s/\(^ *\)\|\x0D//;s/ /\x1B/g" > out.txt"
    \ 3 5 Фильтруем строки с номерами и сортируем их, создаем таблицу A для объединения
    START-APPW: "sed -e "%0 esPICK%q" out.txt | sort -k2 > A.txt"
    \ создаем значение ключа -o для join
    S" 1.1,2.1,0" \ c a 3 5
    \ Кладем на стек число столбцов таблицы B
    START-APPW: "sed -e "${/^$/d};s/[^\t]//g" B.txt | sed_ -z "$s/\n$//" | gawk "{print length ($0)}" | sort -nr | sed -e "1q" > tab_row_counts.txt"
    S" tab_row_counts.txt" FILE S>NUM 1+ \ 4 c a 3 5 | 1 c a 3 5
    \ Если файл пустой, на стеке 1 - выбрасываем, нет - дополняем значение ключа
    DUP 1 > IF \ 6 c a 3 5
        2+ 3 DO S" ,1." S+ I N>S S+ LOOP \ c a 3 5
    ELSE DROP THEN \ c a 3 5
    \ дополняем значение ключа
    ROT 2+ 3 DO \ 5 c a 5
        S" ,2." S+ I N>S S+ LOOP ROT \ 5 c a
    \ Выделяем строки таблицы B, сортируем их и делаем FULL OUTER JOIN с таблицей A, возвращаем пробелы, удаляем номера, результат помещаем в таблицу B:
    START-APPW: "sed -e "1,%0 esPICK%d" out.txt | sort -k2 | join -j 2 -a1 -a2 -o %2 esPICKS% -e 987879798897 - A.txt | sort -nk1,1 -k2,2 | sed -e "s/ /\t/g;s/987879798897//g;s/\x1B/ /g" | cut -f 3- > B.txt"
    \ транспонируем, удаляем TAB в конце строки, результат помещаем в join.txt и буфер
    START-APPW: "gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" B.txt | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%PERCENT%s\x09\",$3 }; END { print \"\" }" | sed "s/\t$//" > join.txt && clip < join.txt"
    BALLOON: "Join2Tables" "Готово!"
)#
Исходный порядок строк навел с помощью нумерации обеих таблиц и численной сортировки по двум столбцам.

Может есть какая-нибудь некостыльная версия join? SQL пока не предлагать, молод я еще.

xseed
() автор топика
Ответ на: комментарий от xseed

есть такой инструмент, miller (mlr, http://johnkerl.org/miller/doc/10-min.html) вот им очень удобно можно такое решить. только я вот так с наскоку full outer join его не смог заставить сделать, только вот что-то такое:

$ mlr --tsv --rs lf join -j "" -f a.txt b.txt
Lubos   John    Linda   Rares   Rick    Anna    Max
        E       F       G       E               I
4       E       F       G                       I
        1       2       3       E       4
4       1       2       3               4


советую связаться с автором, он активно его пилит, может что-то посоветует или реализует такую фичу. даже если не тебе, то наверняка в будущем кому-то пригодится.

val-amart ★★★★★
()
Ответ на: комментарий от val-amart

Немного подредактировал скрипт, теперь может работать с таблицами любых размеров:

#( Join2Tables
SingleInstance
NoActive
Action:
    SWHide
    QUERY: "Стереть общую таблицу?"
    IF S" B.txt" FCREATE THEN
    START-APPW: "paste_.exe | sed_ -e "${/^$/d};s/[^\t]//g" | gawk -F\t "END { print NR\"\n\"NF }" | sed_ -e "$s/^0/1/" > tab_row_counts.txt"
    \ 1st line: Columns A (CA), 2nd line: Rows A (RA). CA-1 CA RA:
    READ-BY-LINE: "tab_row_counts.txt" FOUND-LINE S>NUM ;READ-BY-LINE SWAP DUP 1-
    \ Число столбцов и строк таблицы B на стек: RA CA RB CB CB CA-1:
    1 READ-BY-LINE: "B.txt" FOUND-LINE S" /\t/" RE-ALL 1+ ;RE-ALL RBL-EXIT ;READ-BY-LINE DUP 0 READ-BY-LINE: "B.txt" 1+ ;READ-BY-LINE 2ROT SWAP
    \ прибавляем недостающие табы справа, транспонируем, нумеруем общую таблицу, добавляем метки пробелов, номеров строк, пустых полей:
    START-APPW: "paste_ | gawk -F"\t" -v OFS="\t" "NF=%0 esPICK%" | gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%PERCENT%s\x09\",$3 }; END { print \"\" }" | cut -f 1-%1 esPICK% | sed -e "s/$/\t/;s/\t/&NULL4/" | cat -n B.txt - | sed -e "s/^ *//;s/ /NULL1/g;s/\t$/&NULL2/;:a;s/\t\t/\tNULL2\t/g;ta;s/^\([^\t]*\)\t\([^\t]*\t\)/\2\1NULL3/" > out.txt"
    \ Сортируем строки с номерами, создаем таблицу A для объединения: RB RB CB CB CA-1
    DUP FILE-EMPTY: "B.txt"
        IF
            DROP 2DROP + 1+ \ Если общая таблица была стерта, копируем в нее таблицу А, добавляем недостающие TAB: CB+CA
            START-APPW: "sed -e "s/[0-9]\+NULL3\|NULL2//g;s/NULL1/ /g;s/NULL4//" out.txt | gawk -F"\t" -v OFS="\t" "NF=%0 esPICK%" > B.txt"
        ELSE
            START-APPW: "sed -e "1,%0 esPICK%d" out.txt | sort -k1 > A.txt"
           \ Выделяем строки таблицы B, сортируем их и делаем FULL OUTER JOIN с таблицей A, возвращаем пробелы, удаляем номера, результат помещаем в таблицу B: RB CB CB+CA-1
            2SWAP + ROT ROT START-APPW: "sed -e "%0 esPICK%q" out.txt | sort -k1 | join -j 1 -a1 -a2 -e NULL2 - A.txt | sed -e "s/ /\t/g" | sort -nk2 | sed -e "s/NULL1/ /g;s/[0-9]\+NULL3//g;:a;/^\([^t]\+\t\)\{%1 esPICK%\}NULL4/!s/NULL4/NULL2\t&/;ta;s/NULL2\|\tNULL4//g" | gawk -F"\t" -v OFS="\t" "NF=%2 esPICK%" > B.txt"
        THEN
    \ транспонируем, удаляем последнюю пустую строку с TAB, результат помещаем в join.txt и буфер:
    START-APPW: "gawk -F\x09 "{OFS = FS}{ for (i=1; i<=NF; i++) print i,NR,$i}" B.txt | sort -nk1,1 -k2,2 | gawk -F\x09 " NR>1 && $2==1 { print \"\" }; { printf \"%PERCENT%s\x09\",$3 }; END { print \"\" }" | sed "s/\t$//;$d" > join.txt && clip < join.txt"
    BALLOON: "Join2Tables" "Готово!"
)#

xseed
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.