정규표현식 Part 2. 실전 및 자주 사용되는 패턴들

0

정규식 실전 연습

개행이 아닌 문자에 매칭하기

“abc.def.ghi.jkx”의 형태에 매칭되는 패턴을 찾아보는 예제를 만들어보겠습니다. 이 패턴은 다음과 같은 동작을 해야 합니다.

  • 개행 문자가 아닌 글자 3개가 있고
  • “.” 문자에 이어 다시 개행 문자가 아닌 글자 3개가 오고
  • 2의 패턴이 3회 반복되는 패턴

위와 같은 동작을 표현하는 정규식 패턴은 다음과 같이 쓸 수 있습니다.

[^\n]{3}(?:\.[^\n]{3}){3}
/**
[^\n]{3}    # 개행문자가 아닌 글자가 3개 있고
(?:         # 캡쳐하지 않는 그룹을 시작하고
\.          # . 이 온 후
[^\n]{3}    # . 다음에 다시 개행이 아닌 문자가 3개 오고
){3}        # 이 그룹을 다시 3회 반복한다.
*/

핸드폰 번호 매칭하기(1)

핸드폰 번호는 010-1234-5678과 같은 형식으로 쓰고, 구분자는 없을 수도 있고, 공백일 수도 있습니다. 즉, 다음과 같은 표현 중 어느 하나에 해당된다면, 유효한 핸드폰 번호라고 할 수 있습니다.

  • 010-1234-5678
  • 01012345678
  • 010.1234.5678

하지만 아직도 예전 핸드폰 번호를 사용하는 사람도 일부 존재하는데, 이 경우에는 011, 016, 017, 018, 019로 시작하거나, 가운데 자리가 3자리인 경우도 있을 수 있습니다.

010으로 시작하는 번호는 항상 가운데 번호가 4자리이기 때문에 판별이 쉽지만, 예전 핸드폰 번호 형식이라면 010-123-4567과 같은 번호는 유효한 번호로 판별하면 안되겠죠? 즉 이런 예외적인 사항까지 고려한다면 다음과 같이 표현식을 쓸 수 있습니다.

01[016789]\D?\d{3,4}\D?\d{4}
/**
01[016789]   # 010, 011 등의 식별번호로 시작하고
\D?\d{3,4}   # 숫자가 아닌 구분기호는 있거나 없고
\D?\d{4}$    # 구분기호(옵션)뒤에는 4자리 숫자인 패턴
*/

로그에서 값 추출하기

일반적으로 로그는 텍스트로 출력되기 때문에 로그에서 원하는 정보를 추출하기 위해서는 정규식이 매우 유용합니다.

Line 394 : <0x3924004E|JVM| Free Memory: Heap [ 2368340/ 6291456>21:44:07 Oct 12 Fri] , Native[ 7299824/32505856]

위와 같은 로그 정보가 있을 때, 로그가 찍힌 시간, 남은 heap, 네이티브 메모리의 값만을 추출하고 싶다면 어떤 패턴을 사용할 수 있을까요? 이를 위해서는 다음과 같은 패턴을 쓸 수 있습니다.

((?:\d{2})(?:\:\d{2}){2}).*?\[\s(\d+).*?\[\s(\d+)
/**
(             # 첫번째 그룹 캡쳐
(?:\d{2})     # 숫자 두 개가 연속으로 나오고
(?:           # 캡쳐과 되지 않는 그룹으로 묶고
\:\d{2}       # : 뒤에 숫자 두 개가 오는 패턴이고
){2}          # 두 개가 더 붙어서 12:34:56 패턴을 완성하는
)             # 첫번째 그룹
.*?\[\s       # "[ " 을 만날 때까지 앞으로 전진하고
(\d+)         # 연속된 숫자만 취하는 두 번째 그룹
.*?\[\s(\d+)  # 같은 패턴을 한 번 더 사용한 세 번째 그룹
*/

마지막으로, 이 패턴에 매칭된 결과를 \1, \2, \3으로 치환하면 시간, heap, native 메모리 값만 남기고 나머지를 모두 제거할 수 있습니다.

HTML 태그의 내용 추출하기

특정 HTML 태그의 내용만 추출하는 것은 웹 페이지를 스크래핑하여 특정 정보만 빼낼 때 자주 사용됩니다. 즉, 적절하게 구분할 수 있는 힌트만 있다면 특별한 HTML 파서가 없어도 정규식을 통해 내용을 추출할 수 있습니다.

예를 들어 웹 페이지 내 테이블의 특정 정보들을 추출한다면, 우선 테이블 각 행의 <tr> 태그 사이를 캡쳐하면 되겠죠? 이 패턴은 다음과 같이 쓸 수 있습니다.

<tr.*?>(.*?)</tr>
/**
<tr      # <tr 로 시작
.*?>     # tr 태그가 끝날 때까지
(.*?)    # 다음 패턴의 태그를 만날때 까지 태그내 내용을 캡쳐
</tr>    # 캡쳐할 패턴의 닫는 태그
*/

해당 내용에 매치된 부분은 <td>를 포함하며, 캡쳐링 그룹으로 해당 내용을 캡쳐할 수 있습니다.

정규표현식의 조건절

정규식에서 조건절은 자주 사용되지는 않습니다. 또 모든 정규식 엔진이 조건절을 지원하는 것도 아닙니다. 하지만 사용하는 편집기나 언어의 정규식 엔진에서 조건절을 지원한다면, 조건절을 사용하여 보다 정교한 매칭을 구현할 수 있습니다.

기본 문법

정규 표현식의 조건절에서 조건은 특정한 캡쳐링 그룹의 여부입니다. 즉, 앞에서 특정한 패턴으로 그룹을 정의하고, 해당 그룹이 매칭되었는지 여부에 따라 A 또는 B 패턴을 적용하는 방식으로 사용됩니다.

정규식 조건절 사용 예제
(Z)?.*(?(1)A|B)

위의 패턴에서 “Z”는 기준이 되는 조건 패턴이고, A는 Z그룹이 매치할 때 적용할 패턴, 그리고 B는 Z그룹이 매치하지 않을 때 적용할 패턴입니다. 이 때 ?(1)의 1대신에 다음과 같은 것들이 올 수 있습니다.

  • 앞에서 언급한 캡쳐링 그룹의 번호
  • 캡쳐링 그룹에 이름을 지정할 수 있는 경우에는 해당 그룹의 이름
  • 숫자에 부호를 붙이는 경우에는 그룹의 상대 위치를 offset 방식으로 쓸 수 있음
  • look-around 표현식
  • 서브루틴 콜
  • 재귀 매칭

캡쳐링 그룹 번호를 조건으로 쓰는 경우

정규식에서 가장 일반적으로 사용하는 조건절은 특정한 그룹의 매칭 여부에 따라 뒤쪽의 패턴이 달라지는 것입니다. 다음의 예는 연속된 숫자값이 있는 라인을 찾을 때 사용하는 패턴으로, 두 가지 경우가 있다고 가정합니다.

  • “START”로 시작하고 숫자가 연속되며 다시 “END”로 끝나는 경우
  • “START”와 “END”가 모두 없고 라인 전체가 숫자로만 이루어지는 경우

이 규칙에 따르면 “START”로 시작했지만, 뒤에 “END”가 없거나, “START”가 없는데 뒤에 “END”가 붙은 라인은 모두 무효로 처리되어야 하고, 결국 “START”의 매치 여부에 따라 “END”를 체크할 것인지가 결정됩니다. 이런 동작은 다음과 같은 패턴으로 쓸 수 있습니다.

^(START)?(\d+)(?(1)END|\b)$
/**
^(START)?     # START로 시작했는지 여부를 그룹 1에 캡쳐
(\d+)         # 연속된 숫자를 그룹2에 캡쳐
(?(1)END|\b)  # 그룹1이 있다면 END에 매치, 없다면 \b에 매치
$             # 문자열의 끝
*/

이 패턴의 테스트 결과는 다음과 같습니다.

  • START012314352454END => 매칭
  • 0123134354556 => 매칭되지 않음
  • START0143452456E => 뒤에 END가 없어 매칭되지 않음
  • 12235345345END => END만 있어 매칭되지 않음

사실 이 패턴은 아주 단순하기 때문에, 일반적인 정규식으로도 표현할 수 있습니다.

^(?:START(\d+)END|(\d+))$

하지만 위 표현식의 경우 매칭 여부에 대해서는 동일한 결과를 반환하지만, 결과가 조금 달라지게 됩니다. 조건절을 사용한 패턴에서는 연속된 숫자 부분만 얻을 때 항상 GROUP 2를 확인하면 되지만, 조건절을 지원하지 않는 일반 정규식 패턴에서는 START~END가 있으면 GROUP 1을, 없으면 GROUP 2를 확인해야 합니다.

만약 캡쳐링해야 하는 그룹이 많다면 번호를 가지고 지정하는 그룹이 헷갈리기 쉽기 때문에, 캡쳐링 그룹에 이름을 지정할 수 있습니다.

(?P<name> pattern)

위의 표현식과 같이 ?P<name>을 사용해서 그룹에 이름을 지정할 수 있는데, PCRE 정규식에서는 (?<name>)(?P<name>)이 모두 지원되지만, 파이썬에서는 후자의 패턴만 허용됩니다. 참고로, 그룹이름은 숫자로 시작되지 않는 영문자여야 합니다.

선후방 검색에 의한 조건절

그룹에 의해 조건절은 조건으로 사용되는 그룹이 앞부분에 나와야 한다는 한계가 있지만, 전방탐색을 사용하여 뒤쪽에 나올 패턴을 미리 확인하고 앞에서 조건에 따른 패턴을 적용하는 방식을 사용하면 이런 한계를 극복할 수 있습니다. 이게 바로 선후방 검색 조건절인데, 이 조건절은 PCRE 정규식에서만 사용 가능하고, 파이썬에서는 사용할 수 없습니다.

(?(?=.*_fruit)(?:apple|banana)|(?:potato|coffee))
/**
(?(?=.*_fruit)      # 나중에 "_fruit"가 나오면 true
(?:apple|banana)    # true일 경우 "apple", "banana" 중에서 매칭
|(?:potato|coffee)) # false일 경우 "potato", "coffee" 중에서 매칭
*/

결과는 다음과 같습니다.

  • apple_fruit => “apple”에 매칭
  • apple_nuts => 매칭안됨
  • potato_fruit => 매칭안됨
  • coffee_nuts => “coffee”에 매칭

선후방 조건절을 사용할 수 없다면, 다음과 같이 OR 패턴으로 대체할 수도 있습니다.

^(?:(?:apple|banana)(?=.*_fruits))|(?:(?:potato|coffee)(?!.*_fruits$))

핸드폰 번호 매칭하기(2)

핸드폰 번호를 찾는 패턴을 다시 한번 작성해보겠습니다. 핸드폰 번호의 규칙은 다음과 같습니다.

  • 3자리 – 3~4자리 – 4자리 형태로 구성
  • 각 마디 사이에는 – 이나 . 이 들어감
  • 첫 마디에는 010, 011, 016, 017, 018, 019가 올 수 있음
  • 첫 마디가 010인 경우에는 가운데 마디가 항상 4자리이고, 그외의 식별 번호는 가운데 마디가 3혹은 4자리임

조건절을 사용하면, 첫번째 마디가 010인지 여부에 따라서 가운데 마디의 패턴이 달라지면 됩니다. 이렇게 조건절을 사용하면 표현식을 비교적 간단하게 쓸 수 있습니다.

^(?:(?P<a>010)|01[16789])(?:(?Pb>[\-\.])?(?(a)\d{4}|\d{3,4}))(?:(?(b)(?P=b))\d{4})
/**
^(?:(?P<a>010)|01[16789])  # 첫마디는 010 이거나, 01[16789]로 시작
(?:(?P<b>[\-\.])?          # 두번째 마디 앞에는 -이나 .이 올 수 있고,
                             구분 문자는 b라는 이름의 그룹으로 캡쳐함
(?(a)\d{4}|\d{3,4}))       # 첫마디가 010이었다면 둘째마디는 4자리,
                             그외에는 3~4자리가 됨
(?:(?(b)(?P=b))            # 앞 마디에 구분문자(b)가 있었다면,
                             똑같은 문자가 옴
\d{4})                     # 마지막엔 4자리 숫자가 옴
*/

이 패턴을 사용하면, 다음과 같은 형식의 번호들이 매칭됩니다.

  • 010-1234-1234
  • 011-1234-1234
  • 010.1234.5678
  • 011.123.2134
  • 01012345678
  • 01112345678
  • 0111235678

반면, 다음과 같은 패턴들은 매칭되지 않습니다.

  • 010-123-1234 => 010인데 가운데가 3자리인 경우
  • 014-123-1235 => 식별번호가 틀린 경우
  • 010.1234-5678 => 마디 구분 문자가 일관되지 않은 경우
  • 0101234-5678 => 마디 구분 문자가 뒤에만 쓰인 경우
  • 010-12345678 => 마디 구분 문자가 앞에만 쓰인 경우

자주 사용되는 정규표현식 샘플 모음

특정 단어로 끝나는지 검사하는 패턴

const fileName = 'index.html';
// 'html'로 끝나는지 검사
const regexr = /html$/;

숫자로만 이루어진 문자열 검사 패턴

const targetStr = '12345';

// 모두 숫자인지 검사
const regexr = /^\d+$/;

아이디 검사 패턴

알파벳 대소문자 또는 숫자로 시작하고 끝나며 4~10자리인지 검사하는 예제입니다.

const id = 'abc123';

// 알파벳 대소문자 또는 숫자로 시작하고 끝나며 4~10자리인지 검사
const regexr = /^[A-Za-z0-9]{4,10}$/;

핸드폰 번호 검사 패턴

const cellphone = '010-1234-5678';
const regexr = /^\d{3}-\d{3,4}-\d{4}$/;

URL 형식 검사 패턴

http://https://로 시작하고, 알파벳, 어더스코어(_), 하이픈(-), dot(.)으로 이루어져 있는 패턴을 확인합니다.

const text =
`http://youdad.kr http://google.com 010-1234-5678 02-123-4567 rihodad@youdad.kr`;

text.match(/https?:\/\/[\w\-\.]+/g); // ["http://youdad.kr", "http://google.com"]
/**
http            # http로 시작함
s?              # s는 없거나, 있음
\/\/            # 다음에 특수기호 //가 옴
[\w\-\.]+       # \w(영문자, 언더스코어), 하이픈, 쩜으로 이루어진
                  문자열이 한개 이상(+) 있음
g               # 매칭되는 모든 패턴 검색
*/

전화번호 형식 검사 패턴

유선번호 형식(02-123-4567)과 핸드폰번호 형식(010-1234-5678)을 모두 포함하는 정규식 패턴입니다.

const text =
`http://youdad.kr http://google.com 010-1234-5678 02-123-4567 rihodad@youdad.kr`;

text.match(/\d{2,3}-\d{3,4}-\d{4}/g); // [ '010-1234-5678', '02-123-4567' ]
/**
\d{2,3}         # 숫자 2~3개로 시작
\-              # 다음에 하이픈(-)이 옴
\d{3, 4}        # 다음에 숫자가 3~4개 옴
\-              # 다음에 하이픈(-)이 옴
\d{4}           # 다음에 숫자가 4개 옴
g               # 매칭되는 모든 패턴 검색
*/

이메일주소 형식 검사 패턴

const text =
`http://youdad.kr http://google.com 010-1234-5678 02-123-4567 rihodad@youdad.kr`;
text.match(/<[\w\-\.>\w\-\.]+\+/g); //  'rihodad

// 좀더 엄격한 검사가 필요한 경우
const email = 'rihodad@youdad.kr';

const regexr = /^<[0-9a-zA-Z>0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;

특수기호 정규표현식 패턴

// 모든 특수기호 나열
const regex = /\[\]\{\}\/\(\)\.\?\<\>!@#$%^&*/g

// 문자와 숫자가 아닌 것 매칭
const regex = /[^a-zA-Z0-9가-힣ㄱ-ㅎ]/g

기타 정규표현식 패턴 모음

// 한글
const ko_1 = /[ㄱ-ㅎ|ㅏ-ㅣ]/;
const ko_2 = /가-힇ㄱ-ㅎㅏ-ㅣ/;
const ko_name = /[가-힣]/;

// 영어
const en_1 = /[a-z | A-Z]/;
const en_2 = /a-zA-Z/;

// 일본어
const jp = /[ぁ-ゔ]+|[ァ-ヴー]+[々〆〤]/;

// 중국어
const ch = /一-龥/;

// 숫자
const number = /0-9/;

// 한글 + 영어 + 한자 + 일본어 + 숫자
const all_lang = /[a-zA-Z0-9가-힇ㄱ-ㅎㅏ-ㅣぁ-ゔァ-ヴー々〆〤一-龥]/

// 특수문자
const special_char = /<\#$%&\\\=\(\'\">\{\}\[\]\/?.,;:|\)*~`!^\-+<>/;
const comma_char = /,/g;
const blank = /[\s]/g;

// 아이디, 비밀번호
const id = /^[a-z | A-Z]{3,6}[0-9]{3,6}$/;
const password =/^.*(?=.{6,20})(?=.*[0-9])(?=.*[a-zA-Z]).*$/;

// 이메일 형식
const email =/(<((\[[0-9>\w-\.]+){1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;

// 도메인 형식
const domain_all =/(<((\[[0-9>\w-\.]+){1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)
|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
const domain_include = /^((http(s?))\:\/\/)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/;
const domain_exclude = /^[^((http(s?))\:\/\/)]([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/;

// 영문 + 한글 + 숫자 또는 영문 + 한글
const ko_en_num_charactor = /^[가-힣a-zA-Z0-9]*$/;
const ko_en_charactor = /^[가-힣a-zA-Z]*$/;

// 자동차 번호판
const car = /^[0-9]{2}[\s]*[가-힣]{1}[\s]*[0-9]{4}$/;
const old_car = /^[가-힣]{2}[\s]*[0-9]{2}[\s]*[가-힣]{1}[\s]*[0-9]{4}$/;

답글 남기기