Jekyll로 블로그를 옮기면서, 야크 털 깎기와 같은 일을 이것저것 하고 있다. 포스트를 쓰는 데 드는 노력을 최소한으로 하고, 내가 갖고 있는 리소스를 최대한으로 활용하는 게 목표다.

깔끔한 UI에 반해 1년치 구독을 긁어버린 Mac용 노트 앱 Bear를 주 에디터로 사용하기로 했다. Bear는 마크다운 문법을 굉장히 깔끔한 형식으로 지원한다.

이렇게 마크다운 문법 형식은 정확히 보존하면서도 바로바로 예쁘게 렌더링해주는 게 특히 마음에 들었다.

그런데 문제는, 이렇게 작성한 파일을 Jekyll post로 따로 마이그레이션하는 작업이 필요하다는 것이었다. Jekyll 포스트를 작성할 때에는 맨 앞에 Front Matter라는 메타데이터를 붙여 줄 필요가 있다. 태그와 포스트 제목 등을 명시해주는 용도다. 물론 포스트를 작성할 때 직접 front matter를 달아 줄 수도 있지만, 조금 번거롭게 느껴졌다. 귀찮게 느껴지면 내가 포스트를 안 쓸 게 뻔하고.. 애써 만든 블로그가 방치될 걸 생각하니 슬펐다.

다행히 내게 Bear를 영업한 장본인이자 이전에 먼저 Jekyll로 넘어간 @goofcode 님이 front matter를 generate하는 소스를 만들어서 올려주었다. 고맙게도! (github에 있는 소스) 이걸 내가 포스팅하는 스타일에 맞게 약간 수정하기로 했다.

그러다 보니 정규식 쓰는 걸 피할 수 없었는데.. 내게는 이상하게도 줄곧 정규식을 쓰는 것에 대한 심리적 장벽이 있었다. javascript나 golang을 사용할 때에도 최대한 string 라이브러리에서 지원하는 함수로 어찌저찌 잘 넘어가보려고 했던 적이 태반이었다. 이번 기회에 그 장벽을 넘어 보기로 했다.

import re

기본으로 제공하는 re 모듈을 임포트하면 파이썬에서 정규식 쓸 준비는 모두 끝난다.

메타 문자

메타 문자란 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용되는 문자를 말한다.

문자 클래스 [ ]

[와 ] 사이의 문자들 중 하나를 포함하는지 확인할 때 쓰인다.

  • [abc]: a, b, c 중 한 개의 문자를 포함할 때 match
  • [a-zA-Z0-9]: 알파벳 또는 숫자를 포함할 때 match
  • [^0-9]: 숫자를 포함하지 않을 때 match
문자 클래스 동일 표현
\d [0-9]와 동일
\D [^0-9]
\s [ \t\n\r\f\v]
\S [^ \t\n\r\f\v]
\w [a-zA-Z0-9_]
\W [^a-zA-Z0-9_]

Dot(.)

\n 을 제외한 모든 문자와 매치된다. python에서는 정규식 작성 시 옵션으로 re.DOTALL 옵션을 주면 \n 문자와도 매치되게 할 수 있다.

p = re.compile(r'`(.*)`', re.DOTALL)

반복을 표현하는 메타문자

* 메타문자는 0부터, + 메타문자는 최소 1번 이상부터 반복될 때 사용한다.

또한, 반복 횟수를 정확히 제한하고 싶을 때, {m} 의 경우에는 정확히 m번 반복하고, {m, n} 의 경우에는 m번 이상 n번 이하 반복하는 것을 의미한다.

?

있어도 되고 없어도 되고.

ab?c: "abc", "ac" 모두 매치된다.

|

“or”의 의미와 동일하다. 즉 매치되는 경우의 범위가 더 넓어진다.

>>> p = re.compile('Crow|Servo')
>>> m = p.match('CrowHello')
>>> print(m)
<_sre.SRE_Match object; span=(0, 4), match='Crow'>

^

문자열의 맨 처음과 일치. 여러 줄일 때에는 각 라인의 처음과 일치.

확실하지는 않지만, match 메소드의 경우와 search에서 패턴에 ^ 메타 문자를 포함한 경우가 동일한 것 같다.

$

문자열의 맨 끝과 매치.

\A

문자열의 처음과 매치. re.MULTILINE 옵션을 사용할 때에는 ^과 다르게 라인과 상관없이 전체 문자열의 처음하고만 매치된다.

\Z

동일한 방식으로 전체 문자열의 끝과 매치.

\b

단어 구분자(보통은 whitespace로 구분) backspace와 혼동되지 않도록 앞에 r'\bword\b' 와 같이 r을 꼭 붙여주어야 한다.

\B

whitespace로 구분된 단어가 아닌 경우에만 매치된다.

정규식 사용하기

Method 목적
match() 문자열의 처음부터 정규식과 매치되는지 조사한다.
search() 문자열 전체를 검색하여 정규식과 매치되는지 조사한다.
findall() 정규식과 매치되는 모든 substring을 리스트로 리턴한다.
finditer() 정규식과 매치되는 모든 substring을 iterator 객체로 리턴한다.

compile된 패턴 객체를 통해 이 메소드를 호출하는 것이 일반적이지만, 아래와 같이 re 모듈에서 바로 호출해서 컴파일과 위 메소드를 동시에 수행할 수 있다.

m = re.match([a-z]+, python)

matchsearch의 경우에는 매치된 경우 SRE_Match 객체를 리턴하고 그렇지 않을 경우 None을 리턴한다. search는 문자열 전체를 검색해서 가장 먼저 매치된 문자열 하나를 리턴한다는 것이다. 첫 번째 문자열부터 정확히 매치되어야 하는 match와 전체 substring의 리스트를 리턴하는 find 계열 메소드와 이 점에서 구분된다.

Match 객체의 메소드들은 다음과 같다.

Method 목적
group() 매치된 문자열을 리턴한다.
start() 매치된 문자열의 시작 위치를 리턴한다.
end() 매치된 문자열의 끝 위치를 리턴한다.
span() 매치된 문자열의 (시작, 끝)에 해당하는 tuple을 리턴한다.

실제 문제 해결하기: 코드 블록 안의 # 제외

원래 스크립트는 front matter를 생성하고 파일명 포맷을 Jekyll에 맞게 맞춰 주는 등 내가 필요로 하는 기능은 거의 다 갖추고 있었다. (오예!) 그렇지만 코드 블록 내에 있는 # 문자가 태그로 인식되어 삭제되어 버리는 작은 버그가 있었다. 포스트에 코드 블록을 남발하는 내게는 꽤나 큰 문제였고, 코드 블록으로 감싼 부분을 정규식으로 찾아내 제외한 뒤 태그 검색을 수행하기로 했다.

앞에서 익힌 대로,

p = re.compile("`(.*)`", re.DOTALL)
post_file_without_codeblock = p.sub("", post_file)

backquote(`)로 감싼 부분을 찾아내 제거해 주는 작업을 수행했다.

이후, 태그를 검색할 때에는 이렇게 코드 블록을 제외한 부분만을 확인하도록 수정했다.

post_tags = re.findall("#([^#\s]+)", post_file_without_codeblock)

마치며

블로그 만들다가 뜬금없이 정규식 공부를 다시 하게 됐다. 미뤄 놓은 걸 이제야 확실히 되짚고 가니 뿌듯하기는 하다.

물론 그룹과 전방 탐색과 같은 좀더 고급(?) 기법들이 남아있다. 이것들에 대해서도 다시 포스트 쓸 기회가 있을…. 거다. 이 포스트를 쓰다 보니까 Bear에서 마크다운 테이블을 지원하지 않는다는 슬픈 사실을 깨닫고 말았다. 나는 무엇을 위해 스크립트를 붙들고 있던 것일까.. 하고 잠깐 슬퍼했지만 다른 에디터로 작성한 글에도 충분히 적용할 수 있을 것이다. 파일명 바꾸고 front matter 집어넣는 거 다들 귀찮잖아요?

이렇게 날 잡고 보니 정규식 사실 뭐 별 거 없고 기본적으로는 패턴을 ‘매치’ 시키는 것 뿐이라는 걸 깨달았다. 모쪼록 나처럼 정규식에 이상한 반감.. 공포 같은 걸 갖고 있던 분들이 이 포스트로 도움이 되었으면 한다.

아래 링크를 참조했다.