이전 포스트까지 정규 표현식이 무엇인지, Python에서 정규 표현식을 어떻게 탐색하는지에 대해 알아보았다. 이번 포스트에서는 가장 중요한 "메타 문자로 정규 표현식 패턴 만들기"에 대해 알아보도록 하겠다.
1. 메타 문자로 정규 표현식 패턴 만들기
- 메타 문자로 정규 표현식 만들기는 크게 3가지 방법으로 돌아간다.
- 패턴 문자열 생성: 찾고자 하는 패턴 문자열(문자 클래스)을 생성한다.
- 패턴 문자의 반복 지정: 패턴 문자열이 몇 번 반복되는지 지정하기
- 패턴 문자의 위치 지정: 패턴 문자열로 시작하거나 패턴 문자열로 끝나는지 지정한다.
- 위 3개 과정은 다 함께 진행될 수도 있고, 하나만 진행될 수도 있다. 이 외에도 문자의 패턴들을 그룹으로 만들거나 |(or)를 이용하여, 원하는 패턴이 존재하는 경우를 선택하게 할 수도 있다.
- 각 부분에 해당하는 메타 문자는 이전 포스트인 <Python-기초:3.0. 정규 표현식(1) - 소개: 정규 표현식과 메타 문자>를 보면 깔끔하게 표로 정리해놨으니, 이를 참고하기 바란다.
2. 패턴 문자열 생성 하기
2.1. 문자 클래스 [ ]
- 문자 클래스(character class)라고 하며, [ ] 안에는 1개 이상의 문자를 넣을 수 있고, 그 문자 중 하나라도 일치한다면 일치하는 패턴으로 판단한다.
# [abc]라는 정규 표현식 패턴 생성
>>> pattern = "[abc]"
>>> regex_pattern = re.compile(pattern)
>>> Text_cat = "cat"
>>> print(regex_pattern.findall(Text_cat))
<re.Match object; span=(0, 1), match='c'>
>>> print(regex_pattern.findall(Text_cat))
['c', 'a']
- "[abc]"는 3개의 문자가 들어 있는 것으로 보이지만, [ ]는 문자 하나로 인식한다.
- cat은 "[abc]" 내부의 c와 일치하므로, match 되는 값인 'c'를 반환하였다.
- findall() 메서드로 탐색 시, 일치하는 패턴인 ["c", "a"]를 하나하나 반환하였다.
2.2. 문자 클래스 [ ]와 범위 특수 문자 "-"
- [ ] 안에 들어간 특수문자는 메타 문자가 아닌 원본 문자의 의미로 사용된다.
- 그러나, "-"와 "^"는 별개의 의미를 갖는다.
- "-"는 문자 클래스 내에서 범위를 의미한다.
>>> pattern = "[0-9]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "I have 10 Cats"
>>> print(regex_pattern.findall(Text_data))
['1', '0']
- "[0-9]"는 0에서 9까지의 숫자를 의미한다.
>>> pattern = "[a-z]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "I have 10 Cats"
>>> print(regex_pattern.findall(Text_data))
['h', 'a', 'v', 'e', 'a', 't', 's']
- "[a-z]"는 a부터 z까지의 영문자, 즉 소문자를 의미한다.
>>> pattern = "[A-Z]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "I have 10 Cats"
>>> print(regex_pattern.findall(Text_data))
['I', 'C']
- [A-Z]는 A부터 Z까지의 영문자, 즉 대문자를 의미한다.
>>> pattern = "[a-z]"
>>> regex_pattern = re.compile(pattern, re.IGNORECASE)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
- re.compile() 안에 re.IGNORECASE(re.I)를 넣으면 대소문자 구분 없이 사용할 수 있다.
>>> pattern = "[a-zA-Z0-9]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['H', 'o', 'r', 'm', 'e', 'l', 'F', 'o', 'o', 'd', 's', '1', '0']
- "[a-z]", "[A-Z]", "[0-9]"는 한 문자 클래스 안에서 동시에 사용될 수 있다.
>>> pattern = "[가-힣]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['나', '는', '미', '국', '의', '의', '스', '팸', '통', '조', '림', '개', '를', '가', '지', '고', '있', '다']
- "[가-힣]"을 쓰면 한글에 대해서도 정규 표현식을 사용할 수 있다.
>>> pattern = "[가\-힣]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다. - 힣힣"
>>> print(regex_pattern.findall(Text_data))
['가', '-', '힣', '힣']
- "\-"처럼 특수 문자 앞에 역슬래시를 사용하면 본래 문자를 그대로 쓸 수 있다.
2.3. 문자 클래스 [ ]와 반대 특수 문자 "^"
- 문자 클래스 [ ] 안에 "^"가 들어가면, 그 내용이 반대가 된다.
>>> pattern = "[^가-힣]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
[' ', ' ', 'H', 'o', 'r', 'm', 'e', 'l', ' ', 'F', 'o', 'o', 'd', 's', ' ', ' ', ' ', '1', '0', ' ', ' ', '.']
- 한글을 제외한 모든 문자열이 반환된 것을 볼 수 있다.
- 공백(" ")도 하나의 문자이며, 이는 문자 클래스 안에 띄어쓰기로 표현할 수 있다.
>>> pattern = "[^가-힣 ]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['H', 'o', 'r', 'm', 'e', 'l', 'F', 'o', 'o', 'd', 's', '1', '0', '.']
>>> pattern = "[\^a-z]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['o', 'r', 'm', 'e', 'l', 'o', 'o', 'd', 's']
- "-"와 마찬가지로 앞에 역슬래시를 붙여 "\^"로 만들면 문자열 자체로 사용할 수 있다.
2.4. 별도 표기법을 가진 문자 클래스
- 앞서 학습한 문자 클래스 중 "[a-z]", "[A-Z]", "[0-9]", "[ ]"는 꽤나 빈번하게 사용되는 문자 클래스기 때문에 이를 별도의 표기법으로도 제공한다.
- 대표적인 문자 클래스는 6가지로 다음과 같다.
문자 클래스 | 기능 | 유사한 기능을 하는 문자 클래스 |
\d | 숫자 | [0-9] |
\D | 숫자가 아닌 것 | [^0-9] |
\s | 공백 문자(공백은 단순 띄어쓰기부터 내려쓰기, 탭 등을 포함) | [ \t\n\r\f\v] |
\S | 공백 문자가 아닌 것 | [^ \t\n\r\f\v] |
\w | 문자 + 숫자 + 언더바("_") | [a-zA-Z0-9_] |
\W | 문자 + 숫자 + 언더바("_")가 아닌 것 | [^a-zA-Z0-9_] |
- \t, \n, \r, \f, \v는 이스케이프 문자(Escape Sequence)라 하며, 이에 대한 내용은 이전 포스트 <참고: Python-기초: 2.0. 문자열(1) - 문자열 생성과 이스케이프 문자>를 참고하기 바란다.
- 대문자는 소문자 문자 클래스의 반대이다.
- "\w"와 "\W"의 문자는 단순 영문뿐만 아니라 한글도 포함한다.
>>> pattern = "\W"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '.']
- 위에서 소개한 별도 표기법을 가진 문자 클래스는 [ ] 안에 담을 수도, 따로 사용할 수도 있다.
>>> pattern = "\d"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['1', '0']
>>> pattern = "[\d]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['1', '0']
- 한 문자 클래스에 대해선 별 의미가 없으나, 두 문자 클래스를 동시에 사용할 땐 크게 다르다.
>>> pattern = "\s\d"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
[' 1']
- "\s\d"는 공백 뒤에 숫자가 오는 패턴이므로 '통조림 10개를'에서 ' 1'에 매치된다.
>>> pattern = "[\s\d]"
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
[' ', ' ', ' ', ' ', ' ', ' ', '1', '0', ' ', ' ']
- "[\s\d]"는 공백과 숫자를 함께 넣은 문자 클래스 "[ 0-9]"와 같으므로, 다른 결과를 반환한다.
2.5. 모든 문자를 의미하는 Dot "."
- "."은 모든 문자열 한자리를 의미한다.
- 단 "." 은 줄 바꿈 문자 "\n"는 제외한다.
>>> pattern = "\d."
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['10']
- 숫자 뒤에 문자가 하나 오는 경우인 10을 가지고 왔다.
>>> pattern = ".\d."
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
[' 10']
- 공백도 Dot에 해당하므로 ' 10'을 가지고 왔다.
>>> pattern = "\d..."
>>> regex_pattern = re.compile(pattern)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['10개를']
- 추천하는 방법은 아니지만 Dot를 여러 개 붙여서 문자열의 길이도 지정할 수 있다.
- Dot(".")은 기본적으로 줄 바꿈 문자인 "\n"을 제외하지만, re.DOTALL(re.S) 파라미터를 추가함으로써 줄 바꿈 문자도 포함할 수 있다.
>>> pattern = "\d..."
>>> regex_pattern = re.compile(pattern, re.DOTALL)
>>> Text_data = "나는 미국의 Hormel Foods의 스팸 통조림 10개\n 20개를 가지고 있다."
>>> print(regex_pattern.findall(Text_data))
['10개\n', '20개를']
3. 패턴 문자열의 반복 지정
- 앞서 패턴 문자열을 만드는 법을 배워봤다. 문자 클래스(Character class)는 기본적으로 한 자리이므로, 사용자가 원하는 패턴을 찾아내려면 패턴 문자열의 반복 수를 지정해주면 된다.
- 반복 수를 지정하는 방법은 다음과 같다.
메타 문자 | 의미 |
+ | 앞의 패턴 문자열이 1번 이상 반복되어야 한다. |
* | 앞의 패턴 문자열이 0번 이상 반복되어야 한다. |
? | 앞의 패턴 문자열은 존재할 수도 있고, 없을 수도 있다(0 이상 1 이하). |
{n, m} | 앞의 패턴 문자열은 n번 이상 m번 이하 반복되어야 한다. |
{n, } | 앞의 패턴 문자열은 n번 이상 반복되어야 한다. |
{n} | 앞의 패턴 문자열을 n번 반복되어야 한다. |
- 앞서 학습한 패턴 문자열 작성 방식과 위의 반복 수를 이용해서 문장 내 원하는 패턴을 찾아보도록 사자.
- 다음과 같은 문장이 있다고 가정해보자.
>>> Sentence = "마동석씨의 핸드폰 번호는 010-1234-5678, 이메일 주소는 Email_ID.1234@naver.com이다."
- 여기서 핸드폰 번호를 추출해보자.
- 핸드폰 번호는 01[016789]-[0-9]{4}-[0-9]{4}의 패턴을 갖는다.
>>> pattern = "01[016789]-[0-9]{4}-[0-9]{4}"
>>> regex_pattern = re.compile(pattern)
>>> print(regex_pattern.findall(Sentence))
['010-1234-5678']
- 아주 쉽게 원하는 패턴을 찾아내었다.
- 이번에는 이메일 주소를 추출해보자.
- 이메일 주소의 패턴은 [a-zA-Z]+[a-zA-Z0-9_.]+[@][a-z]+[.][a-z]+[.]?[a-z]? 이다.
>>> pattern = "[a-zA-Z]+[a-zA-Z0-9_.]+[@][a-z]+[.][a-z]+[.]?[a-z]?"
>>> regex_pattern = re.compile(pattern)
>>> print(regex_pattern.findall(Sentence))
['Email_ID.1234@naver.com']
- 이메일 주소의 패턴에는 "?"가 들어갔는데, 이메일 주소의 도메인은 아래와 같이 존재하는 경우도 있기 때문이다.
>>> Sentence = "나문희씨의 이메일 주소는 Email_ID@company.co.kr이다."
>>> pattern = "[a-zA-Z]+[a-zA-Z0-9_.]+[@][a-z]+[.][a-z]+[.]?[a-z]?"
>>> regex_pattern = re.compile(pattern)
>>> print(regex_pattern.findall(Sentence))
['Email_ID@company.co.k']
- 정규 표현식의 첫 포스트에서 제시한 아래 예제에서 휴대전화 번호, 집 전화번호, 이메일 주소 들을 따로 추출해보자.
data = [
"010.1234.5678", "010-1234-5678", "02-1234-5678", "031-123-4567", "042.987.6543",
"emailID01@naver.com", "emailID02@gmail.com", "Email.03@daum.net", "EM_example.01@gmail.com",
"email_1234@company.co.kr"
]
>>> pattern = "[a-zA-Z]+[a-zA-Z0-9_.]+[@][a-z]+[.][a-z]+[.]*[a-z]*"
>>> regex_pattern = re.compile(pattern)
>>> print(regex_pattern.findall(data))
- re 모듈은 기본적으로 하나의 텍스트 데이터에 대해 동작하므로, for문을 이용해서 하나하나씩 비교하고, 해당하는 list에 담아보도록 하자.
# Data가 담길 기본 list를 생성한다.
cellphone_list = []
homephone_list = []
email_list = []
# 각 카테고리별 패턴
cellPhone_pattern = re.compile("01[016789][\-.][0-9]{4}[\-.][0-9]{4}")
homePhone_pattern = re.compile("0[2-6][1-5]?[\-.][0-9]{3,4}[\-.][0-9]{4}")
email_pattern = re.compile("[a-zA-Z]+[a-zA-Z0-9_.]+[@][a-z]+[.][a-z]+[.]*[a-z]*")
for target in data:
cellPhone_Object = cellPhone_pattern.match(target)
homePhone_Object = homePhone_pattern.match(target)
email_Object = email_pattern.match(target)
if cellPhone_Object != None:
cellphone_list.append(cellPhone_Object.group())
if homePhone_Object != None:
homephone_list.append(homePhone_Object.group())
if email_Object != None:
email_list.append(email_Object.group())
>>> print(f"휴대전화 번호: {cellphone_list}")
>>> print(f"휴대전화 번호: {homephone_list}")
>>> print(f"휴대전화 번호: {email_list}")
휴대전화 번호: ['010.1234.5678', '010-1234-5678']
휴대전화 번호: ['02-1234-5678', '031-123-4567', '042.987.6543']
휴대전화 번호: ['emailID01@naver.com', 'emailID02@gmail.com', 'Email.03@daum.net', 'EM_example.01@gmail.com', 'email_1234@company.co.kr']
- 앞서 학습한 패턴 문자열 생성과 패턴 문자열의 반복 횟수만 지정해주니 아주 간단하게 원하는 패턴만 추출하는 데 성공하였다.
4. 패턴 문자열의 위치 지정
- 패턴 문자열이 맨 뒤에 있는지, 맨 앞에 있는지만을 지정할 수 있다.
- '^': 패턴 문자열 앞에 위치하며, 텍스트의 맨 왼쪽이 해당 패턴 문자열로 시작해야 한다.
- '$': 패턴 문자열 뒤에 위치하며, 텍스트의 맨 오른쪽이 해당 패턴 문자열로 끝나야 한다.
- 아래와 같은 이름 데이터가 있다고 가정해보자.
>>> name = "김영수, 김나연, 최영호, 김영식, 박순자, 최정훈, 박성훈, 김상훈, 최현정, 박성민"
>>> name_pattern = re.compile("^김..")
>>> print(name_pattern.findall(name))
['김영수']
>>> name_pattern = re.compile("^최..")
>>> name_pattern.findall(name)
[]
>>> name_pattern = re.compile("박..$")
>>> name_pattern.findall(name)
['박성민']
>>> name_pattern = re.compile("김..$")
>>> name_pattern.findall(name)
[]
- '^', '$' 패턴은 문자열의 맨 처음과 맨 끝만 중요하게 판단한다.
- 성이 김으로 시작하는 사람을 찾고 싶다면, 물론 간단한 패턴 문자열 생성으로 해결할 수야 있다.
>>> name_pattern = re.compile("[ ]?김..")
>>> name_pattern.findall(name)
['김영수', ' 김나연', ' 김영식', ' 김상훈']
- 그러나 간단한 몇 개의 조작만 한다면, '^', '$' 메타 문자만을 이용해서도 찾을 수 있다.
- 먼저 각 이름의 구분자인 ', '를 줄 바꿈 문자인 '\n'으로 바꿔주자.
>>> name_LF = re.sub(", ", "\n", name)
>>> name_LF
'김영수\n김나연\n최영호\n김영식\n박순자\n최정훈\n박성훈\n김상훈\n최현정\n박성민'
- re.sub(pattern, repl, string): string에서 pattern에 해당하는 문자열을 repl 문자열로 치환한다.
- re.compile에 추가 파라미터 re.MULTILINE(re.M)를 하나 넣어줘 보자.
>>> name_pattern = re.compile("^김..", re.MULTILINE)
>>> name_pattern.findall(name_LF)
['김영수', '김나연', '김영식', '김상훈']
- re.MULTILINE은 문자열 전체의 처음이나 끝이 아닌 각 라인의 처음이나 끝으로 설정해주는 파라미터이다.
- 즉, '^'와 '$'를 각 줄에 적용할 수 있다는 것이다.
- 이름이 "훈"으로 끝나는 사람도 쉽게 찾을 수 있다.
>>> name_pattern = re.compile("..훈$", re.MULTILINE)
>>> name_pattern.findall(name_LF)
['최정훈', '박성훈', '김상훈']
- 이 외에도 "\A", "\Z"가 존재하는데, 각각 "^", "$"와 동일한 기능을 갖는다. 유일한 차이라면, re.MULTILINE 파라미터가 있어도 전체에 대해서 작동한다는 것이다.
5. 기타 메타 문자
- 앞서 소개한 메타 문자 중 "|"와 "()" 이 두 가지를 아직 다루지 않았는데, "|"만 다루고, "()"는 다음 포스팅에서 다루도록 하겠다.
- "()"는 Grouping이라는 것으로, 꽤나 복잡한 녀석이라 다음 포스트에서 자세히 설명해보도록 하겠다. 이번 포스트에선 "|"까지만 다루겠다.
5.1. "|": 또는(Or)
- "A|B" 패턴 문자열이 있다면, A에 해당하는 패턴 문자열이나 B에 해당하는 패턴 문자열에 대해 가지고 온다.
>>> Text_data = "Banana, Apple, WaterMelon, Melon, StrawBerry"
>>> pattern = "Apple|Banana"
>>> regex_pattern = re.compile(pattern)
>>> print(regex_pattern.findall(Text_data))
['Banana', 'Apple']
지금까지 기본적인 정규 표현식 문법 작성 방법에 대해 알아보았다. 지금까지 다룬 내용만으로도 정규 표현식을 사용하는 것은 크게 어렵지 않으나, 혹시나 정규 표현식에 대해 보다 자세히 학습해보고 싶은 사람이 있을 수도 있으므로, 다음 포스트에서는 정규 표현식에 대한 보다 어려운 부분도 다뤄보도록 하겠다.
'Python > Basic' 카테고리의 다른 글
Python-기초:3.1. 정규 표현식(2) - Python re 모듈 (0) | 2021.12.14 |
---|---|
Python-기초:3.0. 정규 표현식(1) - 소개: 정규 표현식과 메타 문자 (0) | 2021.12.14 |
Python-기초: 2.2. 문자열(3) - 문자열 전처리 (0) | 2021.01.20 |
Python-기초: 2.1. 문자열(2) - 문자열 포멧팅 (0) | 2021.01.20 |
Python-기초: 2.0. 문자열(1) - 문자열 생성과 이스케이프 문자 (0) | 2021.01.20 |