트랙
/
Go
Go
/
연습 문제
/
로그 파일 파싱
로그 파일 파싱

로그 파일 파싱

학습 연습 문제

소개

regexp 패키지는 Go에서 정규 표현식을 지원해요.

문법

허용되는 정규 표현식의 문법은 Perl, Python 및 다른 언어에서 사용하는 일반적인 문법과 같아요.

검색 패턴과 입력 텍스트는 모두 UTF-8로 해석돼요.

백틱(`)으로 문자열을 만들면 백슬래시(\)는 특별한 의미가 없고, 탭 \t이나 줄바꿈 \n 같은 특수 문자의 시작을 나타내지도 않아요:

"\t\n" // regular string literal with 2 characters: a tab and a newline
`\t\n`// raw string literal with 4 characters: two backslashes, a 't', and an 'n'

이런 이유로 정규 표현식을 만들 때는 백틱을 사용하는 게 좋아요. 백슬래시를 이스케이프할 필요가 없기 때문이에요:

"\\" // string with a single backslash
`\\` // string with 2 backslashes

패턴 컴파일하기 - RegExp 타입

정규 표현식을 사용하려면 먼저 문자열 패턴을 컴파일해야 해요. 여기서 컴파일이란 정규 표현식의 문자열 패턴을 다루기 쉬운 내부 표현으로 변환하는 것을 말해요. 각 패턴은 한 번만 컴파일하면 되고, 그 뒤에는 컴파일된 정규 표현식을 여러 번 사용할 수 있어요. regexp.Regexp 타입은 컴파일된 정규 표현식을 나타내요. regexp.Compile 함수를 사용하면 문자열 패턴을 regexp.Regexp로 컴파일할 수 있어요. 컴파일에 실패하면 이 함수는 nil과 오류를 반환해요:

re, err := regexp.Compile(`(a|b)+`)
fmt.Println(re, err) // => (a|b)+ <nil>
re, err = regexp.Compile(`a|b)+`)
fmt.Println(re, err) // => <nil> error parsing regexp: unexpected ): `a|b)+`

MustCompile 함수는 Compile의 편리한 대안이에요:

re = regexp.MustCompile(`[a-z]+\d*`)

이 함수를 사용하면 오류를 처리할 필요가 없어요.

Caution

MustCompile은 패턴이 컴파일된다는 것을 확실히 알 때만 사용해야 해요. 그렇지 않으면 프로그램이 패닉을 일으키기 때문이에요.

정규 표현식 메서드

정규 표현식과 일치하는 부분을 찾고 일치한 텍스트를 식별하는 Regexp의 메서드가 16개 있어요. 이 메서드들의 이름은 다음 정규 표현식과 일치해요:

Find(All)?(String)?(Submatch)?(Index)?
  • All이 있으면 메서드는 전체 표현식에서 겹치지 않는 연속 일치를 찾아요.
  • String이 있으면 인자는 문자열이고, 없으면 바이트 슬라이스예요. 반환 값은 그에 맞게 조정돼요.
  • Submatch가 있으면 반환 값은 표현식의 연속된 부분 일치를 나타내는 슬라이스예요.
  • Index가 있으면 일치와 부분 일치는 입력 문자열 안의 바이트 인덱스 쌍으로 식별돼요.

다음과 같은 작업을 위한 메서드도 있어요:

  • 정규 표현식의 일치 부분을 대체 문자열로 바꾸기, 그리고
  • 정규 표현식으로 구분된 문자열 나누기.

전체적으로 regexp 패키지는 40개가 넘는 함수와 메서드를 정의해요. 아래에서 몇 가지 메서드의 사용법을 보여드릴게요. 이 함수들과 다른 함수들의 자세한 내용은 API 문서를 참고해요.

MatchString 예제

MatchString 메서드는 문자열이 정규 표현식과 일치하는 부분을 포함하는지 알려줘요.

re = regexp.MustCompile(`[a-z]+\d*`)
b = re.MatchString("[a12]")       // => true
b = re.MatchString("12abc34(ef)") // => true
b = re.MatchString(" abc!")       // => true
b = re.MatchString("123 456")     // => false

FindString 예제

FindString 메서드는 정규 표현식과 일치하는 가장 왼쪽 부분의 텍스트를 담은 문자열을 반환해요.

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.FindString("[a12]")       // => "a12"
s = re.FindString("12abc34(ef)") // => "abc34"
s = re.FindString(" abc!")       // => "abc"
s = re.FindString("123 456")     // => ""

FindStringSubmatch 예제

FindStringSubmatch 메서드는 정규 표현식과 일치하는 가장 왼쪽 부분의 텍스트와, 있다면 하위 표현식의 일치를 담은 문자열 슬라이스를 반환해요. 이는 캡처 그룹과 일치하는 문자열을 식별하는 데 사용할 수 있어요. 반환 값이 nil이면 일치하는 부분이 없다는 뜻이에요.

re = regexp.MustCompile(`[a-z]+(\d*)`)
sl = re.FindStringSubmatch("[a12]")       // => []string{"a12","12"}
sl = re.FindStringSubmatch("12abc34(ef)") // => []string{"abc34","34"}
sl = re.FindStringSubmatch(" abc!")       // => []string{"abc",""}
sl = re.FindStringSubmatch("123 456")     // => <nil>

ReplaceAllString 예제

re.ReplaceAllString(src,repl) 메서드는 src의 복사본을 반환하면서, 정규 표현식 re와 일치하는 부분을 대체 문자열 repl로 바꿔요.

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.ReplaceAllString("[a12]", "X")       // => "[X]"
s = re.ReplaceAllString("12abc34(ef)", "X") // => "12X(X)"
s = re.ReplaceAllString(" abc!", "X")       // => " X!"
s = re.ReplaceAllString("123 456", "X")     // => "123 456"

Split 예제

re.Split(s,n) 메서드는 텍스트 s를 표현식으로 구분된 부분 문자열로 나누고, 그 표현식 일치 사이의 부분 문자열 슬라이스를 반환해요. 개수 n은 반환할 부분 문자열의 최대 개수를 정해요. n<0이면 메서드는 모든 부분 문자열을 반환해요.

re = regexp.MustCompile(`[a-z]+\d*`)
sl = re.Split("[a12]", -1)      // => []string{"[","]"}
sl = re.Split("12abc34(ef)", 2) // => []string{"12","(ef)"}
sl = re.Split(" abc!", -1)      // => []string{" ","!"}
sl = re.Split("123 456", -1)    // => []string{"123 456"}

지침

이 연습 문제는 로그 파일 파싱을 다뤄요.

최근 보안 검토를 거친 뒤, 조직에서 보관 중인 로그 파일을 정리해 달라는 요청을 받았어요.

함수에 전달되는 모든 문자열은 null이 아니며 앞뒤에 공백이 없다고 보장돼요.

1. 깨진 로그 줄 식별하기

보관된 로그 줄 중 얼마나 많은 줄이 현재 표준을 따르지 않는지 대략 알고 있어야 해요. 간단한 검사만으로 로그 줄이 유효한지 알아낼 수 있다고 생각해요. 유효한 줄로 인정되려면 다음 문자열 중 하나로 시작해야 해요:

  • [TRC]
  • [DBG]
  • [INF]
  • [WRN]
  • [ERR]
  • [FTL]

IsValidLine 함수를 구현해서 문자열이 유효하지 않으면 false를, 그렇지 않으면 true를 반환하게 해요.

IsValidLine("[ERR] A good error here")
// => true
IsValidLine("Any old [ERR] text")
// => false
IsValidLine("[BOB] Any old text")
// => false

2. 로그 줄 나누기

새로운 팀이 조직에 합류했는데, 그 팀의 로그 파일은 "필드"를 구분할 때 이상한 구분자를 사용하고 있어요. 콜론 ":"처럼 합리적인 문자 대신 "<--->"나 "<=>" 같은 문자열을 써요(더 예쁘다는 이유로요). 사실 첫 문자가 "<"이고 마지막 문자가 ">"이며, 그 사이에 "~", "*", "=", "-"가 어떤 조합으로든 들어가는 문자열이라면 뭐든 괜찮아요.

SplitLogLine 함수를 구현해요. 이 함수는 줄을 받아서 각각 하나의 필드를 담은 문자열 배열을 반환해요.

SplitLogLine("section 1<*>section 2<~~~>section 3")
// => []string{"section 1", "section 2", "section 3"},

3. 따옴표 안에 password가 포함된 줄 세기

팀은 따옴표 안에서 비밀번호를 언급한 부분을 알아야 직접 검토할 수 있어요.

수동 작업의 규모가 어느 정도일지 가늠할 수 있도록 CountQuotedPasswords 함수를 구현해요.

대소문자가 어떤 조합으로든 섞일 수 있는 문자열 "password"가 따옴표로 둘러싸인 로그 줄을 찾아내요. 따옴표 안에서 "password" 앞뒤에 다른 내용이 있을 수도 있다는 점을 고려해야 해요. 각 줄에는 따옴표가 많아야 두 개 있어요.

이 루틴에 전달되는 줄은 1번 작업에서 정의한 대로 유효할 수도 있고 아닐 수도 있어요. 유효하든 아니든 똑같은 방식으로 처리해요.

lines := []string{
    `[INF] passWord`, // contains 'password' but not surrounded by quotation marks
    `"passWord"`,  // count this one
    `[INF] User saw error message "Unexpected Error" on page load.`, // does not contain 'password'
    `[INF] The message "Please reset your password" was ignored by the user`, // count this one
}
// => 2

4. 로그에서 부산물 제거하기

로그를 처리하는 상위 단계 어딘가에서 "end-of-line"이라는 텍스트와 그 뒤에 줄 번호가 (사이에 공백 없이) 로그 전체에 흩뿌려지고 있다는 걸 발견했어요.

RemoveEndOfLineText 함수를 구현해서 문자열을 받아 "end-of-line" 텍스트를 제거하고 "깨끗한" 문자열을 반환하게 해요.

"end-of-line" 텍스트가 없는 줄은 그대로 반환해야 해요.

"end-of-line" 문자열만 제거해요. 공백은 그대로 두면 돼요.

RemoveEndOfLineText("[INF] end-of-line23033 Network Failure end-of-line27")
// => "[INF]  Network Failure "

5. 사용자 이름으로 줄에 태그 달기

일부 로그 줄에 사용자를 언급하는 문장이 포함되어 있다는 걸 알아차렸어요. 이런 문장에는 항상 "User"라는 문자열이 있고, 그 뒤에 하나 이상의 공백 문자, 그다음에 사용자 이름이 와요. 그래서 그런 줄에는 태그를 달기로 해요.

로그 줄을 처리하는 TagWithUserName 함수를 구현해요:

  • "User " 문자열이 없는 줄은 그대로 둬요.
  • "User " 문자열이 있는 줄은 줄 앞에 [USR]과 사용자 이름을 붙여요.

예를 들어:

result := TagWithUserName([]string{
    "[WRN] User James123 has exceeded storage space.",
	"[WRN] Host down. User   Michelle4 lost connection.",
	"[INF] Users can login again after 23:00.",
	"[DBG] We need to check that user names are at least 6 chars long.",
})
// => []string {
//  "[USR] James123 [WRN] User James123 has exceeded storage space.",
//  "[USR] Michelle4 [WRN] Host down. User   Michelle4 lost connection.",
//  "[INF] Users can login again after 23:00.",
//  "[DBG] We need to check that user names are at least 6 chars long."
// }

다음과 같이 가정할 수 있어요:

  • 로그에서 사용자 이름 뒤에는 공백 문자가 하나 이상 와요.
  • 각 줄에는 "User " 문자열이 많아야 한 번 나와요.
  • 사용자 이름은 공백을 포함하지 않는 비어 있지 않은 문자열이에요.
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Go Exercism

로그 파일 파싱 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Go 트랙을 개념 34개연습 문제 165개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.