Modernizing AWK, a 45-year old language, by adding CSV support

Ben Hoyt

CSV 지원을 추가해 45년 된 언어 AWK 현대화하기

원문은 Ben Hoyt님이 에 게재했습니다. 이 블로그 구독하기

최근 저는 POSIX 호환 AWK 인터프리터인 GoAWK에 CSV 파일을 제대로 처리하는 기능을 추가했습니다. 데이터가 넘쳐나는 세상에서 이 기능은 AWK를 개발자와 데이터 분석가에게 훨씬 더 유용하게 만들어 줄 것이라고 생각합니다.

스프레드시트용 입력을 만들든, 공개 데이터 소스의 데이터를 분석하든, “빅데이터”를 처리하는 스크립트를 작성하든, 오늘날 CSVTSV 파일은 어디에나 존재합니다. 그것이 최선의 형식인지는 논쟁의 여지가 있지만, 누군가 말했듯이 데이터 형식에는 두 종류뿐입니다. 사람들이 불평하는 형식과 아무도 사용하지 않는 형식입니다.

2018년 GoAWK를 공개한 직후 Hacker News에 달린 댓글을 보고부터 GoAWK에 CSV 지원을 추가하는 것을 고민해 왔습니다:

하. csv 파일을 기본적으로 이해하는 도구에서 awk 프로그래밍 언어를 사용할 수 있다면 얼마나 유용할까 생각하고 있었는데. 갑자기 그게 훨씬 더 현실적으로 느껴지네요!

당시 저는 이렇게 얼버무리는 답변을 달았습니다:

흥미로운 지적이네요. AWK가 CSV 파일의 따옴표를 제대로 파싱하는 모드를 가져야 한다고 생각해 왔습니다. 아마 -csv 옵션을 추가할지도 모르겠네요(아니면 FS가 ‘,’일 때 자동으로 그렇게 하도록 할 수도 있지만 — 그러면 하위 호환성이 깨지겠죠).

자, 이제 그런 모드가 생겼습니다! 커맨드라인 옵션은 결국 -i csv(“input CSV”)가 되었지만, 거의 비슷하죠.

이 기능을 후원해 준 앤트워프대학교 도서관에 큰 감사를 전합니다. 제가 아는 한 GoAWK를 사용하는 두 개의 주요 팀 또는 프로젝트 중 하나이며, 다른 하나는 Benthos 스트림 프로세서입니다.

그런데 왜 필요할까?

애초에 왜 이런 기능이 필요할까요? 안타깝게도 표준 AWK에는 따옴표로 묶인 필드가 있는 CSV 파일을 처리할 방법이 없는데, 실제 CSV 파일을 처리할 때는 이 기능이 매우 중요합니다.

필드 구분자를 쉼표(-F, 또는 FS=",")로 설정할 수는 있지만, 이는 완전한 편법에 불과하며 따옴표로 묶인 필드를 만나자마자 깨져 버립니다. 예를 들면 다음과 같습니다:

$ cat quoted.csv
"Smith, Bob",42
$ awk -F, '{ print $1 }' quoted.csv
"Smith    # you want to print the first field: Smith, Bob

현재 AWK로 CSV를 처리하기 위한 몇 가지 우회 방법이 있는데, Gawk의 FPAT 기능, Gawk용 다양한 CSV 확장, 혹은 Adam Gordon Bell의 csvquote 도구 등이 있습니다. csvquote는 awk를 실행하기 전에 입력을 변환하고, 이후에 csvquote -u를 실행해 변환을 되돌리는 방식으로 사용합니다.

Eli Rosenthal이 만든 훌륭한 도구인 frawk도 있는데, CSV를 기본적으로 지원하지만 아쉽게도 POSIX 표준 AWK와는 다소 차이가 있습니다. (frawk는 -i-o 커맨드라인 옵션을 포함해 GoAWK의 CSV 지원 일부에 영감을 주었습니다.)

그래서 POSIX 호환 AWK에서 제대로 된 CSV 지원에 대한 실질적인 수요가 있다고 생각하며, GoAWK가 바로 그 기능을 제공합니다.

본격적으로 살펴보기 전에, GoAWK의 CSV 입력 모드를 사용해 위 예제의 따옴표 문제를 바로잡아 보겠습니다:

$ goawk -i csv '{ print $1 }' quoted.csv
Smith, Bob    # that's better!

기능

CSV 입력 모드. -i csv 옵션은 GoAWK에 CSV 입력 모드를 사용하라고 지시합니다. 즉, 표준 필드 및 레코드 구분자(FSRS)를 무시하고 대신 CSV 파싱을 사용하라는 뜻입니다. TSV 모드도 있으며, 사용자 정의 구분 문자를 사용할 수도 있습니다.

또한 -H 옵션을 사용하면 GoAWK는 입력의 첫 번째 행을 필드 이름으로 사용하고, 번호 대신 이름으로 필드를 가져올 수 있는 유용한 @"named-field" 구문을 추가합니다.

예를 들어 내용이 다음과 같은 states.csv 파일이 있다고 해 보겠습니다:

"State","Abbreviation"
"Alabama","AL"
"Alaska","AK"
"Arizona","AZ"
...

다음 스크립트를 사용해 주의 약어만 출력할 수 있습니다:

$ goawk -i csv -H '{ print @"Abbreviation" }' states.csv
AL
AK
AZ
...

또는 이름에 “New”가 들어간 주의 개수를 세려면 다음과 같이 할 수 있습니다:

$ goawk -i csv -H '@"State" ~ /New/ { n++ } END { print n }' states.csv
4

~는 표준 AWK의 정규식 일치 연산자이므로, 이 코드는 State 필드가 정규식 New와 일치하는 레코드에 대해 n을 증가시키고, 마지막에 n을 출력하라는 의미입니다.

CSV 출력 모드. -o csv 커맨드라인 인자는 GoAWK에 CSV 출력 모드를 사용하라고 지시합니다. 이렇게 하면 하나 이상의 인자를 가진 print가 적절한 CSV 인코딩으로 출력을 작성합니다.

예를 들어 states.csv를 TSV로 변환하려면 다음과 같이 할 수 있습니다(여기서는 -H사용하지 않으므로 헤더 행이 포함됩니다):

$ goawk -i csv -o tsv '{ print $1, $2 }' states.csv
State   Abbreviation
Alabama AL
Alaska  AK
Arizona AZ
...

게다가 표준 AWK의 모든 기능을 그대로 사용할 수 있으므로, 할 수 있는 일이 훨씬 더 많습니다.

CSV 문서를 읽어보세요 — 전체 세부 정보와 더 많은 예제를 확인할 수 있습니다.

구현 관련 메모

구현은 광범위한 테스트를 포함해 약 2000줄의 코드로 이루어져 있습니다.

물론 interp.Config 구조체로 옵션을 설정해 CSV 입력 또는 출력 모드를 활성화할 수 있는 Go API도 있습니다. BEGIN 블록에서 INPUTMODEOUTPUTMODE 특수 변수를 설정할 수도 있습니다.

여러 가지 이유로 CSV 입력 모드에 encoding/csv.Reader를 직접 사용할 수는 없었지만, 표준 라이브러리 코드의 구조를 제 csvSplitter에 재사용하여 필드를 파싱하고 전체 행을 토큰으로 제공하는 bufio.SplitFunc 형태로 다듬었습니다.

제가 작성한 많은 테스트 외에도, 제 구현이 뭔가 잘못되지 않았고 여전히 RFC 4180을 준수하는지 확인하기 위해 표준 라이브러리의 csv.Reader 테스트 중 관련된 일부를 실행합니다.

CSV 출력 모드에서는 encoding/csv.Writer를 직접 사용할 수 있었습니다(CSV 쓰기는 읽기보다 훨씬 간단하기는 합니다). 자세한 내용은 writeCSV 함수를 참조하세요.

성능

아직 성능에 많은 시간을 투자하지는 않았습니다. 언젠가 제대로 프로파일링할 계획이지만, 현재로서는 “충분히 빠릅니다” — Go의 encoding/csv 패키지를 직접 사용하는 것과 비슷한 수준이며, Python의 csv 모듈보다는 훨씬 빠릅니다. 제 노트북에서 복잡한 CSV 입력 1기가바이트를 약 3초 만에 읽을 수 있습니다.

frawk와 비교하면 CSV 입력 속도는 상당히 느리지만, (다소 놀랍게도) CSV 출력 속도는 훨씬 빠릅니다.

아래는 goawkfrawk, 그리고 순수 Python과 Go를 사용한 간단한 읽기 및 쓰기 벤치마크 결과입니다. 쓰기 벤치마크의 출력은 20개 열(따옴표로 묶인 열 포함)을 가진 1GB, 350만 행의 CSV 파일이며, 읽기 벤치마크의 입력은 동일한 파일을 사용합니다. 시간은 초 단위이며, SSD를 탑재한 64비트 Linux 노트북에서 세 번 실행한 결과 중 가장 좋은 기록입니다:

테스트goawkfrawkPythonGo
1GB CSV 읽기3.181.0113.43.22
1GB CSV 쓰기5.6413.017.03.24

결론

컴퓨터는 이제 정말 빠르므로, 개발자용 노트북이나 비교적 작은 가상 머신만으로도 GoAWK를 사용해 대부분의 CSV 데이터셋, 심지어 기가바이트 단위의 데이터셋까지 처리할 수 있습니다. 누가 Hadoop이 필요하다고 하겠습니까?

그러니 다음 CSV 처리 작업에 GoAWK를 한번 사용해 보시기 바랍니다. AWK는 45년이 되었지만 여전히 널리 사용되고 있으며, 이 기능이 개발자들의 데이터 도구 모음에서 AWK를 더욱 유용하게 만들어 주기를 바랍니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글