이번 글에서는 토큰 분석기, Lexer를 파이썬을 이용해 만들어 보는 내용을 담았습니다.

이 글은 https://riptutorial.com/en/python/example/31584을 참고하여 작성하였습니다.

 

Python Language - Part 1: Tokenizing Input with Lex | python Tutorial

python documentation: Part 1: Tokenizing Input with Lex

riptutorial.com


먼저 ply 모듈에 대한 설치가 필요합니다.

conda나 pip 명령어를 활용하여, ply 모듈을 다운로드하여 주세요.

다운로드가 잘 되어있는지 확인은

import ply.lex 를 통해 확인하실 수 있습니다.

 


Code

전체적인 코드입니다.

import ply.lex as lex

tokens = [  # 사용되는 토큰 정의하기, 필수
    'NUMBER',
    'PLUS',
    'MINUS',
    'TIMES',
    'DIVIDE',
    'LPAREN',
    'RPAREN',
]  # 토큰 별로 정규식이 정의되어야 한다 함수를 통해서 혹은 단순히 아래처럼, 둘 다 규칙은 t_이름

t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'


# 값 변환 등 액션이 필요한 토큰은 함수로 정의한다.
def t_NUMBER(t):  # t가 인스턴스 토큰
    r'\d+'
    t.value = int(t.value)
    return t


def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)


t_ignore = ' \t'


def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)


lexer = lex.lex()


while True:
    tok = lexer.token()
    if not tok:
        break  # No more input
    print(tok)

 

1) tokens 라는 리스트로 사용할 토큰들을 정의하는 과정이 필요합니다.

위 코드에서는 수식을 토큰들로 나누는 예제입니다.

 

2) 토큰들을 정의한 후, 각각의 토큰들이 어떤 형식으로 표현되는 토큰들인지를 정의해야 합니다.

어떤 토큰을 정의하는 것인지를 표현할 때는 < t_토큰 이름 > 형식으로 사용하시면 됩니다.

간단하게 정의를 할 때는 아래와 같이 정의할 수 있습니다. 표현은 정규식의 형태로 되어 있습니다.

t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'

 

토큰을 표현하면서 추가적인 작업이 필요한 경우에는 함수로 구현할 수 있습니다.

def t_NUMBER(t):  # t가 인스턴스 토큰
    r'\d+'
    t.value = int(t.value)
    return t

함수 정의 맨 윗줄의 정규식이 토큰의 표현식이 됩니다. 이 함수의 매개변수로 들어오는 t는 인스턴스 토큰입니다. 위 같은 경우에는 숫자 자료형으로 변환시켜주기 위해서 함수로 정의를 했습니다.

 

t는 다음과 같은 속성값들을 가지고 있습니다.

1) t.type : 토큰의 타입을 의미합니다. 기본값으로는 t_ 전두어 다음으로 들어오는 이름입니다.

2) t.value : 실제 텍스트 값을 의미합니다.

3) t.lineno : 몇번째 줄인지를 표시하는 값인데, lexer 자체로는 라인을 알 수 없기 때문에 아래와 같은 함수를 정의하면 됩니다.

def t_newline(t):
      r'\n+'
      t.lexer.lineno += len(t.value)

4) t.lexpos : 들어오는 글의 시작부터 상대적인 위치를 나타냅니다.

 

 

만약에 무시하고 싶은 토큰이라면 아래 두 가지 방법 중 한 가지를 쓰면 됩니다.

def t_COMMENT(t):
       r'\#.*'
       pass
t_ignore_COMMENT = r'\#.*'

 

함수로 정의할 땐 반환값을 적지 않으면 됩니다. 혹은 < t_ignore_토큰 이름 > 으로 더 간편하게 정의할 수 있습니다. 딱히 토큰의 이름을 정의하지 않고도 무시하고 싶다면 < t_ignore > 로 정의할 수 있습니다.

 t_ignore  = ' \t'    # 스페이스와 탭을 무시하게 됩니다.

 

 

Literals : t.type 과 t.value가 문자 자체로 표현되는 토큰입니다. 

literals = [ '+', '-', '*', '/' ]
literals = "+-*/"

 

윗 줄과 아랫 줄은 같은 의미입니다. Literal 또한 함수로 정의하여 추가적인 작업을 하게끔 할 수 있습니다.  

 

literals = [ '{', '}' ]

def t_lbrace(t):
    r'\{'
    t.type = '{'  # 토큰 타입을 매칭되는 Literal로 설정합니다. (Literal이라면 꼭 필요합니다.)
    return t

 

에러는 t_error 함수로 다룰 수 있습니다. 

def t_error(t):
    print("옳지 않은 문자 '%s'" % t.value[0])
    t.lexer.skip(1) # 몇개의 문자를 넘어가는 함수입니다.

 

모든 준비가 끝나셨으면 lexer를 만드시면 됩니다.

lexer = lex.lex()

 

이런 지금까지의 준비 코드는 임의의 class 안에 정의하여 사용할 수도 있습니다. 

import ply.lex as lex  
 class MyLexer(object):            
       ...     # 위와 같은 내용들을 적습니다.

       # Build the lexer
       def build(self, **kwargs):
           self.lexer = lex.lex(module=self, **kwargs)

       def test(self, data):
           self.lexer.input(data)  # 데이터를 lexer에 넣는 함수입니다.
           for token in self.lexer.token():  # 토큰들을 확인할 수 있습니다.
               print(token)


 m = MyLexer()
 m.build() 
 m.test("3 + 4")  

 

추가적으로 반복문을 활용해서 분리해낸 토큰들을 얻을 수도 있습니다.

for i in lexer: 
    print(i)

 

 

다음 글로는 Lex와 연계되어 자주 사용되는 Yacc 모듈에 대해 알아보겠습니다.

+ Recent posts