우선 첫번째 AWK 사례를 설명해 보자. 왼쪽과 같이
이때 awk의 associative array(예제 링크)가 쓰이게 된다. Associative array는 숫자가 아닌 것도 인덱스로 사용할 수 있는 배열을 의미한다. Perl에서 흔히 사용하는 hash와 매우 유사하다. 위에서 기술한 매우 간단한 과업을 수행하려면 다음의 명령어 한 줄이면 된다.
$ awk -F, '{a[$1] = a[$1] FS $2} END{for (i in a) print i a[i]}' file-F,는 입력 파일의 field separator를 쉼표로 지정한다는 뜻이다. {}으로 둘러싼 AWK 명령어 내부에서는 FS가 같은 의미로 쓰였다.
다음으로는 두 텍스트 파일을 공통된 컬럼 값으로 묶는 약간 복잡한 응용 사례이다. Using Awk to join two files based on several columns를 참조하였다. 여러 컬럼으로 이루어진 두 개의 텍스트 파일 file_a.bed와 file_b.bed가 있다고 가정하자. 첫번째 파일의 컬럼 1-2와 두번째 파일의 컬럼 3-4가 동등한 종류의 내용을 담고 있다. 빨갛게 표시한 것은 값 자체가 같은 것이다. 컬럼을 구분하는 것은 공백이 아니고 탭문자이다.
$ cat file_a.bed
chr1 123 aa b c d
chr1 234 a b c d
chr1 345 aa b c d
chr1 456 a b c d
$ cat file_b.bed
xxxx abcd chr1 123 aa c d e
yyyy defg chr1 345 aa e f g
chr1 123 aa b c xxxx abcd
chr1 234 a b c
chr1 345 aa b c yyyy defg
chr1 456 a b c
다음과 같이 awk 명령어를 작성하면 된다(명령어 1).
$ awk 'NR==FNR{a[$3,$4,$5]=$1OFS$2;next}{$6=a[$1,$2,$3];print}' OFS='\t' file_b.bed file_a.bed
읽어 들인 라인에 대해서 associative array가 어떻게 작동하는지는 이 글의 뒷부분에 [Awk associative array의 활용 예제]를 통해서 설명하였다. 먼저 awk의 내장 변수에 대해서 이해를 넓힐 필요가 있다. NR는 현재 처리 중인 라인 번호를 의미한다. Awk가 여러 파일을 처리하는 경우 NR은 전체 파일에 대한 라인 번호를, FNR은 각 입력 파일에 대한 라인 번호를 의미한다. 다음을 실행해보면 감이 잡힐 것이다. 인수로 주어진 파일은 두 개이지만, awk는 라인 단위로 순차적인 처리를 하게 되므로 첫 번째 파일(file_a.bed)에 대해서 작업을 먼저 한 뒤, 이어서 두 번째 파일(file_b.bed)를 가지고서 일을 하게 된다.
$ awk '{print FILENAME, NR, FNR}' file_a.bed file_b.bed
file_a.bed 1 1
file_a.bed 2 2
file_a.bed 3 3
file_a.bed 4 4
file_b.bed 5 1
file_b.bed 6 2
명령어 1을 실행할 때 언제 NR==FNR{}의 조건을 충족하게 되는가? 첫 번째 파일에 대한 작업을 할 때이다. 즉, 첫번째 인수로 주어진 파일(file_a.bed)에 대해서 {} 내의 명령을 실행하라는 뜻이다. next는 그 다음에 따라오는 {} 명령어 블록을 실행하지 말라는 뜻이다. 이것은 대단히 중요하지만 약간 어려운 개념이다. 그림으로 설명을 해 보겠다. 부디 내가 이해한 것이 옳기를 바란다. 별도로 작성한 글 awk와 sed를 사용하여 multi-fasta 파일의 서열 ID를 일괄적으로 바꾸기에서도 next문의 중요성을 설명하였다.
numbers라는 파일은 각 라인에 1..10까지의 숫자를 수록하고 있다. 각 라인의 수와 홀수 또는 짝수줄 여부를 화면으로 프린트하려면 어떻게 하는지를 다음의 그림에 설명하였다. next가 왜 중요한지를 잘 보여준다.
그러면 다시 원래의 awk 문으로 돌아가자. 두번째 인수로 주어진 file_a.bed에 대해서는 NR==FNR이 FALSE가 된다. 따라서 두번째 {} 내부의 명령어를 수행하게 된다. 즉 file_a.bed를 읽어서 마지막 필드($6)를 associative array의 값으로 바꾸어서 프린트하게 된다.
어쩌면 if..else를 쓰는 것이 좀 더 명확할지도 모르겠다.
$ awk '{if(NR==FNR){a[$3,$4,$5]=$1OFS$2}else{$6=a[$1,$2,$3];print}}' OFS='\t' file_b.bed file_a.bed
첫번째 주어진 답을 다르게 표현하면 이렇게 된다. 오히려 이것도 이해하기 쉬운 해결책이다.
$ awk 'NR==FNR{a[$3,$4,$5]=$1OFS$2}NR!=FNR{$6=a[$1,$2,$3];print}' OFS='\t' file_b.bed file_a.bed
awk에서 다음을 잘 구별하는 노력이 필요하다.
- 조건{...} {...} <= 조심해서 써야 한다!
- 조건{...;next}{...} <= 이 사례는 의외로 유용하다. 어떤 조건을 만족하는 라인은 조작을 가해서 출력하고, 조건을 충족시키지 않는 것을 그대로 출력하는데 좋다. FASTA file의 서열 ID만 바꾸는 경우가 이에 해당한다. sed로도 이렇게 할 수 있을까>?
- 조건1{...} 조건2{...}
- if문을 사용하는 것
[업데이트] Awk associative array의 활용 예제
이 글을 추가로 써 넣으면서 걱정이 앞서기 시작하였다. awk는 그렇게 고수준의 언어가 아니라서 복잡한 일을 시키기는 곤란하다. 아래에서 든 예제 및 결과를 나중에 다시 찾아보았을때 어떻게 동작하는지를 이해하지 못하게 될까봐 고민이다. 자주 읽어보고 실습을 해 보는 것이 해결책이다.
두 개의 파일(file_a, file_b)를 갖고 있다고 가정하자. 첫번째 파일 file_a는 자료를 담고 있고, 이 중에서 첫번째 컬럼에 특정 값이 있는 경우 그 라인만을 뽑아내고 싶다. 해당되는 값은 두번째 파일 file_b의 첫번째 컬럼(key)에 담겨 있다. 만약 file_b가 하나의 컬럼만으로 구성되어 있다면 shell 환경에서 grep -f file_b file_a라고 입력하면 된다. 그러나 파일이 길어지면 속도가 느려질 것이 뻔하고(file_b에 수록된 패턴 한 줄에 대해서 file_a 전체를 훑는 일을 계속 반복해야 하므로), file_a의 해당되는 라인을 출력하는 것 이외의 일은 하지 못할 것이다.
두 개의 파일(file_a, file_b)를 갖고 있다고 가정하자. 첫번째 파일 file_a는 자료를 담고 있고, 이 중에서 첫번째 컬럼에 특정 값이 있는 경우 그 라인만을 뽑아내고 싶다. 해당되는 값은 두번째 파일 file_b의 첫번째 컬럼(key)에 담겨 있다. 만약 file_b가 하나의 컬럼만으로 구성되어 있다면 shell 환경에서 grep -f file_b file_a라고 입력하면 된다. 그러나 파일이 길어지면 속도가 느려질 것이 뻔하고(file_b에 수록된 패턴 한 줄에 대해서 file_a 전체를 훑는 일을 계속 반복해야 하므로), file_a의 해당되는 라인을 출력하는 것 이외의 일은 하지 못할 것이다.
$ cat file_a 1 A 2 B 3 C 4 D 5 E 6 F 7 G 8 H 9 I 10 J $ cat file_b 7 seven 3 three 6 six 2 two
첫번째 파일을 그대로 출력하되 두번째 파일에서 정의된 라인 뒤에 특별한 표시를 하는 것이 작업 목표이다. 표시할 값은 두번째 파일의 두번째 컬럼(value)에서 나열한 값이다. 실생활에서 쉽게 접할 수 있는 사례를 들자면 한 파일에는 학번과 과목별 점수가, 다른 파일에는 학번과 이름이 적혀 있는 상황에서 학번, 이름, 점수를 표시하는 일과 같은 것을 생각하면 된다. 두 파일에서 동일한 성격의 값이 수록된 필드, 즉 첫번째 컬럼이 동일한지를 살피는 것이 핵심이다. 만약 두번째 파일에 엉뚱하게 '0 zero'가 있다면 어떻게 할 것인가? 이것까지는 해결하지 못한다.
아니면 지정된 라인만 출력하고 싶다. 이건 조금 더 까다롭다. 왜냐하면 if(..){..} 구문이 들어가야 하기 때문이다.
지정된 라인만 출력하되 약간의 장식을 하려면 두번째의 array가 필요해진다. b[$1]=1이라 선언하고 뒤의 if문에서 b[$1]==1로 하는 것이 좀 더 간단하다. Perl이라면 hash를 이용하면 되고, if exists $seen{$key} 구문을 이용하여 주어진 key를 갖는 hash가 존재하는지를 점검할 수 있다. 그러나 awk에서는 그렇게까지 하지 못하기 때문에 두번째의 associative array인 b[]를 도입하게 된 것이다.
$ awk -F '\t' 'NR==FNR{a[$1]=$2;next}{$3=a[$1];print}' OFS='\t' file_b file_a 1 A 2 B two 3 C three 4 D 5 E 6 F six 7 G seven 8 H 9 I 10 J
아니면 지정된 라인만 출력하고 싶다. 이건 조금 더 까다롭다. 왜냐하면 if(..){..} 구문이 들어가야 하기 때문이다.
$ awk -F '\t' 'NR==FNR{a[$1]=$1;next}{if($1==a[$1]){print}}' OFS='\t' file_b file_a 2 B 3 C 6 F 7 G
지정된 라인만 출력하되 약간의 장식을 하려면 두번째의 array가 필요해진다. b[$1]=1이라 선언하고 뒤의 if문에서 b[$1]==1로 하는 것이 좀 더 간단하다. Perl이라면 hash를 이용하면 되고, if exists $seen{$key} 구문을 이용하여 주어진 key를 갖는 hash가 존재하는지를 점검할 수 있다. 그러나 awk에서는 그렇게까지 하지 못하기 때문에 두번째의 associative array인 b[]를 도입하게 된 것이다.
$ awk -F '\t' 'NR==FNR{a[$1]=$2;b[$1]=$1;next}{if(b[$1]==$1){$3="FOUND!";print}}' OFS='\t' file_b file_a 2 B FOUND! 3 C FOUND! 6 F FOUND! 7 G FOUND!
$ awk -F '\t' 'NR==FNR{a[$1]=$2;b[$1]=$1;next}{if(b[$1]==$1){$3=a[$1];print}}' OFS='\t' file_b file_a 2 B two 3 C three 6 F six 7 G seven
위의 사례에서는 file_a에서 원하는 라인을 그대로 출력해 놓고 라인의 끝에 추가적인 컬럼을 덧붙이는 형식이었다. 군더더기 없이 file_a를 그대로 출력하되 숫자 자리(첫번째 컬럼)를 아예 치환하고 싶다면?
위 예문을 기초로 하여 여러 가지 응용과 변형을 가해 보는 것이 좋을 것이다.
$ awk -F '\t' 'NR==FNR{a[$1]=$2;b[$1]=$1;next}{if(b[$1]==$1){$1=a[$1];print}}' OFS='\t' file_b file_a two B three C six F seven G
위 예문을 기초로 하여 여러 가지 응용과 변형을 가해 보는 것이 좋을 것이다.
댓글 없음:
댓글 쓰기