인문주의 사피엔스

Flutter/Dart에서 정규 표현식(Regular Expression) 사용하기 본문

프로그래밍/일반

Flutter/Dart에서 정규 표현식(Regular Expression) 사용하기

인문주 2022. 5. 19. 20:22
반응형

정규 표현식은 문자열을 정해진 규칙에 따라 표현한 것을 뜻하며 다음 요소들을 조합하여 구성됩니다.

 

  • 일반 문자(character) a-z A-Z 0-9
  • 메타 문자(metacharacter) ., \d, \D, \s, \S, \w, \W
  • 연산자(operator) +, *, ?, |, ^

 

정규 표현식을 제대로 사용하면 문자열과 관련된 다양한 작업을 아주 효율적으로 처리할 수 있습니다. 

 

단일 문자 검색하기

정규 표현식으로 할 수 있는 가장 단순한 작업은 단일 문자 검색입니다. 예를 들어 어떤 문자열에 a, 2, @ 같은 단일 문자가 포함되어 있는지의 여부를 확인하는 것입니다. 아래 Dart 코드는 문자열 ‘2@a’에 포함된 문자 ‘a’를 검색하는 예제입니다. 이 경우에 문자 'a'는 그 자체로 가장 단순한 정규 표현식이 됩니다.

 

void testRegExp() {
  void match(String string, String regex) {
    var text = '$string: $regex => ';
    for (final match in RegExp(regex).allMatches(string)) {
      text = ‘$text(${match[0]}) ';
    }
    print(text);
  }
  match(‘2@a', r'a');
}

 

 

match 함수의 입력 인자는 두 개입니다. 검색 대상 문자열 '2@a' 그리고 정규 표현식 r'a'입니다. 여기서 r'a' 맨앞에 붙은 r은 'raw'를 의미합니다. 'r'을 붙인 문자열은 '있는 그대로의 날것'으로 취급됩니다. '날것'으로 취급되면 백슬래시(\)에 의한 회피(escape) 기능이 작동하지 않습니다. 예를 들어 r'a\nb'를 print하면 다음과 같이 한 줄로 출력됩니다.

 

print(r'a\nb')

a\nb

 

하지만 r을 제거하면 백슬래시(\)에 의한 회피 기능이 작동됩니다. 백슬래시는 알파벳 n의 문자로서의 기능을 회피하고 '새 줄'이라는 기능을 부여합니다. 그래서 'a\nb'를 print하면 다음과 같이 두 줄로 출력됩니다.

 

print('a\nb')

a

b

 

match 함수 안에 있는 RegExp는 정규 표현식을 나타내는 Dart 클래스입니다. RegExp의 멤버 함수인 allMatches는 정규 표현식으로 찾아낸 정보를 담은 클래스(RegExpMatch)의 목록을 반환합니다. 위 코드를 실행한 결과는 다음과 같습니다.

 

2@a: a => (a)

 

RegExp 클래스에 대한 상세한 내용은 다음의 공식 웹사이트에서 확인할 수 있습니다.

 

RegExp class - dart:core library - Dart API

 

RegExp class - dart:core library - Dart API

A regular expression pattern. Regular expressions (abbreviated as regex or regexp) consist of a sequence of characters that specify a match-checking algorithm for text inputs. Applying a regexp to an input text results either in the regexp matching, or acc

api.dart.dev

 

정규 표현식은 다양한 프로그래밍 언어에서 같은 방식으로 사용됩니다. 이 포스팅과 같은 내용을 Python으로 설명한 다음 포스팅에서도 그 사실을 확인할 수 있습니다.

 

Python에서 정규 표현식(Regular Expression) 사용하기

 

Python에서 정규 표현식(Regular Expression) 사용하기

정규 표현식은 문자열을 정해진 규칙에 따라 표현한 것을 뜻하며 다음 요소들을 조합하여 구성됩니다. 일반 문자(character) a-z A-Z 0-9 메타 문자(metacharacter) . \d \D \s \S \w \W 연산자(operator) + * ? ^ 정

theoryof0.tistory.com

 

특정 용도의 특수 문자(special character)

점(.), 목록([]), 위치 앵커(^$), 출현 표시자(+*?{}), 괄호(()), OR 연산자(|), 백슬래시(\) 같은 특수 문자들이 정규 표현식에 포함되면 특정 용도로 지정되어 사용됩니다. 때때로 그런 특수 문자들을 일반 문자처럼 취급해야 하는 경우가 생깁니다. 그럴 때는 특정 용도를 회피(escape)해야 합니다.

 

회피 문자열(escape sequence)

어떤 문자의 의미나 용도를 회피하려면 그 앞에 백슬래시(\)를 붙여야 합니다. 백슬래시를 붙인 문자는 원래의 의미나 용도가 회피 또는 무효화됩니다. 예를 들어 앞에 백슬래시를 붙이면 문자 n은 알파벳으로서의 의미가 ‘새 줄’이라는 의미로 바뀝니다. 특수 문자의 경우도 마찬가지입니다. 예를 들어 앞에 백슬래시를 붙이면 연산자 +는 연산자로서의 의미가 사라지고 일반 문자처럼 취급됩니다.

 

다음은 문자열 ‘a+b'에서 문자 '+'를 검색하는 예제입니다.

 

match('a+b', r'\+');
a+b: \+ => (+)

 

그런데 '+'는 정규 표현식의 연산자로서 용도가 지정된 문자입니다. 하지만 지금 필요한 것은 연산자가 아니라 일반 문자로서의 '+'입니다. '+'를 일반 문자로 취급하려면 연산자라는 원래 용도를 회피해야 합니다. 용도를 회피하려면 '+' 앞에 백슬래시를 붙여야 합니다. 따라서 정규 표현식은 '\+'가 됩니다.

 

하지만 여기서 '\+'를 그대로 사용하면 결과가 제대로 출력되지 않습니다. 조금 헷갈리지만 단순한 이유가 있습니다. 백슬래시 자체의 회피 기능이 아직 살아있기 때문입니다. 해결책은 다음과 같이 두 가지입니다.

 

  • '\\+' (백슬래시의 회피 기능 자체를 회피)
  • r'\+' (맨앞에 r을 붙여 문자열을 '날것 그대로' 사용)

 

문자열 검색하기

정규 표현식을 사용하면 단일 문자뿐만 아니라 문자열을 검색할 수도 있습니다. 다음은 문자열 ‘Hello World’로부터 문자열 ‘Hello’를 검색하는 예제입니다.

 

match('Hello World', r'Hello');
Hello World: Hello => (Hello)

 

논리합(OR) 연산자(|)

정규 표현식에 논리합 연산자를 사용하면 다음과 같이 한번에 다수의 문자열을 검색할 수도 있습니다.

 

match('Get on bus or taxi at 12', r'bus|taxi|12');
Get on bus or taxi at 12: bus|taxi|12 => (bus) (taxi) (12)

 

괄호 목록

정규 표현식에 괄호([])를 사용하여 목록을 만들면 한번에 다수의 문자들을 검색할 수 있습니다. 괄호 안의 문자들은 문자열이 아닌 개별 문자로 취급됩니다. 다음은 입력 문자열로부터 a, b, c, d를 검색하는 예제입니다.

 

match('Get on bus or taxi at 12', r'[abcd]');
Get on bus or taxi at 12: [abcd] => (b) (a) (a)

 

괄호 목록 맨앞에 ^를 붙이면 논리 부정(NOT)이 적용됩니다. 다음은 입력 문자열로부터 a, b, c, d가 아닌 문자들을 검색하는 예제입니다.

 

match('Get on bus or taxi at 12', r'[^abcd]');
Get on bus or taxi at 12: [^abcd] => (G) (e) (t) ( ) (o) (n) ( ) (u) (s) ( ) (o) (r) ( ) (t) (x) (i) ( ) (t) ( ) (1) (2)

 

괄호 목록에 모든 문자들을 열거하는 대신 -를 사용하여 범위를 지정할 수도 있습니다.

 

match('Get on bus or taxi at 12', r'[a-d]');
Get on bus or taxi at 12: [a-d] => (b) (a) (a)

 

match('Get on bus or taxi at 12', r'[^a-d]');
Get on bus or taxi at 12: [^a-d] => (G) (e) (t) ( ) (o) (n) ( ) (u) (s) ( ) (o) (r) ( ) (t) (x) (i) ( ) (t) ( ) (1) (2)

 

괄호 목록에 포함된 메타 문자나 특수 문자는 일반 문자처럼 취급됩니다. 따라서 백슬래시를 사용해 회피할 필요는 없습니다. 단 아래의 네 가지 경우(^, -, ], \)는 예외입니다.

 

  • ] 를 포함하려면 \] 처럼 회피합니다. 또는 목록의 맨앞에 둡니다.
  • ^ 를 포함하려면 \^ 처럼 회피합니다. 또는 목록의 맨앞을 제외한 위치에 둡니다.
  • - 를 포함하려면 \- 처럼 회피합니다. 또는 목록의 맨뒤에 둡니다.
  • \ 를 포함하려면 \\ 처럼 회피합니다.

 

다음은 문자열 ‘[a-b]'로부터 [, ], -를 검색한 결과입니다.

 

match('[a-b]', r'[[\]-]');
[a-b]: [[\]-] => ([) (-) (])

 

메타 문자(metacharacter) 

메타 문자는 문자의 범주를 기술하는 문자로서 점(.), \w, \d, \s 등이 있습니다.

 

점(.)은 단일 문자를 나타냅니다. 다음은 정규 표현식 r’the..'을 사용하여 입력 문자열로부터 these, the t, there를 검색한 결과입니다.

 

match('these is the thing there', r'the..');
these is the thing there: the.. => (these) (the t) (there)

 

\w는 단일 문자, 숫자, 밑줄을 나타냅니다. 다음은 정규 표현식 r’\w'을 사용하여 입력 문자열 '<Ab_1>'로부터 A, b, _, 1를 검색한 결과입니다. 정규 표현식 [a-zA-Z0-9_]를 사용한 경우와 같습니다.

 

match('<Ab_1>', r'\w');
<Ab_1>: \w => (A) (b) (_) (1)

 

\W는 \w와 정반대입니다. 정규 표현식 [^a-zA-Z0-9_]를 사용한 경우와 같습니다.

 

match('<Ab_1>', r'\W');
<Ab_1>: \W => (<) (>)

 

\d는 단일 숫자를 나타냅니다. 정규 표현식 [0-9]를 사용한 경우와 같습니다.

 

match('<Ab_1>', r’\d');
<Ab_1>: \d => (1)

 

\D는 \d와 정반대입니다. 정규 표현식 [^0-9]를 사용한 경우와 같습니다.

 

match('<Ab_1>', r'\D');
<Ab_1>: \D => (<) (A) (b) (_) (>)

 

\s는 단일 공백을 나타냅니다. 정규 표현식 [ \t\n\r\f]를 사용한 경우와 같습니다.

 

match('(Ab 1)', r’\s');
<Ab 1>: \s => ( )

 

\S는 \s와 정반대입니다. 정규 표현식 [^ \t\n\r\f]를 사용한 경우와 같습니다.

 

match('(Ab 1)', r'\S');
<Ab 1>: \S => (<) (A) (b) (1) (>)

 

출현 표시자(occurrence indicator) 

출현 표시자 또는 반복 연산자(repetition operator)는 문자의 출현이나 반복 횟수를 다룰 때 사용됩니다.

 

+는 그 앞에 놓이는 문자가 1회 이상 출현하는 것을 나타냅니다. 다음은 숫자가 1회 이상 출현하는 경우를 검색한 결과입니다.

 

match('a123b', r'[0-9]+');
a123b: [0-9]+ => (123)

 

*는 그 앞에 놓이는 문자가 0회 이상 출현하는 것을 나타냅니다. 다음은 숫자가 0회 또는 그 이상 출현하는 경우를 검색한 결과입니다.

 

match('a123b', r'[0-9]*');
a123b: [0-9]* => () (123) () ()

 

?는 그 앞에 놓이는 문자가 0회 또는 1회 출현하는 것을 나타냅니다. 다음은 + 또는 -가 0회 또는 1회 출현하는 경우를 검색한 결과입니다.

 

match('+1-2', r'[+-]?');
+1-2: [+-]? => (+) () (-) () ()

 

{m}는 그 앞에 놓이는 문자가 정확히 m회 출현하는 것을 나타냅니다. 다음은 숫자가 2회 출현하는 경우를 검색한 결과입니다.

 

match('1.2345', r'[0-9]{2}');
1.2345: [0-9]{2} => (23) (45)

 

{m,}는 그 앞에 놓이는 문자가 m회 이상 출현하는 것을 나타냅니다. 다음은 숫자가 2회 이상 출현하는 경우를 검색한 결과입니다.

 

match('1.2345', r'[0-9]{2,}');
1.2345: [0-9]{2,} => (2345)

 

{m,n}는 그 앞에 놓이는 문자가 m회에서 n회까지 출현하는 것을 나타냅니다. 다음은 숫자가 2회 이상 3회 이하 출현하는 경우를 검색한 결과입니다.

 

match('1.2345', r’[0-9]{2,3}');
1.2345: [0-9]{2,3} => (234)

 

*, +, ?, {m,n} 등의 연산자는 긴 문자열을 우선적으로 검색합니다. 예를 들어 정규 표현식 ab{2,4}의 검색 우선 순위는 abbbb, abbb, abb 순입니다. 다음은 긴 문자열을 우선적으로 검색한 결과입니다.

 

match('The <a>first</a> and <a>second</a> words', r’<a>.*</a>');
The <a>first</a> and <a>second</a> words: <a>.*</a> => (<a>first</a> and <a>second</a>)

 

그런데 *, +, ?, {m,n}의 뒤에 ?를 하나 더 붙이면 우선 순위가 뒤바뀌게 됩니다. 다음은 짧은 문자열을 우선적으로 검색한 결과입니다.

 

match('The <a>first</a> and <a>second</a> words', r'<a>.*?</a>');
The <a>first</a> and <a>second</a> words: <a>.*?</a> => (<a>first</a>) (<a>second</a>)

 

둥근 괄호

표현식을 둥근 괄호로 둘러싸면 하나의 그룹이 됩니다. 다음은 abc를 그룹으로 만들어 검색한 결과입니다.

 

match(‘abcabccc', r’(abc)+’);
abcabccc: (abc)+ => (abcabc)

 

다음은 abc를 그룹으로 만들지 않은 경우의 결과입니다.

 

match(‘abcabccc', r’abc+’);
abcabccc: abc+ => (abc) (abccc)

 

둥근 괄호를 사용해 그룹을 만들 때마다 각 그룹에 대한 참조 변수를 사용할 수 있습니다.

 

(\S+)\s+(\S+) 

 

위의 정규 표현식은 하나 이상의 공백으로 분리된 두 개의 단어를 나타냅니다. 그리고 둥근 괄호로 묶인 두 개의 그룹은 각각의 참조 변수 $1과 $2를 생성시킵니다. 참조 변수의 표시 방법은 프로그래밍 언어마다 다릅니다. Dart의 경우에는 Match 클래스가 사용됩니다. 다음은 두 개의 단어를 교환하는 Dart 코드입니다.

 

swapWords(String words) => words.replaceAllMapped(
  RegExp(r'(\S+)\s+(\S+)'), (Match m) => '${m[2]} ${m[1]}');
print(swapWords('I     have    a     dream’));

 

결과는 다음과 같습니다.

 

have I    dream a

 

위치 앵커(position anchor)

위치 앵커는 한 줄의 시작과 끝, 단어의 경계 등 위치를 나타낼 때 사용됩니다.

 

  • ^는 한 줄의 시작을 나타내고 $는 한 줄의 끝을 나타냅니다.
  • \b는 단어의 경계를 나타내고 \B는 단어의 경계가 아닌 부분을 나타냅니다.
  • \<는 단어의 시작을 나타내고 \>는 단어의 끝을 나타냅니다.
  • \A는 입력의 시작을 나타내고 \Z는 입력의 끝을 나타냅니다.

 

반응형
Comments