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”)
match
와 search
의 경우에는 매치된 경우 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 집어넣는 거 다들 귀찮잖아요?
이렇게 날 잡고 보니 정규식 사실 뭐 별 거 없고 기본적으로는 패턴을 ‘매치’ 시키는 것 뿐이라는 걸 깨달았다. 모쪼록 나처럼 정규식에 이상한 반감.. 공포 같은 걸 갖고 있던 분들이 이 포스트로 도움이 되었으면 한다.
아래 링크를 참조했다.