애증의 정규식... 1탄
이 글은 정규식 삽질의 기록이며, 나중에 내가 같은 패턴을 쓸 일이 있을 때 찾아보기 위함이다. 매번 느끼지만, 정규식은 문제마다 솔루션이 다르게 존재하는 느낌이다(...). 100개의 문제가 있으면 솔루션도 100개인 느낌이랄까.
요약
• regexp는 모든 occurrence를 찾는다. 첫 번째 occurrence만 찾으려면 'once' 옵션을 사용한다.
idx = regexp(str, pattern, 'once');
• regexp는 기본적으로 인덱스를 반환한다. 문자열을 받고 싶으면 'tokens' 옵션을 사용한다. 이때 2, 3번째 반환값은 startIdx와 endIdx이다. 매칭이 N개이면 startIndx와 endIdx의 길이도 N이다.
[matches, startIdx, endIdx] = regexp(str, pattern, 'tokens');
• '>[뭐뭐]' 패턴을 찾고 싶으면 아래처럼 쓴다. (.*?)는 non-greedy 매칭이다. (.*)는 greedy 매칭이다. 이때 반환하는 문자열에 괄호 [ ]는 포함되지 않는다.
regexp(str, '>\[(.*?)\]', 'tokens')
• 패턴에 괄호가 포함되어 있으면 \(, \[처럼 역슬래시를 써야 한다.
• '(#뭐뭐)' 패턴을 찾고 싶으면 아래와 같이 쓴다. #이 괄호 안에 있으면 '#뭐뭐'가 반환되고, #이 괄호 밖에 있으면 '뭐뭐'만 반환된다.
regexp(str, '\((#.*?)\)', 'tokens')
• replace를 쓰면 길이가 다른 문자열 대체가 가능하다.
위키독스에 매트랩 책을 쓰고 있다. 그런데 위키독스의 마크다운 에디터는 여러모로 불편하다. Preview pane이 없던 문제는 StackEdit로 해결되긴 했는데, StackEdit는 그것대로 또 자잘한 버그가 있다. 무엇보다 스페이스바가 간혹 안 눌리는 문제가 제일 심각하다. 그래서 생각해 낸 꼼수가...
구글 코랩에 쓰기이다.(...)
파이썬 개발환경이기 전에 훌륭한 마크다운 에디터이며, 무엇보다 목차 셀이 자동생성된다!
이제 할일은
1. 코랩 텍스트 셀에 내용을 쓰기
2. py 파일로 다운로드 받기 (모든 내용이 """ """로 감싸진 comment로 처리됨)
3. 적절히 수정하고 위키독스에 올리기
이다.
그런데 '적절히 수정'이 생각보다 좀 번거로웠다.
1. 목차가 시작되는 지점을 찾아서, 그 이전을 모두 없앤다.
솔루션: 목차는 '>'로 시작한다. 첫 번째 occurrence만 찾아야 하므로 정규식에서 'once' 옵션을 사용해야 한다.
idx = regexp(txt, '>', 'once');
txt(1:idx-1) = [];
2. 목차에 번호를 붙인다.
왜인지 모르겠으나, 코랩 자동생성 목차의 heading level 1(#으로 시작하는 부분)은 heading 뒤에 숫자를 써도 목차에는 번호가 자동으로 붙지 않는다. 그래서 이렇게 생긴 목차를,
>[행렬끼리 더하고 빼기](#scrollTo=s_q7lVr6e4l_)
>[행렬에 스칼라를 곱하거나 나누기](#scrollTo=mFwtPwPgjJoT)
>[행렬 간의 곱셈: 행렬곱](#scrollTo=nGg8_dctmvPv)
이렇게 바꿔야 한다.
>[1. 행렬끼리 더하고 빼기](#scrollTo=s_q7lVr6e4l_)
>[2. 행렬에 스칼라를 곱하거나 나누기](#scrollTo=mFwtPwPgjJoT)
>[3. 행렬 간의 곱셈: 행렬곱](#scrollTo=nGg8_dctmvPv)
솔루션: 정규식으로 '>[뭐뭐]' 패턴을 찾아서 일련번호를 추가한다. 원래 regexp는 인덱스를 반환하는데, 'tokens' 옵션을 쓰면 찾은 문자열 자체를 셀 배열로 반환한다. 이 경우 2, 3번째 반환값이 startIdx와 endIdx이다. 괄호(소, 중, 대 모두)는 정규식 문법의 일부이므로, 문자로서 괄호를 찾으려면 \(, \{, \[처럼 써야 한다. 인덱스를 사용하는 것보다 replace가 더 편해보여서 아래와 같이 작성했다.
matches = regexp(txt, '>\[(.*?)\]', 'tokens');
for i = 1:length(matches)
txt = replace(txt, ...
sprintf('>[%s]', matches{i}{1}), ...
sprintf('>[%d. %s]', i, matches{i}{1}));
end
미해결1: 이 방식으로 찾으면 '>[뭐뭐]'가 반환되지 않고 '뭐뭐'만 반환된다. 그래서 replace에 해당 부분을 추가해줬다. 더 좋은 방법이 있으면 좋겠다.
미해결2: 목차가 아닌데 '>[뭐뭐]' 패턴이 들어갈 일이 있을까? 있으면 그때 생각하자.
3. 목차의 링크를 수정한다.
코랩 목차의 링크는 heading이 붙어있는 부분을 가리킨다. 각 heading마다 고유링크가 있다.
>[1. 행렬끼리 더하고 빼기](#scrollTo=s_q7lVr6e4l_)
>[2. 행렬에 스칼라를 곱하거나 나누기](#scrollTo=mFwtPwPgjJoT)
>[3. 행렬 간의 곱셈: 행렬곱](#scrollTo=nGg8_dctmvPv)
위키독스에서는 당연히 이 링크가 동작하지 않는다. 대신 heading 뒤에 쓴 텍스트가 그대로 고유링크가 된다. 예를 들어,
### linspace
라는 heading을 쓰면, 페이지 주소 뒤에 '#linspace'를 붙이면 해당 heading으로 이동한다. 그런데 문제는, heading 뒤 텍스트에 한글이 들어가 있으면 한글만 빼고 링크가 만들어진다. 일단 최근에 작업한 문서는 목차에 한글밖에 없어서 맨 앞에 숫자를 붙여서 해결했다. 이제 페이지 주소 뒤에 '#1'을 붙이면 첫 번째 heading으로 이동한다.
솔루션: 정규식으로 '(#뭐뭐)' 패턴을 찾아서 '(#숫자)'로 바꾼다. 조심할 것이 있다. '(.*?)'는 non-greedy 매칭이다. 해당 패턴 중 가장 짧은 것을 찾는다. 반대로 '(.*)'는 greedy 매칭이다. 그런데 '#(.*?)'라고 쓰면 #이 포함되지 않은 문자열을 반환하고, '(#.*?)'라고 써야 #이 포함된 문자열을 반환한다. 즉, 괄호로 감싸지면서 #으로 시작하는 부분을 찾되, 반환하는 것은 ( ) 안의 패턴만 반환한다. 나는 분명히 이것을 까먹고 나중에 이 글을 다시 읽게 될 것이다.
matches = regexp(txt, '\((#.*?)\)', 'tokens');
for i=1:length(matches)
txt = replace(txt, matches{i}, sprintf('#%d', i));
end
미해결1. 여전히 같은 문제이다. 나는 '#뭐뭐'가 아니라 '(#뭐뭐)'를 반환받고 싶다.
미해결2. 목차가 아닌데 '(#뭐뭐)'라고 쓸 일이 정말 절대 없을까? 분명히 언젠가 나올 것 같다. 그때 생각하자.
미해결3. 처음엔 분명히 링크가 #updateTitle로 시작했는데, 어느 순간 갑자기 #scrollTo로 바뀌었다. 뭔가 규칙을 찾아서 그에 맞게 수정해야 한다.
미해결4. 목차에 영어와 한글이 같이 있으면? 이미 그런 링크를 만든 적이 있으므로, 반드시 언젠가 해결해야 할 문제이고, 근시일 내에 나타날 것이다. 이것도 그때 가서 생각하자.
4. \\를 \\\\로 바꾼다.
코랩의 수식은 LaTeX이다. 위키독스의 수식은 MathJax이다. MathJax에서는 줄넘김을 할 때 \\가 아니라 \\\\를 써야한다. 해당 부분을 모두 찾아서 replace 한다. 설마 수식 아닌데 \\를 쓸 일은 없겠지. (없어라)
txt = replace(txt, '\\', '\\\\');
일단은 여기까지.
어느 정도 (반)자동화 된 것 같다.
나머지는 다음에 생각하자.
아참, 챗gpt는 정규식도 잘 알려준다.