2018년 10월 12일 금요일

Awk를 이용한 간단한 텍스트 파일 조작

Awk는 개발자 세 사람 - Alfred Aho, Peter Weinberger 및 Brian Kerninghan - 이름의 머리글자를 따서 명명된 프로그래밍 언어이다. 이들은 전부 1940년대 초반에 출생하신 원로이다. 아마추어 무선을 하는 사람을 일컫는 ham (ham radio)역시 무선통신의 역사를 세운 세 사람의 이름인 Hertz, Armstrong, Marconi에서 왔다는 설도 있으나 이는 근거가 없다고 한다(Etymology of ham radio). 여담이지만 우리나라에서는 radio라 하면 공중파 방송을 듣는 라디오 수신기만을 생각하지만, 미국에서는 무선을 이용한 모든 활동을 뜻하는 것 같다. 무전기(휴대용 및 거치형 전부)를 two-way radio라고도 부른다고 하니 말이다.

우선 첫번째 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 명령어를 작성하면 된다.

$ awk 'NR==FNR{a[$3,$4,$5]=$1OFS$2;next}{$6=a[$1,$2,$3];print}' OFS='\t' file_b.bed file_a.bed

먼저 awk의 내장 변수에 대해서 이해를 넓힐 필요가 있다. NR는 현재 처리 중인 라인 번호를 의미한다. Awk가 여러 파일을 처리하는 경우 NR은 전체 파일에 대한 라인 번호를, FNR은 각 입력 파일에 대한 라인 번호를 의미한다. 다음을 실행해보면 감이 잡힐 것이다.

$ 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

NR==FNR{}은 첫번째 인수로 주어진 파일(file_b.bed)에 대해서 {} 내의 명령을 실행하라는 뜻이다. 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에서 다음을 잘 구별하는 노력이 필요하다.
  • 조건{...} {...} <= 조심해서 써야 한다!
  • 조건1{...} 조건2{...}
  • if문을 사용하는 것

댓글 없음: