원문:http://www.linuxnewbie.org/nhf/intel/programming/text_processing_pipes.html

저자: Adrian J. Chung

날짜: ?

제목:텍스트 프로세싱 파이프 라인으로 있는 일들
 

명령 라인이 사용하기에 불편한 것은 사실이지만, 일단 명령 라인 사용법을 마스터하면 그래픽 유저 인터페이스 하에서는 알지 못했던 Unix의 강력한 힘을 얻게 된다. 이 글은 일반적인 GNU 유틸리티들을 이용해서 몇 가지의 상당히 해볼 만한 작업을 수행해내는 텍스트 프로세싱 파이프 라인을 구성하는 법에 관한 자세한 내용을 다룬다.

많이 사용되는 단어 순위 알아내기

가정하기를, 어떤 사람이 이 글처럼 인터넷에서 다운로드한 텍스트 파일에서 단어의 쓰임새가 어떤지에 관심이 있다고 하자. 아마도 그 사람은 소스 코드에서의 변수 이름이나 쓰레기 파일의 임의의 비트들 같은 모든 비단어를 무시했을 때 어떤 단어가 가장 자주 사용되는 지가 궁금할 것이다. 자주 사용되는 단어의 순위가 필요할 지도 모른다. 특정 단어를 카운트하는 프로그램을 Perl로 작성해야 될 것인가? 여기 몇 가지의 GNU 텍스트 유틸리티와 방대한 자료인 /usr/dict/words의 도움만으로 이 목적을 달성할 수 있는 방법이 소개된다.

우선 우리는 텍스트 파일 안에 있는 문장을 나눠서 한 라인에 한 개의 단어만이 있게 하는 것으로 시작한다. 이 때에는 "tr" 도구가 유용하다. 이 도구는 파일들을 한번에 하나의 문자로 번역한다. 여기에 "tr"을 이용하는 핵심적인 USENET 도구 rot13를 예로 들어보자:

% tr a-zA-Z n-za-mN-ZA-M < rot13-encrypted.txt

두 개의 인자는 사용하고자 하는 문자 번역 테이블을 명시하고 있다:

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM

처음 라인에 있는 문자들은 그에 해당하는 두 번째 라인에 있는 문자로 바뀌어진다. 일치하지 않는 문자들은 영향 받지 않는다. "tr"은 이와는 반대로 행동하게도 해 준다. 즉, 오직 일치하지 않는 문자들만 바꿀 수도 있다:

% tr -c a-zA-Z '\n' < article.txt

문자가 아닌 모든 것은 개행 문자로 바뀐다. 이렇게 하면 한 줄에 한 단어씩 오게 하는 첫번째 단계가 완성된다.

다음엔 유사한 단어를 함께 묶을 필요가 있다. 파일의 라인들을 정렬하는 정도면 충분하다. "sort" 명령 역시 필터로 쓸 수 있으며, "-f" 옵션을 사용하면 대소문자를 구분하지 않게 된다. 그래서 우리가 사용하게 될 파이프 라인은 다음과 같다:

% tr -c a-zA-Z '\n' < article.txt | sort -f

여러분이 지금 읽고 있는 이 글과 같은 텍스트의 본문에는 많은 비단어들(예를 들면, "za","tr", "txt")이 포함되어 있다. 우리는 /usr/dict/words 같은 유효한 단어들의 리스트를 참조함으로써 이런 것들을 제거할 수 있다. "join" 명령은 데이터베이스 용어로 알려진 것처럼 자연스러운 결합을 수행한다. 이 명령은 두 개의 텍스트 스트림을 읽어 들인다. 이 때 두 개의 텍스트는 키 필드에 의해 미리 정렬되어 있어야만 하고 양쪽 스트림으로부터 들어오는 라인들을 일치시키는 데에 키 필드가 사용된다. /usr/dict/words는 이미 저장되어 있으므로 이것을 사용하는 것이 이상적이다. 다른 텍스트 스트림 안에 일치하는 짝이 없는 행의 데이터의 경우에는 출력이 없는 것이 자연스러운 결합의 유용한 점이다. 그렇기 때문에 우리의 사전 안에 있지 않은 모든 단어들은 제거된다:

% tr -c a-zA-Z '\n' < article.txt | sort -f | join -i /usr/dict/words -

"-"는 "join"에게 두 번째 텍스트 스트림을 표준 입력으로부터 받아들이라고 알려준다(즉, 파이프 라인의 앞선 단계에서 나오는 출력). "-i"는 join이 대소문자를 구별하지 않도록 한다.

다음 단계는 그룹 지어진 단어를 세는 것인데 이 작업을 위해서 우리는 "uniq" 유틸리티를 쓸 수 있다. "uniq"는 연속적으로 위치한 반복되는 라인들을 제거한다. "-c" 옵션을 사용하면 "uniq"는 개 개의 유일한 라인에 대해서 이와 비슷한 라인의 수를 출력한다. 앞선 경우와 마찬가지로 "-i"는 대소문자를 구별하지 않게 한다:

% tr -c a-zA-Z '\n' < article.txt | sort -f | join -i /usr/dict/words - | uniq -i -c

마지막으로, 이 출력 결과는 "sort" 명령을 이용해서 정렬할 필요가 있다:

% tr -c a-zA-Z '\n' < article.txt | sort -f | join -i /usr/dict/words - | uniq -i -c | sort -r

일반적으로 ASCII 문자열 보다 수치 값에 의해 정렬을 할 때는 "-n" 옵션이 주어져야 한다. 여기서는 "uniq" 명령이 개별 단어에 대한 총 개수를 계산했기 때문에 사용하지 않았다. 가장 빈번히 사용되는 단어가 맨 처음에 나오게 하기 위해서 정렬 순서를 거꾸로 하는 "-r"을 사용했다.

이 방법에는 축약어를 다루는 경우(예를 들면, "we'll", "it's", "can't")와 같은 몇 가지 단점이 있기는 하지만 이런 단점은 단어를 카운트하기 위해 특별히 작성된 프로그램에서도 예상되는 것이다.

순서대로 정렬되지 않은 파일들을 결합하기

어떤 경우에는 두 개의 파일이 개별적으로 가지고 있는 정보를 하나의 파일로 합쳐야 할 필요가 있을 때도 있다. 이 때, 두 파일을 결합해서 얻은 정보가 합치기 이전의 정보와 같아야 한다. 예를 들어 다음의 두 개의 파일을 가지고 있다고 하자:

alpha.txt

tetex-xdvi 1.0.6-11
ElectricFence 2.1-3
newt-devel 0.50.8-2
rgrep 0.98.7-5
dosfstools 2.2-4
bdflush 1.5-11
bin86 0.4-7
gnuplot 3.7.1-3
dialog 0.6-16
kernel-utils 2.2.14-5.0
termcap 10.2.7-9

beta.txt

ElectricFence 36903
bdflush 8861
bin86 74968
dialog 85955
dosfstools 106819
gnuplot 1345702
kernel-utils 292693
newt-devel 144815
rgrep 15202
termcap 625272
tetex-xdvi 1222425

자연스럽게 "join" 명령을 사용할 것을 생각하게 된다. 하지만 alapha.txt 파일에서 정렬된 것과 같은 순서를 지킬 필요가 있다고 가정하자. 키 필드가 저장되지 않으면 "join" 명령으로는 그렇게 할 수 없다. 원래 파일의 순서를 저장하도록 도울 수 있는 별도의 인덱싱 필드를 첫번째 파일에 더해야 할 필요가 생긴다. 이런 때에 "nl" 명령을 유용하게 써 먹을 수 있다:

% nl alpha.txt

1 tetex-xdvi 1.0.6-11
2 ElectricFence 2.1-3
3 newt-devel 0.50.8-2
4 rgrep 0.98.7-5
5 dosfstools 2.2-4
6 bdflush 1.5-11
7 bin86 0.4-7
8 gnuplot 3.7.1-3
9 dialog 0.6-16
10 kernel-utils 2.2.14-5.0
11 termcap 10.2.7-9

이제 우리는 키 필드에 의해 정렬을 해서 join을 수행한다. 그런 다음에 원래의 순서를 나타내는 인덱스 필드를 가지고 다시 정렬한다:

% nl alpha.txt |sort +1 |join -j2 2 beta.txt -

"-j2 2" 인자는 "join"이 두 번째 필드를 두 번째 입력 스트림의 키 필드로 사용하도록 알려준다(beta.txt는 첫번째 입력 스트림이다). 출력 결과는 약간은 복잡하겠지만 정렬하는 데 사용된 인덱스가 세 번째 필드란 것을 볼 수 있다. "sort +2"는 행을 비교할 때 처음의 두 개 필드를 건너 뛰게 한다:

% nl alpha.txt |sort +1 |join -j2 2 beta.txt - |sort +2 -n

이제 "cut" 명령을 이용해서 순서를 나타내는 필드를 제거한다:

% nl alpha.txt |sort +1 |join -j2 2 beta.txt -| sort +2 -n | cut -f 1,2,4 -d " "

"-f" 인자는 출력에 포함되는 필드의 리스트를 넘겨준다. "cut"은 일반적으로 필드 구분자로 [TAB]을 사용하지만 "-d"를 사용해서 변경할 수 있다. 또 출력 결과가 예쁘게 나오려면 출력 형식 지정이 필요하다. 여기서는 "pr" 도구를 사용했다:

% nl alpha.txt |sort +1 |join -j2 2 beta.txt -| sort +2 -n | cut -f 1,2,4 -d " " |pr -e\ 16 -T

tetex-xdvi 1222425 1.0.6-11
ElectricFence 36903 2.1-3
newt-devel 144815 0.50.8-2
rgrep 15202 0.98.7-5
dosfstools 106819 2.2-4
bdflush 8861 1.5-11
bin86 74968 0.4-7
gnuplot 1345702 3.7.1-3
dialog 85955 0.6-16
kernel-utils 292693 2.2.14-5.0
termcap 625272 10.2.7-9

"pr"은 대개 라인 프린터의 출력 형식을 지정한다. 이제는 이런 오래된 하드웨어를 사용하는 사람이 거의 없지만 "pr" 명령은 여전히 사용되고 있다. "-e"는 TAB 문자를 스페이스로 대체한다. 우리가 위에서 사용한 "-e" 인자는 "pr"이 TAB 문자로 하나의 스페이스를 사용하고 탭 위치를 16 문자 너비로 간격을 두게 한다. "-T"는 머리말, 꼬리말, 폼 피드를 출력하지 않도록 한다.

결론

GNU 텍스트 프로세싱 유틸리티가 제공하는 기능들은 명령 라인을 이용해서 여러 가지의 각기 다른 많은 방식으로 결합하여 이들 유틸리티가 아니었으면 특별한 프로그램이 작성되어야만 했을 작업들을 수행해낸다.  명령 라인 인터페이스를 사용하는 방법을 익히는 데에 적은 시간을 투자함으로써 결국에는 많은 문제점을 해결할 수 있다.