DaleSchool

awk와 sed 심화

입문30분

학습 목표

  • awk로 패턴/액션 구조를 활용한 데이터 처리를 할 수 있다
  • awk의 BEGIN, END, NR, NF를 활용할 수 있다
  • sed로 텍스트를 검색하고 치환할 수 있다
  • 실전 로그 분석 파이프라인을 구성할 수 있다

동작하는 코드

예제 1: awk 패턴/액션 기본 구조

# 연습용 로그 파일 만들기
cat > app.log << 'EOF'
2024-01-15 10:30:01 INFO 사용자 로그인: user001
2024-01-15 10:30:05 ERROR 데이터베이스 연결 실패
2024-01-15 10:30:10 INFO 파일 업로드 완료: report.pdf
2024-01-15 10:31:00 WARN 메모리 사용량 80% 초과
2024-01-15 10:31:05 ERROR 파일 저장 실패: permission denied
2024-01-15 10:32:00 INFO 사용자 로그아웃: user001
EOF

awk 기본 사용:

# 3번째 열(로그 레벨)만 출력
awk '{ print $3 }' app.log

출력:

INFO
ERROR
INFO
WARN
ERROR
INFO
# ERROR인 행만 출력
awk '$3 == "ERROR"' app.log

출력:

2024-01-15 10:30:05 ERROR 데이터베이스 연결 실패
2024-01-15 10:31:05 ERROR 파일 저장 실패: permission denied

예제 2: awk BEGIN과 END

# 처음에 헤더, 끝에 요약 출력
awk '
  BEGIN { print "=== 오류 목록 ===" }
  $3 == "ERROR" { print NR"번째 줄:", $0 }
  END { print "=== 검색 완료 ===" }
' app.log

출력:

=== 오류 목록 ===
2번째 줄: 2024-01-15 10:30:05 ERROR 데이터베이스 연결 실패
5번째 줄: 2024-01-15 10:31:05 ERROR 파일 저장 실패: permission denied
=== 검색 완료 ===

예제 3: sed 텍스트 치환

echo "Hello World" | sed 's/World/Terminal/'

출력:

Hello Terminal
# 파일에서 치환 (파일 내용은 변경 안 됨, stdout으로 출력)
sed 's/INFO/정보/g' app.log | head -3

출력:

2024-01-15 10:30:01 정보 사용자 로그인: user001
2024-01-15 10:30:05 ERROR 데이터베이스 연결 실패
2024-01-15 10:30:10 정보 파일 업로드 완료: report.pdf

직접 수정하기

awk 심화: 집계와 계산

# 로그 레벨별 개수 집계
awk '{ count[$3]++ } END { for (level in count) print level":", count[level] }' app.log

출력:

INFO: 3
ERROR: 2
WARN: 1
# CSV 데이터 처리
cat > sales.csv << 'EOF'
이름,상품,금액
홍길동,노트북,1200000
김철수,마우스,45000
이영희,키보드,89000
박민수,노트북,1150000
최지원,모니터,350000
EOF

# 금액 합계 계산
awk -F',' 'NR > 1 { sum += $3 } END { printf "총 매출: %d원\n", sum }' sales.csv

출력:

총 매출: 2834000원
# 100만원 이상 상품만 출력
awk -F',' 'NR > 1 && $3 >= 1000000 { print $1, $2, $3 }' sales.csv

출력:

홍길동 노트북 1200000
박민수 노트북 1150000

awk NF와 NR 활용

# NF: 현재 줄의 필드 수
echo "a b c d" | awk '{ print "필드 수:", NF }'
# 필드 수: 4

# 마지막 필드
echo "a b c d" | awk '{ print "마지막:", $NF }'
# 마지막: d

# NR: 줄 번호
awk 'NR >= 2 && NR <= 4' app.log

출력 (2~4번째 줄):

2024-01-15 10:30:05 ERROR 데이터베이스 연결 실패
2024-01-15 10:30:10 INFO 파일 업로드 완료: report.pdf
2024-01-15 10:31:00 WARN 메모리 사용량 80% 초과

sed 심화

# 기본 치환: s/찾기/바꾸기/플래그
echo "apple apple apple" | sed 's/apple/banana/'      # 첫 번째만
echo "apple apple apple" | sed 's/apple/banana/g'     # 모두 (global)
echo "APPLE" | sed 's/apple/banana/i'                 # 대소문자 무시

# 특정 줄만 치환 (2번째 줄)
sed '2s/ERROR/에러/' app.log

# 특정 줄 삭제
sed '1d' app.log           # 1번째 줄 삭제
sed '/ERROR/d' app.log     # ERROR 포함 줄 삭제
sed '2,4d' app.log         # 2~4번째 줄 삭제

# 특정 줄만 출력 (-n과 p 조합)
sed -n '2,4p' app.log      # 2~4번째 줄만 출력
sed -n '/ERROR/p' app.log  # ERROR 포함 줄만 출력

# 줄 앞에 텍스트 추가
sed 's/^/[LOG] /' app.log

파일 직접 수정 (-i 옵션):

# 백업 없이 직접 수정 (주의!)
sed -i 's/ERROR/에러/g' app.log

# 백업 파일 만들면서 수정 (원본은 .bak으로 보존)
sed -i.bak 's/INFO/정보/g' app.log
ls
# app.log  app.log.bak

"왜?" — 실전 로그 분석 파이프라인

시나리오: 아파치 웹 서버 로그 분석

아파치 접속 로그 형식:

192.168.1.1 - - [15/Jan/2024:10:30:01] "GET /index.html HTTP/1.1" 200 1234
192.168.1.2 - - [15/Jan/2024:10:30:05] "POST /api/login HTTP/1.1" 401 567
10.0.0.1 - - [15/Jan/2024:10:30:10] "GET /admin HTTP/1.1" 404 234
# IP별 접속 횟수 (상위 10개)
awk '{ print $1 }' access.log | sort | uniq -c | sort -rn | head -10

# 404 에러 URL만
awk '$9 == "404" { print $7 }' access.log

# 시간대별 요청 수
awk '{ print substr($4, 2, 14) }' access.log | cut -d: -f2 | sort | uniq -c

# 특정 IP 차단 (iptables에 추가할 목록)
awk '$9 == "401" { print $1 }' access.log | sort | uniq -c | sort -rn | awk '$1 > 100 { print $2 }'

시나리오: CSV 데이터 변환

# CSV를 TSV로 변환 (구분자 변경)
sed 's/,/\t/g' data.csv > data.tsv

# 헤더 제거
sed '1d' data.csv > data-no-header.csv

# 특정 열 제거 (2번째 열)
awk -F',' 'BEGIN{OFS=","} { $2=""; print }' data.csv

# 열 순서 변경 (3,1,2 순으로)
awk -F',' 'BEGIN{OFS=","} { print $3, $1, $2 }' data.csv

흔한 실수

실수 1: awk와 grep 역할 혼동

# grep: 패턴이 있는 줄 찾기
grep "ERROR" app.log

# awk: 특정 필드가 일치하는 줄 찾기
awk '$3 == "ERROR"' app.log   # 3번째 필드가 정확히 "ERROR"인 것만

# 차이: grep은 줄 전체에서, awk는 필드 기준
grep "ERROR" app.log   # "NOT_AN_ERROR"도 매칭됨
awk '$3 == "ERROR"' app.log  # 정확히 3번째 필드가 "ERROR"인 것만

실수 2: sed의 -i 옵션 주의

# macOS에서 -i 뒤에 반드시 확장자나 '' 필요
sed -i '' 's/old/new/g' file.txt  # macOS
sed -i 's/old/new/g' file.txt     # Linux

# 실수 방지: 먼저 stdout으로 확인
sed 's/old/new/g' file.txt | head -5
# 결과가 맞으면 -i로 실제 수정

실수 3: awk에서 문자열 비교 vs 숫자 비교

# 문자열 비교 (== 사용)
awk '$3 == "ERROR"' app.log   # 올바름

# 숫자 비교 (산술 비교)
awk '$2 > 100' numbers.csv    # 올바름

# 문자열로 저장된 숫자 비교 주의
awk '$2 > "100"' numbers.csv  # 사전순 비교! "9" > "100"이 됨
awk '$2 + 0 > 100' numbers.csv  # 숫자로 강제 변환 후 비교

심화 학습

awk 다중 파일 처리와 FILENAME
# 여러 파일 처리 시 현재 파일명 출력
awk '{ print FILENAME, $0 }' *.log

# 파일별 통계
awk '{ count[FILENAME]++ } END { for (f in count) print f, count[f] }' *.log

# FNR: 파일 내 줄 번호 (NR은 전체 줄 번호)
awk '{ print FILENAME, FNR, $0 }' file1.txt file2.txt
sed 정규식 활용
# 날짜 형식 변환 (YYYY-MM-DD → DD/MM/YYYY)
echo "2024-01-15" | sed 's/\([0-9]*\)-\([0-9]*\)-\([0-9]*\)/\3\/\2\/\1/'
# 15/01/2024

# 빈 줄 제거
sed '/^$/d' file.txt

# 주석 줄 제거 (# 으로 시작하는 줄)
sed '/^#/d' config.txt

# 앞뒤 공백 제거
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' file.txt

# HTML 태그 제거
sed 's/<[^>]*>//g' page.html
실전: 설정 파일 자동 수정 스크립트
#!/bin/bash
set -euo pipefail

CONFIG="app.conf"

# 특정 설정값 변경
update_config() {
  local key="$1"
  local value="$2"
  # 이미 있으면 교체, 없으면 추가
  if grep -q "^${key}=" "$CONFIG"; then
    sed -i.bak "s/^${key}=.*/${key}=${value}/" "$CONFIG"
  else
    echo "${key}=${value}" >> "$CONFIG"
  fi
}

update_config "DB_HOST" "localhost"
update_config "DB_PORT" "5432"
update_config "DEBUG" "false"

echo "설정 업데이트 완료"
  1. 로그 파일을 만들고 awk '$3 == "ERROR"' 파일명으로 에러 줄만 출력해보세요.
  2. awk '{ count[$3]++ } END { for (k in count) print k, count[k] }' 파일명으로 레벨별 개수를 집계하세요.
  3. sed 's/INFO/정보/g' 파일명으로 텍스트를 치환해보세요.
  4. sed -n '/ERROR/p' 파일명으로 ERROR 포함 줄만 출력해보세요.
  5. CSV 파일을 만들고 awk -F',' 'NR > 1 { sum += $2 } END { print "합계:", sum }' 파일명으로 두 번째 열의 합을 구해보세요.

Q1. awk 'BEGIN { print "시작" } $3 == "ERROR" { count++ } END { print "에러:", count }' log.txt에서 각 블록의 실행 시점은?

  • A) BEGIN=매 줄마다, $3=="ERROR"=파일 처음, END=파일 끝
  • B) BEGIN=파일 처리 전, $3=="ERROR"=조건 맞는 줄마다, END=파일 처리 후
  • C) BEGIN=조건 처음, $3=="ERROR"=파일 처음, END=파일 끝
  • D) BEGIN=항상, $3=="ERROR"=ERROR 파일에서, END=항상