Post

(Regex) JS

JS Regex

regex에서 “또는 or”을 표현하기 위해 [(bar)(foo)]처럼 사용하면 ()로 묶어도 한 char 씩 잘라서 인식하기 때문에 ((bar)|(foo))를 사용해야 한다.

()가 추가되기 때문에 캡쳐링 그룹 인덱스가 틀려질 수 있다는 점에 주의한다.

JS에서 정규 표현식은 보통 같은 문자열에 대해 반복해서 연산을 수행할 때 주목할 만한 성능상의 이점이 있다. 그래서 indexOf를 쓰는 것 보다는 정규 표현식을 쓰는 것이 좋다.

정규식은 inline 보다는 변수에 저장해서(stored) 사용하는 것이 좋고, RegExp 객체를 사용하는 것 보다 literal을 사용하는 것이 좋다. 또한, 반복문 안에 넣으면 계속 컴파일 되는 경우가 있으므로 반복문 외부에 선언한다.

literal
1
2
const re = /ab+c/;

스크립트가 로드되었을 때 정규식의 컴파일을 제공한다. 정규식이 계속 지속 될것이라면 이렇게 사용하는 것이 좋다.

RegExp 객체의 생성자 함수
1
2
const re = new RegExp("ab+c");

정규식의 런타임 컴파일을 제공한다. 정규식 패턴이 바뀔 것이라고 알고 있거나, 패턴을 잘 모르거나, 사용자 입력과 같이 다른 소스로부터 패턴을 얻어 올 때에 생성자 함수를 쓰는 것이 좋은데, stored literal에 비해 많이 느리다. 역슬래시가 literal에서와는 조금 다른 의미로 보통 두 개의 역슬래시를 사용해야 하며 따옴표도 이스케이프시켜야 한다.

capturing group

= bar foo=는 객체가 반환되기는 했으나 length == 0

[a-z]\*가 0개이고 \s가 붙는 것으로 잡혀서 객체가 반환되기는 된다.

=bar=는 객체가 반환되지 않는다. ([a-z]\*)\s가 아예 없는 것으로 잡힌다.

중요한 것은 둘 다 false로 취급된다는 것.

JS는 null, undefined, NaN, 빈 문자열 '', 0, falsefalse로 본다. * 이 외의 값은 모두 참이다. 해서 test()를 사용하지 않아도 if(matches[1])로 검사할 수 있다.

RegExp.$_ / $1…9

RegExp.$\_ : 최근에 탐색한 문자열 전체를 저장하고 있다. RegExp.$1...$9 : 가장 최근에 매치된 capturing group을 저장하고 있다.

test() 를 사용했더라도, group으로 묶었다면 RegExp.$1 ~ $9에 저장된다.

또한 exec()의 반환값 [매칭된 부분 전체, captured group1, captured group2, ...][RegExp.$\_, RegExp.$1, RegExp.$2, ...] 와 동일하다.

그래서 exec()대신 text()를 사용해도 구현상 문제는 없으며 text()를 사용하는 것에 성능 이득을 보려면 캡쳐링 하지 않도록 (?:...)을 지정해야 한다.

match()

capturing group을 사용하지 않는 경우 일반적으로 match()를 사용하는게 훨씬 편하다. match()를 사용할 때 gflag가 없다면 js exec()와 동일하게 동작하게 되므로 match()를 사용하는 의미가 없어진다. 그래서 꼭 patten에 g flag를 설정해야 한다. match()메소드는 js exec()와 달리 전체 문자열에서 매치되는 부분을 한 번에 Array로 반환해 줘서 반복 돌릴 필요가 없다는 장점이 있지만,captured groups을 반환하지 않는다. return type도 Object로 이루어진게 아닌 String들로 이루어진 Array다. 즉 matched substrings를 반환한다. 실제로 match()를 수행하고 RegExp를 조사해 보면 RegExp.$\_는 존재하지만, RegExp.$1...9는 비어있는 string으로 나온다. 그래서 matched substrings을 반환받는 동시에 captured groups도 반환받고 싶다면, exec() 를 반복 호출해야 한다.

exec()

g flag를 설정하는 경우 몇 가지 주의할 점이 있다.

1
2
3
var re = /Hello/g;
var matches = re.exec("Hello world! Hello world2! Hello world3!);

직관적으로 생각해 보면 matches[Hello, Hello, Hello]가 반환될 것 같지만 아니다. 그건 match()를 사용했을 경우다. exec()가 반환하는 값은 항상 [매칭된 부분 전체, captured group1, captured group2, ...] 이다.

뒤에 있는 Hello를 잡고 싶다면, global flag를 그대로 두고 exec()를 연속으로 호출해야 한다. gflag는 다음 패턴 검색을 위해 어디서 부터 패턴 매칭을 수행해야 하는지를 저장(js RegExp.lastIndex)해 두는 역할을 하기 때문에 exec()를 연속으로 호출하면 뒤에 있는 Hello가 차례로 검출된다.

이상하게 동작하는 경우

이상하게 동작하는 경우는 보통 다음 두 가지 이유로 일어난다.

#1exec()를 호출하는 사이에 문자열이 변경되는 경우

#2exec()를 호출하는 사이에 정규표현식 객체를 재사용하는 경우.

정상적인 예제는 다음과 같다.

#1 각 exec()를 호출하는 사이에 문자열이 변경되는 경우

exec()는 호출될 때 마다 매번 string에 접근하기 때문에 replace등으로 string을 변경하는 경우 주의해야 한다. gflag가 설정되어 있으면 어디까지 읽었는지 index를 저장해 놓았다가, 다음 js exec() 호출 시 거기서부터 읽어나가게 되는데, replace() 시 문자열의 각 문자의 index가 틀어질 수 있기 때문에 (문자열이 당겨지는 등) 조회가 안될 수 있다.

두 번째에 위치한 Hello world3이 매치되지 않았다. 이 경우 gflag가 설정되어 있지 않으면 제대로 매치 되기는 한다.즉, g flag가 없어야 제대로 global match 된다. g flag가 없는 경우 매번 string의 처음부터 매치를 수행하게 되는데 string이 수정되면 수정된 string을 대상으로 매칭을 수행하기 때문에 이전에 매치된 substring이 변경되어 더 이상 매치되지 않기 때문에 그 다음 매치를 찾아주게 되어 제대로 동작하는 것 처럼 보인다. 그러나 이렇게 사용하는건 좋지 못하다.

그래서 문자열을 변경해야 하는 경우, 다음과 같이 원본 문자열을 buffer에 넣어놓고 이를 대상으로 match 검사를 수행하는 것이 좋다.

1
2
3
var match\_buf = line;
var matches = re.exec(match\_buf);

#2 각 exec()를 호출하는 사이에 정규표현식 객체를 재사용하는 경우.

g flag가 있지만, 반복을 거듭해도 첫 번째 매치된 문자열만 계속 매치되며 무한 루프에 빠지게 된다. 이는 re객체를 replace()에서 재사용해서 어디까지 읽었는지 저장해놓은 index가 초기화되어, g flag가 없을 때 처럼 매번 처음부터 다시 매치하기 때문이다.

그래서 re 객체를 재사용하는 것이 아니라, 반환된 matched substring과 captured group을 사용해야 한다.

1
2
line = line.replace(matches[0], "REP"+matches[1]);

This post is licensed under CC BY 4.0 by the author.