파이썬 Lark를 이용하여 LaTeX 문법 파싱하기 (1)
파서는 무엇인가?
파서는 데이터 스트림을 읽고 규칙에 따라 토큰으로 분해하는 도구입니다.
이러한 규칙의 집합을 문법(grammar)이라고 합니다.
프로그래밍 언어의 경우, 파서는 코드의 각 줄을 읽어 들이고 이를 토큰으로 분해하여 그 의미를 파악합니다.
예를 들어, A = 14라는 코드를 파싱하면 다음과 같은 토큰으로 분해됩니다:
- A: 식별자(identifier)
- =: 등호(equal sign)
- 14: 숫자(number)
이렇게 분해된 토큰을 기반으로 프로그램은 해당 코드의 의미를 이해하고 실행할 수 있게 됩니다.
파서의 활용 분야
파서는 단순히 컴파일러를 작성하는 데만 사용되는 것이 아닙니다.
데이터의 구조를 파악하고 필요한 정보를 추출해야 하는 다양한 상황에서 유용하게 활용됩니다.
- 정규 표현식 대체: 복잡한 패턴 매칭이 필요한 경우 정규 표현식보다 파서가 더 적합할 수 있습니다.
- 도메인 특화 언어 처리: 특정 문제 영역에 맞는 언어를 정의하고 처리할 때 사용됩니다.
- 데이터 구조 생성: 파싱 결과로부터 원하는 데이터 구조를 생성할 수 있습니다.
파서의 활용 예시
- 파이썬 코드 바이트코드 변환: 소스 코드를 바이트코드로 변환하여 실행합니다.
- 계산기 구현: 수식을 입력받아 계산 결과를 출력합니다.
- 그래프 데이터 처리: 그래프 데이터를 파싱하여 시각화하거나 분석합니다.
- 모스 부호 해석: 점과 대시로 이루어진 모스 부호를 문자로 변환합니다.
- 음성 합성: 텍스트를 파싱하여 음성으로 변환합니다.
Lark 소개
https://github.com/lark-parser/lark
[GitHub - lark-parser/lark: Lark is a parsing toolkit for Python, built with a focus on ergonomics, performance and modularity.
Lark is a parsing toolkit for Python, built with a focus on ergonomics, performance and modularity. - lark-parser/lark
github.com](https://github.com/lark-parser/lark)
Lark는 파이썬으로 작성된 강력한 파싱 라이브러리입니다.
다양한 형태의 데이터를 파싱 할 수 있으며, 복잡한 문법도 간단하게 정의할 수 있습니다.
Lark의 주요 특징
- 유연한 문법 정의: 직관적이고 간단한 문법으로 복잡한 언어를 파싱 할 수 있습니다.
- 빠른 성능: 효율적인 파싱 알고리즘을 사용하여 빠른 속도를 자랑합니다.
- 공통 라이브러리 지원: 정수, 공백, 단어 등 일반적인 토큰을 위한 공통 정의를 제공합니다.
- 트리 변환 지원: 파싱 결과로 생성된 트리를 쉽게 변환하고 조작할 수 있습니다.
Lark를 사용한 데이터 파싱 기본 과정
- 문법 정의: 파싱할 데이터에 대한 문법을 작성합니다.
- 파서 생성: Lark 라이브러리를 사용하여 파서를 생성합니다.
- 입력 파싱: 파서에 데이터를 입력하여 파싱 트리를 얻습니다.
- 트리 변환: 파싱 트리를 원하는 형태로 변환하거나 결과를 도출합니다.
문법 정의하기
파서를 사용하려면 먼저 파싱 할 데이터의 구조를 정의하는 문법이 필요합니다.
예를 들어, 간단한 덧셈 표현식 45 + 33을 파싱 하기 위한 문법은 다음과 같이 작성할 수 있습니다.
1
2
3
4
5
6
?sum: addend "+" addend
addend: INT
%import common.INT
%import common.WS
%ignore WS
문법의 구성 요소
- 규칙(Rule): 파싱 대상의 구조를 정의하며, 소문자로 시작합니다.
- 터미널(Terminal): 더 이상 분해되지 않는 기본 요소로, 대문자로 시작합니다.
- 공통 라이브러리 사용: %import common.INT와 같이 Lark에서 제공하는 기본 정의를 가져옵니다.
- 공백 무시: %ignore WS를 사용하여 공백을 무시하도록 설정합니다.
파싱 트리 생성 및 확인
파서를 사용하여 입력 데이터를 파싱 하면 파싱 트리가 생성됩니다.
이 트리는 입력 데이터의 구조를 계층적으로 나타냅니다.
파싱 트리 확인 방법
- 트리 출력: print(tree)를 사용하여 트리의 구조를 텍스트로 확인합니다.
- 트리 시각화: tree.pretty()를 사용하여 계층적으로 트리를 출력합니다.
- 그래프 생성: tree.draw()를 사용하여 트리를 그래프로 시각화합니다.
예시 코드
1
2
3
4
5
6
7
8
from lark import Lark
parser = Lark(grammar, start='sum')
tree = parser.parse("45 + 33")
print(tree) # 트리 객체 출력
print(tree.pretty()) # 계층적 트리 출력
tree.draw() # 그래프 시각화
트리 변환 및 계산 수행
파싱 트리는 그대로 사용하기보다는 원하는 결과를 얻기 위해 변환이 필요합니다.
이를 위해 Lark의 Transformer 클래스를 사용합니다.
Transformer 클래스 사용
Transformer를 상속하여 각 규칙에 해당하는 메서드를 정의합니다.
이 메서드들은 파싱 트리의 각 노드에서 호출되며, 반환값은 부모 노드로 전달됩니다.
예시 코드
1
2
3
4
5
6
7
8
from lark import Transformer
class CalculateTransformer(Transformer):
def addend(self, items):
return int(items[0])
def sum(self, items):
return items[0] + items[1]
사용 방법
1
2
3
transformer = CalculateTransformer()
result = transformer.transform(tree)
print(result) # 출력: 78
예제: 덧셈 계산기 구현
위에서 정의한 문법과 Transformer를 사용하여 간단한 덧셈 계산기를 구현할 수 있습니다.
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from lark import Lark, Transformer
grammar = """
?sum: addend "+" addend
addend: INT
%import common.INT
%import common.WS
%ignore WS
"""
parser = Lark(grammar, start='sum')
class CalculateTransformer(Transformer):
def addend(self, items):
return int(items[0])
def sum(self, items):
return items[0] + items[1]
expression = "45 + 33"
tree = parser.parse(expression)
result = CalculateTransformer().transform(tree)
print(result) # 출력: 78
복잡한 산술 표현식 파싱
더 복잡한 산술 표현식을 파싱하기 위해 문법을 확장할 수 있습니다.
연산자 우선순위, 괄호 등의 처리를 위해 재귀적인 문법을 작성합니다.
확장된 문법 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
?expr: expr "+" term -> add
| expr "-" term -> sub
| term
?term: term "*" factor -> mul
| term "/" factor -> div
| factor
?factor: "(" expr ")"
| INT
%import common.INT
%import common.WS
%ignore WS
확장된 Transformer 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CalculateTransformer(Transformer):
def add(self, items):
return items[0] + items[1]
def sub(self, items):
return items[0] - items[1]
def mul(self, items):
return items[0] * items[1]
def div(self, items):
return items[0] / items[1]
def INT(self, token):
return int(token)
예시 계산
1
2
3
4
expression = "18 / 6 + (4 * 5)"
tree = parser.parse(expression)
result = CalculateTransformer().transform(tree)
print(result) # 출력: 23.0
파싱 트리 시각화
복잡한 표현식의 파싱 트리를 시각화하여 연산자 우선순위와 계산 순서를 명확하게 파악할 수 있습니다.
1
tree.draw()
파싱 트리를 활용한 추가 작업
파싱 트리는 다양한 방식으로 활용될 수 있습니다.
트리를 변환하거나 탐색하여 원하는 결과를 얻을 수 있습니다.
예시: 후위 표기법 변환
파싱 트리를 순회하여 중위 표기법을 후위 표기법으로 변환할 수 있습니다.
이를 통해 스택 기반의 계산을 수행할 수 있습니다.
예시: 모스 부호 해석
Lark를 사용하여 모스 부호를 파싱하고 문자로 변환하는 작업도 가능합니다.
점(.)과 대시(-)로 이루어진 모스 부호를 규칙에 따라 해석합니다.
연습 문제
- 빈칸 채우기: Lark에서 문법을 정의할 때, 규칙은 ______로 시작하고, 터미널은 ______로 시작합니다.
답변: 규칙은 소문자로 시작하고, 터미널은 대문자로 시작합니다. - 확인 문제: Transformer 클래스에서 각 메서드는 어떤 역할을 하나요?
a) 파싱 트리를 생성한다.
b) 파싱 트리의 노드를 변환한다.
c) 입력 데이터를 토큰으로 분해한다.
답변: b) 파싱 트리의 노드를 변환한다. - 실습 문제: 다음 산술 표현식 3 + 4 * 2 / (1 - 5) ** 2를 계산하는 파서를 Lark를 사용하여 작성해 보세요.