정규식으로 버거울 때, Parser Combinators가 답인 이유

텍스트를 객체 같은 구조화된 데이터로 바꾸는 파싱에서 정규식은 복잡한 작업에서 금세 엉망이 되고, tree-sitter는 과한 경우가 많다. 그 중간 지대를 채우는 것이 Parser Combinators다. 입력 문자열을 토큰과 나머지 문자열의 튜플로 돌려주는 작은 partial parser들을 pSeq, pMap 같은 결합자로 이어 붙이면, ping 출력처럼 복잡한 텍스트도 단계적으로 소비하며 구조화할 수 있다. 저자는 이 기법을 모르는 사람이 많다며 직접 소개한다.
정규식은 복잡한 작업에서 엉망이 되고, tree-sitter는 종종 과하다. 이 중간 지대에는 parser combinators가 딱 맞는다.
HN 토론
88- ArneCode
애니메이션이 정말 마음에 들어요. 무슨 일이 일어나는지 직관적으로 이해할 수 있게 해주네요!
파서 컴비네이터의 단점이라고 생각하는 것 중 하나는, 예를 들어 (별도의 문법 파일에서 파서를 생성하는) 파서 생성기 라이브러리와 비교했을 때, 매핑/언래핑 코드를 많이 추가해야 한다는 점입니다. 제 생각에는 이 때문에 파서를 읽기가 더 어려워집니다.
이를 개선하려는 시도로, 저는 Rust로 파서 컴비네이터 라이브러리를 만들어 왔습니다.
여기에는 두 가지 추가 컴비네이터(capture와 bind)가 있는데, 파서의 결과를 버킷에 저장하고(bind) 나중에 이 버킷에 저장된 값을 사용해 결과를 구성(capture)할 수 있습니다.
이를 통해 언래핑/매핑 코드를 없애고 문법과 결과 구성을 분리할 수 있습니다.
시간이 되신다면, 이에 대한 피드백을 정말 감사히 받겠습니다!
여기에서 찾아보실 수 있습니다: https://github.com/ArneCode/marser
- marginalia_nu
왜 이걸 한 페이지에 담을 수 없는지 이해가 안 됩니다. 슬라이드쇼 형식이라 새로운 정보를 얻을 때 이전 "챕터"를 다시 참조하기가 정말 어렵네요. 검색도 안 되고요.
이상한 미적 이유 때문에 디지털 매체에는 존재하지 않는 종이 매체의 한계를 불필요하게 다시 도입하는 것 같습니다?
- akst
정말 멋진 웹사이트이고 이 주제를 사랑합니다.
하지만 작은 화면에서 이 페이지를 테스트해 보시는 게 좋을 것 같습니다. 관심 있는 콘텐츠에 할당된 화면 공간이 매우 제한적이거든요. 오른쪽 상단 절반은 여백이 많고, 스크롤 가능한 콘텐츠는 기본적으로 사용 가능한 화면 공간의 1/3이고, 왼쪽도 또 다른 1/3을 차지합니다.
웹사이트는 당신의 비전이니까, 이해하고 UI로 뭔가 다른 시도를 하시는 것도 존중합니다. 하지만 여기서 목표가 아이디어를 전달하는 것이라면, 독자로서 콘텐츠에 더 많은 화면 공간이 있으면 좋겠습니다. 이 특정 글을 보려고 링크를 클릭했을 때, 다른 글들에는 관심이 없거든요 (적어도 일부러 읽으러 온 글을 다 읽기 전까지는요).
참고로, 지금 MacBook Air를 사용 중입니다.
- BoppreH
Python을 사용 중이고 문제가 단일 정규식으로 처리하기엔 너무 크지만 기사에 나온 것 같은 본격적인 파서를 쓰기엔 너무 작다면, 내장된 re.Scanner[0]를 추천합니다. `re` 모듈에 있는 정규식 기반 토크나이저[1]인데(즉, 재귀적 파싱은 없음), 어떤 이유에서인지 공식 문서 어디에도 완전히 빠져 있습니다.
각 토큰 유형에 대한 패턴과 각 매치를 처리할 함수를 주면, 처리된 토큰 목록을 돌려받습니다.
중요한 점은, 목록을 한 번에 처리하고 매치가 연속되도록 보장한다는 것입니다. 캡처 그룹이 있는 순진한 `re.findall`은 매치되지 않은 문자를 무시하죠. 또한 실행 중인 스캐너에 대한 참조도 얻을 수 있어서, 오류 보고를 위해 매치 위치를 기록할 수 있습니다.
import re
# 스캐너를 초기화합니다.
scanner = re.Scanner([
(r"\d+", lambda scanner, token: ('COUNT', int(token))),
(r"\w+", lambda scanner, token: ('ANIMAL', token.removesuffix('s'))),
(r"[,. ]", None), # None -> 토큰 건너뛰기.
])
# 샘플 문자열을 스캔합니다.
results, remainder = scanner.scan("45 pigeons, 23 cows, 11 spiders.")
# 전체를 파싱했는지 확인합니다.
assert not remainder
print(results)
# [('COUNT', 45),
# ('ANIMAL', 'pigeon'),
# ('COUNT', 23),
# ('ANIMAL', 'cow'),
# ('COUNT', 11),
# ('ANIMAL', 'spider')]
[0]: https://stackoverflow.com/a/693818/252218
[1]: https://en.wikipedia.org/wiki/Lexical_analysis#Tokenization
- boomlinde
첫 번째 주소는 호스트 이름일 수도 있으므로 IP 주소로 파싱하지 않는 것이 좋을 것 같습니다. 사소한 지적처럼 보일 수 있지만, 둘 이상의 종류 중 하나를 예상할 때 어떻게 해야 하는지 보여줄 기회가 생깁니다.
본문에 할당된 영역이 제한적이라 기사를 읽기가 다소 어려웠습니다. 챕터가 짧음에도 불구하고 여러 페이지를 스크롤해야 했습니다.