정규표현식 in Go

Sungwoo Hwang·2021년 10월 10일
0

Go

목록 보기
1/1
post-thumbnail

Go에서는 기본패키지인 regexp 에서 정규표현식을 지원한다. 간단하게 아래와 같이 func MatchString(pattern string,s string) (matched bool, err error)를 이용해 확인 가능하다.

import "regexp"

// ( )는 substring이다.
match, _ :=regexp.MatchString("p([a-z]+)ch","peach")	

Regexp 구조체

하지만 정규식의 재사용을 위해서는 func Compile(expr string) (*Regexp, error) 이라는 함수를 사용해 Regexp 구조체를 반환받고 Rexexp 구조체의 함수들을 이용해야 한다.

r, _ := regexp.Compile("p([a-z]+)ch")

// or

r_ :=regexp.MustComplie("p([a-z]+)ch")	

만약 인자로 넘겨준 regexp 이 유효한 regexp이 아닐때 panic을 발생시키길 원한다면 , func MustCompile(str string) *Regexp 을 사용해야 한다.

표현식이 합당하기만 하다면, MustCompile() 도 내부적으로 Compile() 을 사용하기때문에 두 코드의 결과물은 정확히 같다.

반환값으로 error를 받아서 처리하길 원한다면 Compile() 을 , panic을 발생시키고 recover() 로 처리를 원한다면 MustCompile()을 사용하면 된다.

// MustCompile is like Compile but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
	regexp, err := Compile(str)
	if err != nil {
		panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
	}
	return regexp
}

어느 방법이 되었건 Regexp 구조체를 사용해 정규표현식을 검증해보자.

func (re *Regexp) MatchString(s string) bool 함수를 이용하면 정규표현식에 맞는지 bool 타입의 반환값을 얻음으로써 정규표현식을 간단하게 검증할 수 있다.

r, _ := regexp.Compile("p([a-z]+)ch")

fmt.Println(r.MatchString("peach")) // true
fmt.Println(r.MatchString("pch")) // false

만약 문자열이 아닌 []byte 를 검증하려면 func (re *Regexp) Match( b[] byte) bool 을 , Reader를 검증하고 싶다면 func (re *Regexp) MatchReader(r io.RuneReader) bool 을 사용한다.

Regexp 구조체의 함수들

func (re * Regexp) FindString(s string) string) : 인자로 받은 s 에서 가장 첫번째로 매치되는 문자열을 반환한다. 모든 매치되는 문자열을 얻고 싶으면 FindAllString() 을 사용한다.

fmt.Println(r.FindString("pzch peach peach)") // pzch

func (re *Regexp) FindStringIndex(s string) (loc []int]) : 첫번째로 매칭되는 문자열을 찾되 인자로 넘겨준 string 에서 일치하는 텍스트의 첫 인덱스와 마지막 인덱스를 반환한다.

fmt.Println(r.FindStringIndex("peach punch")) // [0 5]

func (re *Regexp) FindStringSubmatch (s string) [] string : 첫번째로 매칭되는 문자열의 전체 패턴일치와 submatch 되는 정보를 포함하는 string slice 를 반환한다.
아래 코드를 보면 "pch"가 가장 첫번째 문자열이지만 매치되지 않고 첫번째로 매치되는 "punch"와 submatch 되는 "uh"를 반환한다.

fmt.Println(r.FindStringSubmatch("pch punch peach")) // [punch un]

특정 문자열 중에서 정규표현식과 일치하는 부분 모두를 어떤 문자열로 바꾸고 싶으면 func (re *Regexp) ReplaceAll(src, repl string) string) 을 사용 할 수 있다.

fmt.Println(r.ReplaceAllString("a peach pzch pch ", "<fruit>")) // a <fruit> <fruit> pch

Scanner와 정규표현식

예를 들어 log파일을 Go에서 읽어서 어떤 후처리가 필요하다고 해보자. 정규표현식을 읽어서 shell script를 작성해도 되지만, Go 내부에서 로직을 구현하고 싶다면, 아래와 같은 구조를 만들면 된다.

  • log 파일을 Open한다.
  • Scanner 구조체의 Scan() 을 이용해서 읽는다.
  • 각 줄 (혹은 각 문자,단어)에 대해 정규표현식 검사를 수행한다.
  • 필요에 따라 로직을 수행한다.

예를 들어 아래와 같은 example.log 가 있을때, 0,1,4,5분에 panic 에 대한 기록이 되어 있다.
이 때 panic이 난 시간에 대한 정보와 이유를 error.log 에 기록하는 로직을 구성해본다고 하자.

2021/10/09 00:00:00 out of memory
2021/10/09 00:01:00 lost connection
2021/10/09 00:02:00 ok
2021/10/09 00:03:00 ok
2021/10/09 00:04:00 server error
2021/10/09 00:05:00 app crash
2021/10/09 00:06:00 ok

먼저 log를 Scanner 를 이용해서 읽는 코드를 작성해보자. 아래 코드는 log를 line으로 seperate해서 읽는다.

package main

...
...

func main() {
	args := os.Args

	logPath := args[1]

	f, err := os.Open(logPath)
	checkError(err)
	defer f.Close()

	logScanner := bufio.NewScanner(f)

	for logScanner.Scan() {
		line := logScanner.Text()
		...
		...
	}
}

정규표현식 작성하기

이제 위의 로그를 검증할 수 있는 정규표현식을 작성해보자.

`^([0-9]{4})/([0-9]{2})/([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})\s([[:alpha:]\s]+)$`

길지만 차근차근 보면 아주 간단하다.

([0-9]{n}) 은 숫자가 n번 반복된다는 것이고 공백에 대해서 escape를 한 후 (\s )
([[:alpha:]\s]+) 이 로그에 적힌 ok 혹은 panic 의 이유를 가져오는 부분이다.

[:alpha:] 는 alphabetic (== [A-Za-z])이다.

이것을 바탕으로 Go에서 정규표현식을 검증하는 함수를 만들어보자.

FindAllStringSubMatch()FindStringSubMatch() 의 "All" 버전이다. Go Docs

// 패닉이 있었다면 패닉에 대한 정보와 bool type을 반환
func ParseLog(reg *regexp.Regexp, logLine string) (panicInfo []string, ok bool) {
	matched := reg.FindAllStringSubmatch(logLine, -1)
	
  // matched[0][0] 에는 전문이 들어있고, matched[0][7]에는 7번째 그룹인 상태가 들어있다.
	if matched[0][7] == "ok" {
		return make([]string, 0), true
	}
	
	// 전문을 제외한 나머지를 리턴
	return matched[0][1:8], false
}

이젠 ParseLog 의 반환값을 error.log 파일에 작성 할 수 있게 해주는 HandlePanicLog 함수를 작성해보자.

func HandlePanicLog(panicInfo []string, logWriter *bufio.Writer) {
	logWriter.WriteString(strings.Join(panicInfo, " ") + "\n")
	logWriter.Flush()
}

이제 전체적인 코드를 다시 보자.

package main

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"
)

func checkError(err error) {
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

// 패닉이 있었다면 패닉에 대한 정보와 bool type을 반환
func ParseLog(reg *regexp.Regexp, logLine string) (panicInfo []string, ok bool) {
	matched := reg.FindAllStringSubmatch(logLine, -1)
	
  // matched[0][0] 에는 전문이 들어있고, matched[0][7]에는 7번째 그룹인 상태가 들어있다.
	if matched[0][7] == "ok" {
		return make([]string, 0), true
	}
	
	// 전문을 제외한 나머지를 리턴
	return matched[0][1:8], false
}

func HandlePanicLog(panicInfo []string, logWriter *bufio.Writer) {
	logWriter.WriteString(strings.Join(panicInfo, " ") + "\n")
	logWriter.Flush()
}

func main() {
	args := os.Args

	logPath := args[1]

	f, err := os.Open(logPath)
	checkError(err)
	defer f.Close()

	// error.log 파일에 대하여, 없으면 만들고, 있으면 비우고 읽고 쓰고 , 읽고 쓰고, rw-r--r-- 모드
	wf, err := os.OpenFile("error.log", os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(0644))

	checkError(err)

	defer wf.Close()

	errorLogWriter := bufio.NewWriter(wf)

	logScanner := bufio.NewScanner(f)

	// {YYYY}/{MM}/{DD} {hh}:{mm}:{ss} {ok|panic reason} 검증 정규표현식
	reg := regexp.MustCompile(`^([0-9]{4})/([0-9]{2})/([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})\s([[:alpha:]\s]+)$`)

	for logScanner.Scan() {
		line := logScanner.Text()
		checkError(logScanner.Err())
		
		if panicInfo, ok := ParseLog(reg, line); !ok {
			HandlePanicLog(panicInfo, errorLogWriter)
		}
	}
}

이제 이 코드를 실행시켜보면 , error.log 파일이 만들어져있다. 직접 확인해보면 아래와 같다.

$ cat error.log
> 2021 10 09 00 00 00 out of memory
	2021 10 09 00 01 00 lost connection
	2021 10 09 00 04 00 server error
	2021 10 09 00 05 00 app crash

완성!

profile
becomeweasel.tistory.com로 이전

0개의 댓글