1. 정규표현식
카카오톡은 전화번호, URL, 이메일 주소를 어떻게 찾을까?
- 정규표현식의 목적
- 패턴을이용하여
- 문자 검색 (search)
- 문자 대체 (replace)
- 문자 추출 (extract)
- 패턴을이용하여
- 성능은? 매우 느리다! 하지만 매우 편함…
- 그러나 정규표현식은 주석이나 공백을 허용하지 않고 여러가지 기호를 혼합하여 사용하기 때문에
- 가독성이 좋지 않다는 문제가 있다는 단점이 있다.
1.1 정규식 구성
슬래쉬 문자 두개 사이로 정규식 기호가 들어가는 형태이다. 뒤의 i 는 정규식 플래그이다.
1/regexr/i23/ : 시작기호4regexr : 패턴5i : 플래그
아래 코드들은 모두 JavaScript 코드지만, 대부분의 언어들의 정규식 문법이 비슷해서, 한 언어의 정규식을 잘 익혀두면 다른 언어의 정규식을 배우는데 수월하다.
1// 리터럴 방식2const regex1 = /abc/34// 생성자 방식5const regex2 = new RegExp('abc')6const regex3 = new RegExp(/abc/) // 이렇게 해도 됨
1.2 정규식 메서드
위의 정규표현식을 가지고 이메일이나 전화번호 매칭 필터링을 하기위해선 자바스크립트 정규식 메서드를 이용하여 패턴을 검사하고, 매칭되는 문자열을 추출, 변환한다.
| 메서드 | 의미 |
|---|---|
("문자열").match(/정규표현식/플래그) | ”문자열”에서 “정규표현식”에 매칭되는 항목들을 배열로 반환 |
("문자열").replace(/정규표현식/, "대체문자열") | ”정규표현식”에 매칭되는 항목을 “대체문자열”로 변환 |
("문자열").split(정규표현식) | ”문자열”을 “정규표현식”에 매칭되는 항목으로 쪼개어 배열로 반환 |
(정규표현식).test("문자열") | ”문자열”이 “정규표현식”과 매칭되면 true, 아니면 false반환 |
(정규표현식).exec("문자열") | match메서드와 유사(단, 무조건 첫번째 매칭 결과만 반환) |
1// 정규표현식을 담은 변수2const regex = /apple/ // apple 이라는 단어가 있는지 필터링34// "문자열"이 "정규표현식"과 매칭되면 true, 아니면 false반환5regex.test('Hello banana and apple hahahaha') // true67// "문자열"에서 "정규표현식"에 매칭되는 항목들을 배열로 반환8const txt = 'Hello banana and apple hahahaha'9txt.match(regex) // ['apple']1011// "정규표현식"에 매칭되는 항목을 "대체문자열"로 변환12txt.replace(regex, 'watermelon') // 'Hello banana and watermelon hahahaha'
1.3 정규식 플래그
정규식 플래그는 정규식을 생성할 때 고급 검색을 위한 전역 옵션을 설정할 수 있도록 지원하는 기능이다.
1// flags 에 플래그 문자열이 들어간다.2const flags = 'i'3const regex = new RegExp('abapplec', flags)45// 리터럴로 슬래쉬 문자뒤에 바로 표현이 가능6const regex1 = /apple/i7const regex2 = /apple/gm
| Flag | Meaning | Description |
|---|---|---|
i | Ignore Case | 대소문자를 구별하지 않고 검색한다. |
g | Global | 문자열 내의 모든 패턴을 검색한다. |
m | Multi Line | 문자열의 행이 바뀌더라도 검색을 계속한다. |
s | .(모든 문자 정규식)이 개행 문자 \n도 포함하도록 | |
u | unicode | 유니코드 전체를 지원 |
y | sticky | 문자 내 특정 위치에서 검색을 진행하는 ‘sticky’ 모드를 활성화 |
1.3.1 g - 전역 검색
- 전역 검색 플래그가 없는 경우에는 최초 검색 결과 한번만 반환하는 반면,
- 전역 검색 플래그가 있는 경우에는 모든 검색 결과를 배열로 반환
1// `a`가 두 개 포함된 문자열2const str = 'abcabc'34// `g` 플래그 없이는 최초에 발견된 문자만 반환5str.match(/a/) // ["a", index: 0, input: "abcabc", groups: undefined]67// `g` 플래그가 있으면 모든 결과가 배열로 반환8str.match(/a/g) // (2) ["a", "a"]
1.3.2 m: 줄바꿈 검색
- 여러 줄의 문자열에서 필터링 해야 될때 사용된다.
- 뒤에서 배울 입력 시작(
^) 앵커나 입력 종료($) 앵커는 전체 문자열이 아닌 각 줄 별로 대응되게 만들어졌기 때문에, - 만일 여러줄을 검색해야 한다면 m 플래그를 사용한다고 보면 된다
1// 줄바꿈이 포함된 3줄 문자열2const str = 'Hello World and\nPower Hello?\nPower Overwhelming!!'3/*4Hello World and5Power Hello?6Power Overwhelming!!7*/89// Hello 단어로 시작하는지 검사 (^ 문자는 문장 시작점을 의미)10str.match(/^Hello/) // ["Hello"]11// → 첫번째 줄은 잘 찾음1213// Power 단어로 시작하는지 검사 (^ 문자는 문장 시작점을 의미)14str.match(/^Power/) // null15// → 그러나 그 다음 줄은 검색되지 아니함1617// 따라서 m 플래그를 통해 개행되는 다음 줄도 검색되게 설정18str.match(/^Power/m) // ['Power']1920// 세번째 줄도 검색되게 하고싶으면 g 플래그와 혼합 사용21str.match(/^Power/gm) // ['Power', 'Power']
1.3.3 i: 대소문자 구분 없음
- 정규식은 기본적으로 대소문자를 구분 (Case sensitive)
- 대신
i플래그를 통해 대소문자 구분 하지 않을수 있다.
1const str = 'abcABC'23// 대소문자 a 검색4str.match(/a/gi) // (2) ["a", "A"]
2. 정규식 기호 모음
2.1 정규식 특정 문자 숫자 매칭 패턴
| 패턴 | 의미 |
|---|---|
a-zA-Z | 영어알파벳(-으로 범위 지정) |
ㄱ-ㅎ가-힣 | 한글 문자(-으로 범위 지정) |
0-9 | 숫자(-으로 범위 지정) |
. | 모든 문자열(숫자, 한글, 영어, 특수기호, 공백 모두) 단, 줄바꿈 X |
\d | 숫자 |
\D | 숫자가 아닌 것 |
\w | 밑줄 문자를 포함한 영숫자 문자에 대응[A-Za-z0-9_] 와 동일 |
\W | \w가 아닌 것 |
\s | space 공백 |
\S | space 공백이 아닌 것 |
\특수기호 | 특수기호 \* \^ \& \! \? ...등 |
\b | 63개 문자(영문 대소문자 52개 + 숫자 10개 + \_(underscore))가 아닌 나머지 문자에 일치하는 경계(boundary) |
\B | 63개 문자에 일치하는 경계 |
\x | 16진수 문자에 일치/\x61/는 a에 일치 |
\0 | 8진수 문자에 일치/\141/은 a에 일치 |
\u | 유니코드(Unicode) 문자에 일치/\u0061/는 a에 일치 |
\c | 제어(Control) 문자에 일치 |
\f | 폼 피드(FF, U+000C) 문자에 일치 |
\n | 줄 바꿈(LF, U+000A) 문자에 일치 |
\r | 캐리지 리턴(CR, U+000D) 문자에 일치 |
\t | 탭 (U+0009) 문자에 일치 |
2.2 정규식 검색 기준 패턴
| 기호 | 의미 |
|---|---|
| | OR a|b |
[] | 괄호안의 문자들 중 하나. or 처리 묶음 보면 된다. /abc/ : “abc”를 포함하는 /[abc]/ : “a” 또는 “b” 또는 “c” 를 포함하는 [다-바] : 다 or 라 or 마 or 바 |
[^문자] | 괄호안의 문자를 제외한 것[^lgEn] “l” “g” “E” “N” 4개 문자를 제외대괄호 안에서 쓰면 제외의 뜻, 대괄호 밖에서 쓰면 시작점 뜻 |
^문자열 | 특정 문자열로 시작 (시작점)/^www/ |
문자열$ | 특정 문자열로 끝남 (종착점)/com$/ |
2.3 정규식 갯수 반복 패턴
| 기호 | 의미 |
|---|---|
? | 없거나 or 최대 한개만 /apple?/ |
\* | 없거나 or 있거나 (여러개)/apple*/ |
+ | 최소 한개 or 여러개/\apple\+/ |
\*? | 없거나, 있거나 and 없거나, 최대한개 : 없음 {0}와 동일 |
+? | 최소한개, 있거나 and 없거나, 최대한개 : 한개 {1}와 동일 |
{n} | n개 |
{Min,} | 최소 Min개 이상 |
{Min, Max} | 최소 Min개 이상, 최대 Max개 이하{3,5}? == {3}와 동일 |
2.4 정규식 그룹 패턴
| 기호 | 의미 |
|---|---|
() | 그룹화 및 캡쳐 |
(?: 패턴) | 그룹화 (캡쳐 X) |
(?=) | 앞쪽 일치(Lookahead), /ab(?=c)/ |
(?!) | 부정 앞쪽 일치(Negative Lookahead), /ab(?!c)/ |
(?<=) | 뒤쪽 일치(Lookbehind), /(?<=ab)c/ |
(?<!) | 부정 뒤쪽 일치(Negative Lookbehind), /(?<!ab)c/ |
정규식 그룹 패턴 부분은 꽤나 난이도 있는 정규표현식에 속한다. 어렵고 이해가 잘 안되는 것이 당연하니, 차근차근 알아가보자.
2.4.1 정규식 그룹화
1'kokokoko'.match(/ko+/) // "ko"2'kooookoooo'.match(/ko+/) // "koooo"
코드를 보면 알수 있듯이, 표현식 ko+는 “o”만 + 를 적용시킨다. (“k” 는 적용안시킴) 그 결과로 “koooo”가 반환되었다.
1'kokokoko'.match(/ko+/) // "ko"2'kooookoooo'.match(/ko+/) // "koooo"
하지만 표현식 (ko)+는 “k”와 “o”를 묶었기(그룹화) 때문에 “ko” 자체를 1회 이상 연속으로 반복되는 문자로 검색하게 된다. 따라서 결과가 “kokokoko”가 반환되었다.
그런데 마지막으로 패턴 ()를 사용한 정규식들의 결과를 잘 보면 일치한 결과가 2개가 나온다. 일부러 한번만 검색되라고, 플래그 g를 사용하지 않았는데 말이다.
2.4.2 정규식 캡처 기능
패턴 그룹화 ()는 괄호 안에 있는 표현식을 캡처하여 사용한다. 캡처는 일종의 복사본을 생성하는 개념이라고 보면 된다. (복사라는 단어는 이해를 돕기 위해서만 사용하며, 실제 개념과는 다르다)
1'kokokoko'.match(/(ko)+/) // "kokokoko", "ko"
정규식의 캡처 원리를 알아보자면, 패턴 ()안에 있는 “ko”를 그룹화하여 캡처(복사)한다. 우선 캡처된 표현식은 당장 사용되지 않으며, 그룹화된 “ko”를 패턴 +로 1회 이상 연속으로 반복되는 문자로 검색한다. 그렇게 캡처 외 표현식이 모두 작동하고 난 뒤에 복사했던(캡처된) 표현식 “ko”가 검색되는 것이다.
즉, 위의 검색 순서를 정리하자면 다음과 같게 된다.
- 그룹화된 “ko”를 패턴 +로 1회 이상 연속으로 반복하여 검색하여 “kokokoko”를 반환하고
- 캡처된 “ko”로 검색하여 “ko”를 추가 반환
1'123abc'.match(/(\d+)(\w)/) // "123a", "123", "a"2/*31. 패턴 ()안의 표현식을 순서대로 캡처. \d+, \w42. 캡처 후 남은 표현식으로 검색.53. 패턴 \d로 숫자를 검색하되 패턴 +로 1개 이상 연속되는 숫자를 검색 → "123"64. 다음 패턴 \w는 문자를 검색하니 "a"가 일치75. 최종적으로 "123a"가 반환.896. 첫 번째 캡처한 표현식 \d+로 숫자를 재검색하되 패턴 +로 1개 이상 연속되는 숫자를 검색107. "123"가 일치하여 반환11128. 나머지 캡처한 표현식 \w로 문자를 검색하니 "a"가 일치하여 반환13*/
2.4.3 캡처하지 않는 그룹화 (?:)
위에서 살펴봤듯이 뜻하지않은 정규식 그룹화 캡쳐 기능 때문에 쓸데없는 결과값을 얻는 것이 싫다면, 괄호 안에 ?: 문자를 씀으로써 캡쳐를 비활성화 할 수 있다. 따라서 표현식 캡처를 하지 않기 때문에 “k”와 “o”를 그룹화한 “ko”만으로 검색되게 된다.
1// 그룹화 + 캡처2'kokokoko'.match(/(ko)+/) // "kokokoko", "ko"34// 그룹화만5'kokokoko'.match(/(?:ko)+/) // "kokokoko"
3. 정규식 테스트 사이트
3.1 Regexr👍
가장 유명한 정규식 테스트 사이트가 아닐까 싶다. 깔끔한 UI로 인기가 가장 많다.
A-Z로 시작하는 문자 검색하는 정규표현식으로 전체문장에서 하이라이트로 표기해주고, 정규표현식 작성시 도움이 되는 문법도 왼쪽에 표기된다.
정규식에 커서를 올리면 짧게 문법 의미를 볼수 있다.
예제 Text란에도 커서를 올리면 매치 정보(인덱스 범위, 그룹 등)를 보여준다.
사이드 메뉴 사용법
- Pattern Settings : 우측에서 사용한 정규식 예제를 저장할수 있다. (로그인 필요)
- MyPatterns : 내 계정에 저장한 정규식을 사용할수있다. 일종의 즐겨찾기 개념
- Cheatsheet : 자주 사용하는 정규식 메뉴
- RegEx Reference : 정규식 기호를 메뉴로 선택해 사이드바 하단에 예제를 보여주어 사용법을 알려준다.
- Community Patterns : 다른 사람이 직접 만든 정규식와 예제를 불러와서 테스트해볼 수 있다.
3.2 regex101
위의 Refexer는 자바스크립트와 PHP 밖에 지원을 안하지만, 이 사이트는 보다 다양한 언어를 지원한다는 장점이 있다.
3.3 Regexper (그림으로 분석)
한 문자열로 쭉 이루어진 정규표현식은 가독성이 매우 안좋은데, 이를 그림(도식)으로 표현해서 쉽게 분석할수 있게 도와주는 사이트다. 처음에는 생소해서 오히려 그림의 의미를 알수가 없어 거부감이 들테지만, 익숙해지면 정말 가독성이 좋아지니 한번 시간이 있다면 도전해 보는걸 권장한다.
3.4 Rubular 사이트 (Ruby 기반)👍
루비 언어의 정규식 문법을 체크할수있다.
3.5 정규표현식 샘플 모음 사이트
자주 사용하는 정규표현식 예제들이 모여있는 사이트이다. 만일 이메일이나 전화번호를 체크하는 정규식 문법이 필요하다면, 검색창에 찾으려는 타입을 치고 검색하면 여러 정규식 예제들을 얻을 수 있다.
4. 휴대폰 번호를 찾아보자!
-
정규표현식을 사용하기 위해선 패턴을 찾는 것이 제일 중요하다.
-
휴대폰 번호 패턴은?
- 010-1234-5678
- 018-123-4567
- 01X-XXX(X)-XXXX
- 국가번호, 안심번호 등 예외 케이스는 제외
-
세 자리 숫자, 하이픈, 셋 혹은 네 자리 숫자, 하이픈, 네 자리 숫자 패턴으로 이루어져 있다.
-
\d{3}= 3자리 아무 숫자 -
\d{3, 4}= 3~4자리 아무 숫자
패턴을 조합해보면?
1\d{3}-\d{3,4}-\d{4}23안녕하세요. 010-1234-5678로 연락해주세요!
5. 이메일 주소에서 중간 문자열만
-
이메일 주소 패턴은?
-
문자열, @, 문자열, ., 문자열패턴으로 이루어져 있다. -
여기서 naver, cobalt만 뽑고 싶다면?
-
.+= 한 개 이상의 문자열 -
(...)= 캡처
1.+@.+\..+23kciter@naver.com4kciter@cobalt.run5kciter@naver6kciter@cobal@run
일단 이메일만 뽑아낼 수는 있다.
캡처를 이용하면 원하는 부분만 뽑을 수 있다.
1.+@(.+)\..+
캡처는 여러 번 할 수 있다.
1(.+)@(.+)\..+
6. JavaScript
6.1 생성 방법
- JavaScript는 RegExp 객체로 정규표현식 기능을 제공한다.
- Array, Object처럼 Literal로 생성 가능하다.
1/* 생성자 함수 방식 */2// new RegExg(표현식)3const regExp1 = new RegExp('^d+')45// new RegExg(표현식, 플래그)6const regExp2 = new RegExp('^d+', 'gi')78/* 리터럴 방식 */9// /표현식/10const regexp1 = /^\d+/1112// /표현식/플래그13const regexp2 = /^\d+/gi
6.2 test
- 정규표현식 객체의 test 함수는 입력받은 문자열에 찾은 패턴이 있는 찾은 후
- 있다면 true를 반환하고, 없으면 false를 반환한다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'2const message2 = '안녕하세요. 연락하지 마세요!'34// 정규표현식 리터럴5const regExp = /\d{3}-\d{3,4}-\d{4}/6console.log(regExp.test(message)) // true7console.log(regExp.test(message2)) // false
6.3 exec
- 정규표현식 객체의 exec 함수는 입력받은 문자열에 찾는 패턴이 있는지 찾은 후
- 일치한 패턴 정보를 반환하고 없으면 null을 반환한다.
- 문자 추출에 해당한다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'2const message2 = '안녕하세요. 연락하지 마세요!'34// 정규표현식 리터럴5const regExp = /\d{3}-\d{3,4}-\d{4}/6console.log(regExp.exec(message))7// [8// '010-1234-5678',9// index: 7,10// input: '안녕하세요. 010-1234-5678로 연락주세요!',11// groups: undefined12// ]1314console.log(regExp.exec(message2)) // null
6.4 match
- String 객체의 match 함수는 정규표현식 객체를 파라미터로 받아 패턴이 있는지 찾은 후
- 일치한 패턴 정보를 반환하고 없으면 null을 반환한다. 정규표현식 객체의 exec 함수와 같다.
- 문자 추출에 해당한다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'2const message2 = '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!'34// 정규표현식 리터럴5const regExp = /\d{3}-\d{3,4}-\d{4}/6console.log(message.match(regExp))7// [8// '010-1234-5678',9// index: 7,10// input: '안녕하세요. 010-1234-5678로 연락주세요!',11// groups: undefined12// ]1314// 📌 무조건 처음 매칭된 것을 반환한다.15console.log(message2.match(regExp))16// [17// '010-1234-5678',18// index: 7,19// input: '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!',20// groups: undefined21// ]2223// 📌 모두 탐색하려면 matchAll을 사용할 수 있다.24console.log([...message2.matchAll(/\d{3}-\d{3,4}-\d{4}/g)])25// [26// [27// '010-1234-5678',28// index: 7,29// input: '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!',30// groups: undefined31// ],32// [33// '010-9876-5432',34// index: 23,35// input: '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!',36// groups: undefined37// ]38// ]
6.5 replace
- String 객체의 replace 함수는 정규표현식 객체를 파라미터로 받아 패턴이 있는지 찾은 후
- 일치한 패턴 정보를 원하는 문자열로 바꿀 수 있다.
- 문자 대체에 해당한다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'2const message2 = '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!'34// 정규표현식 리터럴5const regExp = /\d{3}-\d{3,4}-\d{4}/6console.log(message.replace(regExp, '전화번호'))7// 안녕하세요. 전화번호로 연락주세요!89console.log(message2.replace(regExp, '전화번호'))10// 안녕하세요. 전화번호말고 010-9876-5432로 연락주세요!1112// 📌 옵션으로 g를 붙임녀 모두 변경된다.13console.log(message2.replace(/\d{3}-\d{3,4}-\d{4}/g, '전화번호'))14// 안녕하세요. 전화번호말고 전화번호로 연락주세요!
6.6 search
- String 객체의 search 함수는 정규표현식 객체를 파라미터로 받아 패턴이 있는지 찾은 후
- 일치한 패턴 정보의 위치를 반환한다.
- 문자 검색에 해당한다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'2const message2 = '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!'34// 정규표현식 리터럴5const regExp = /\d{3}-\d{3,4}-\d{4}/6console.log(message.search(regExp)) // 77console.log(message2.search(regExp)) // 789// 📌 무조건 처음 매칭된 것을 반환한다.10console.log(message2.search(/\d{3}-\d{3,4}-\d{4}/g)) // 71112// 안녕하세요. 전화번호말고 전화번호로 연락주세요!1314// 📌 모두 탐색하려면 matchAll을 사용할 수 있다.15console.log([...message2.matchAll(/\d{3}-\d{3,4}-\d{4}/g)])16// [17// [18// '010-1234-5678',19// index: 7,20// input: '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!',21// groups: undefined22// ],23// [24// '010-9876-5432',25// index: 23,26// input: '안녕하세요. 010-1234-5678말고 010-9876-5432로 연락주세요!',27// groups: undefined28// ]29// ]
6.7 capture
- 캡처가 적용된 정규표현식을 이용하면 match 반환값의
- 1번 인덱스부터 순차적으로 캡처 결과가 들어간다.
1const message = '안녕하세요. 010-1234-5678로 연락주세요!'23// 정규표현식 리터럴4const regExp = /(\d{3})-(\d{3,4})-(\d{4})/5console.log(message.match(regExp))6// [7// '010-1234-5678',8// '010',9// '1234',10// '5678',11// index: 7,12// input: '안녕하세요. 010-1234-5678로 연락주세요!',13// groups: undefined14// ]
7. Run-length encoding
- 매우 간단한 비손실 압축 방법
- “AAAAAABBBDFFFFFFFKK” 문자열을 어떻게 압축할것인가?
- “6A3B1D7F2K”로압축할 수 있다.
- 6개의 A, 3개의 B, 1개의 D, 7개의 F, F, 2개의 K
- 패턴을 발견할 수 있는가?
1(.)\1*23ABBBCC
1const raw = 'AAAAAABBBDFFFFFFFKK'2const compressed = '6A3B1D7F2K'34const regExp = /(.)\1*/g5const result = raw.match(regExp).reduce((a, b) => a + `${b.length}${b.slice(0, 1)}`, '')67console.log(result) // 6A3B1D7F2K8console.log(result === compressed) // true
8. 예제
8.1 범위에서 고르기
연속된 영어 소문자를 찾으려면 어떻게 할까요?
- 소문자를 뜻하는
[a-z]와 - 반복을 뜻하는
+를 붙여 =>[a-z]+를 씁니다.
빈칸에 [a-z]+를 입력하고 [실행]해 보세요.
8.2 한글 고르기
한글의 첫 번째 글자는 가이고 마지막 글자는 힣입니다. 따라서 한글은 [가-힣]으로 찾을 수 있습니다.
빈칸에 [가-힣]+를 입력하고 [실행]해 보세요.