■ 정규 표현식 (Regular Expression)
- 정규 표현식은 복잡한 문자열을 처리할 때 사용하는 기법
[사례] 주민등록번호를 포함하고 있는 텍스트에서 주민등록번호 뒷자리를 * 문자로 변경 - 주민등록번호 문자열은 주민등록번호 형식인지 조사한다. |
- 일반 파이썬 문법을 이용한 코드
data = """
park 800904-1234567
kim 841204-1034562
"""
result = []
for line in data.split('\n'):
word_result = []
for word in line.split():
if len(word) == 14 and word[:6].isdigit() and word[7:].isdigit: # 숫자인지 체크
word = word[:6] + '-' + '*******'
word_result.append(word)
result.append(' '.join(word_result)) # 이름 한칸 띄고 주민번호 출력
print('\n'.join(result))
- 정규 표현식을 이용한 코드
import re
data = """
park 800904-1234567
kim 841204-1034562
"""
pat = re.compile('(\d{6})[-]\d{7}')
# compile(): 패턴을 만드는 함수, \d{6}: 숫자 6자리와 일치, (괄호)는 그룹을 의미
print(pat.sub(r'\1-*******', data).strip())
# sub(): replace함수와 비슷, 1번 그룹-뒤에는 *으로 대체, strip()은 공백 제거
■ 메타 문자
- 정규 표현식에 사용되어 지는 특별한 의미를 갖는 문자
. ^ $ * + ? { } [ ] \ | ( ) |
■ 문자 클래스 [ ]
- [ ] 안에 포함된 문자들 중 하나와 매치 여부를 확인한다.
정규식 | 문자열 | 매치 여부 |
[abc] | a | Yes |
before | Yes | |
dude | No |
- 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위(from-to)를 의미한다.
[a-c] → [abc], [0-5] → [012345]와 동일
- 문자 클래스 안에 ^ 메타 문자가 사용되면 not의 의미를 갖는다. [^0-9]는 숫자가 아닌 문자만 매치한다.
- ^ 메타 문자가 [] 밖에 사용되면 특정 문자로 시작하는지 판단
■ 자주 사용하는 문자 클래스
- 자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다.
정규 표현식 | 설명 |
\ | 정규 표현식에서 사용하는 문자 그대로 표현하려면 앞에 \를 붙인다 (예 : \+, \*) |
\d | 숫자와 매치, [0-9]와 동일한 표현식 |
\D | 숫자가 아닌 것과 매치, [^0-9]와 동일한 표현식 |
\s | 공백문자(space, tab)와 매치, [\t\n\r\f\v]와 동일한 표현식 |
\S | 공백문잘가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일한 표현식 |
\w | 문자+숫자와 매치, [a-zA-Z0-9]와 동일한 표현식 |
\W | 문자+숫자가 아닌 문자와 매치, [^a-zA-Z0-9]와 동일한 표현식 |
■ Dot(.)
- Dot(.) 메타 문자는 줄바꿈 문자인 \n을 제외한 모든 문자와 매치됨을 의미한다.
- [] 사이에 . 을 사용할 경우 문자 원래의 의미인 마침표가 된다.
정규식 | 문자열 | 매치 여부 |
a.b | aab | Yes |
a0b | Yes | |
abc | No |
■ 반복(*)
- *은 * 바로 앞에 있는 문자가 0부터 무한대로 반복될 수 있다는 의미이다.
정규식 | 문자열 | 매치 여부 |
ca*t | ct | Yes |
cat | Yes | |
caaat | Yes |
■ 반복(+)
- +는 최소 1번 이상 반복될 때 사용한다.
정규식 | 문자열 | 매치 여부 |
ca+t | ct | No |
cat | Yes | |
caaat | Yes |
■ 반복 횟수 지정({m, n})
- { } 메타 문자는 반복 횟수를 지정할 수 있다. {m, n} 정규식을 사용하면 반복 횟수가 m부터 n까지 매치할 수 있다. m 또는 n을 생략하거나 정수 한 개만 쓸 수 있다. {m, }이면 반복 횟수가 m이상인 경우이고, { ,n}이면 반복 횟수가 n이하를 의미한다.
정규식 | 문자열 | 매치 여부 |
ca{2}t | cat | No |
caat | Yes |
정규식 | 문자열 | 매치 여부 |
ca{2,5}t | cat | No |
caat | Yes | |
caaaaat | Yes |
■ ?
- ? 앞에 문자가 하나 있거나 없을 때 매치된다.
정규식 | 문자열 | 매치 여부 |
go?d | gd | Yes |
god | Yes | |
good | No |
■ $
- $ 앞에 있는 문자로 끝나면 매치된다. 여러 줄의 문자열일 경우 마지막 줄만 적용된다.
정규식 | 문자열 | 매치 여부 |
a$ | a | Yes |
baa | Yes | |
aabb | No |
■ ^
- ^ 다음에 있는 문자로 시작하면 매치된다.
정규식 | 문자열 | 매치 여부 |
^a | aaa | Yes |
abc | Yes | |
good | No |
■ 파이썬에서 정규 표현식 사용
- re 모듈 사용
> re 모듈의 compile 함수를 이용하여 정규 표현식을 컴파일 한다.
import re
p = re.compile('ab*')
- 정규식을 사용한 문자열 검색
- 컴파일 된 객체를 이용하여 다음의 함수를 이용하여 문자열 검색을 할 수 있다.
함수 | 설명 |
match() | 문자열의 처음 시작부터 검색하여 일치하지 않는 부분이 나올 때까지 찾는다 |
search() | 문자열 전체를 검색하여 처음으로 매치되는 문자열을 찾는다 |
findall() | 정규식과 매치되는 모든 문자열을 찾아 리스트로 반환한다 |
finditer() | 정규식과 매치되는 모든 문자열을 반복 가능한 객체로 반환한다 |
# 컴파일과 매치의 동시 수행
m = re.match('[a-z]+', 'python')
# match() : 문자열의 처음부터 정규식과 매치되는지 조사
m1 = p.match('python')
print(m1) # <re.Match object; span=(0, 6), match='python'>
m2 = p.match('pYthon')
print(m2) # <re.Match object; span=(0, 1), match='p'>
m3 = p.match('3 python')
print(m3) # None : 처음부터 숫자가 나왔기 때문에
# search() : 문자열 전체를 검색하여 **처음으로** 매치되는 문자열을 찾는다
m = p.search('3 python')
print(m.group()) # group() : 매치된 것만 추출 python
[문제] 전화번호 추출하기
다음의 전화번호 데이터에서 전화번호만 추출하는 정규 표현식을 작성하세요.
import re
phone = ['홍길동:010-1234-5678', '우리집:02-555-3333']
pat = re.compile('\d{2,3}-\d{3,4}-\d{4}') # 전화번호 패턴
for p in phone:
s = pat.search(p)
print(s.group())
# findall() : 정규식과 매치되는 모든 문자열을 찾아 리스트로 반환한다
p = re.compile('[a-z]+')
result = p.findall('life is too short')
print(result) # ['life', 'is', 'too', 'short']
# finditer() : 정규식과 매치되는 모든 문자열을 반복 가능한 객체로 반환한다
result = p.finditer('life is too short')
for m in result:
print(m)
# <re.Match object; span=(0, 4), match='life'>
# <re.Match object; span=(5, 7), match='is'>
# <re.Match object; span=(8, 11), match='too'>
# <re.Match object; span=(12, 17), match='short'>
■ Match 객체 함수
함수 | 설명 |
group() | 매치된 문자열을 반환한다 |
start() | 매치된 문자열의 시작 위치를 반환한다 |
end() | 매치된 문자열의 끝 위치를 반환한다 |
span() | 매치된 문자열의 시작과 끝 값을 튜플로 반환한다 |
import re
p = re.compile('[a-z]+')
m = p.search('python')
print(m.group())
print(m.start()) # 시작 위치
print(m.end()) # 끝 위치
print(m.span()) # 시작과 끝
■ 컴파일 옵션
옵션 | 약어 | 설명 |
DOTALL | S | dot(.) 메타 문자가 줄바꿈 문자('\n')를 포함하여 모든 문자와 일치한다 |
IGNORECASE | I | 대소문자에 관계 없이 일치한다 |
MULTILINE | M | 여러 줄의 문자열에 대해 ^, $ 메타문자를 적용할 수 있다 ※ ^는 문자열의 처음을, $는 문자열의 마지막을 의미 |
VERBOSE | X | 정규식을 보기 편하게 만들고 주석 등을 사용할 수 있게 한다 |
▶ DOTALL
- dot(.) 메타 문자가 줄바꿈 문자(\n)을 포함하여 모든 문자와 일치한다.
m = re.match('a.b', 'a\nb') # \n은 해당 사항 없음
print(m) # 매치 결과 없음
p = re.compile('a.b', re.DOTALL) # \n까지 포함
m = p.match('a\nb')
print(m) # <re.Match object; span=(0, 3), match='a\nb'>
s = '''hello
python'''
p = re.compile('hello.python')
m = p.match(s)
print(m) # 개행 문자가 있기 때문에 결과 없음
s = '''hello
python'''
p = re.compile('hello.python', re.DOTALL)
m = p.match(s)
print(m) # <re.Match object; span=(0, 12), match='hello\npython'>
▶ IGNORECASE
- 대소문자에 관계 없이 매치한다.
p = re.compile('[a-z]+', re.IGNORECASE)
m = p.match('pYthon')
print(m) # <re.Match object; span=(0, 6), match='pYthon'>
▶ MULTILINE
- 여러 줄의 문자열에 대해 ^, $ 메타 문자를 적용할 수 있다.
# python으로 시작하면서 공백 문자 하나, 숫자 또는 문자가 나와야 함
p = re.compile('^python\s\w+')
text = '''python one
life too short
python two
you need python
python three'''
m = p.findall(text)
print(m) # 첫줄만 반환 : ['python one']
# python으로 시작하면서 공백 문자 하나, 숫자 또는 문자가 나와야 함
p = re.compile('^python\s\w+', re.MULTILINE)
text = '''python one
life too short
python two
you need python
python three'''
m = p.findall(text)
print(m) # ['python one', 'python two', 'python three']
p = re.compile('python\s\w+')
text = '''python one
life too short
python two
you need python
python three'''
m = p.findall(text)
print(m) # ^를 빼면 : ['python one', 'python two', 'python\npython']
■ 백슬래시
[가정] "\section" 문자열을 찾기 위한 정규식을 만들어야 하는 상황
→ re.compile("\section")
- 이 정규식은 \s 문자가 공백으로 해석되어 의도한 대로 매치가 이뤄지지 않음
- 위쪽의 정규식은 [\t\n\r\f\v]ection과 같은 의미로 해석 됨
- 결국 정규식에서 사용한 \ 문자가 문자열 자체임을 알려 주기 위해 \를 2개 사용하여 이스케이프 처리를 해야 한다
>>> re.compile("\\section")
- 하지만 파이썬 정규식 엔진에는 파이썬 문자열 리터럴 규칙에 따라 \\ → \로 변경되어 결국 \section이 전달된다
- 결국 파이썬 정규식 엔진에 \\ 문자를 전달하기 위해서는 백슬래시 4개 \\\\를 사용해야 한다
- 이러한 문제로 인해 파이썬 정규식에는 Raw String 규칙이 생기게 됐다.
- 따라서 다음과 같이 정규식 문자열 앞에 r 문자를 삽입하면 Raw String 규칙에 의해 \를 1개만 써도 동일한 의미를 갖게 된다. >>> re.compile(r'\\section')
import re
p = re.compile('\\section') # '\section'으로 해석됨, [\t\n\r\f\v]ection 문자열과 일치됨을 찾음
m = p.search('What is \section and example?')
print(m) # \section -> ection으로 수정하면 검색 됨
p = re.compile(r'\\section') # Raw String 사용
m = p.search('What is \section and example?')
print(m) # <re.Match object; span=(8, 16), match='\\section'>
■ 메타 문자
메타 문자 | 설명 |
| | or의 의미 |
^ | 문자열의 처음과 일치함을 의미 |
$ | 문자열의 끝과 일치함을 의미 |
\A | 문자열의 처음과 일치함을 의미. ^은 MILTILINE 옵션을 사용할 경우 각 줄의 문자열의 처음과 매치되지만 \A는 줄과 상관 없이 전체 문자열의 처음하고만 일치된다 |
\Z | 문자열의 끝과 일치함을 의미. 여러 줄로 작성된 문자열에서 줄과 상관없이 전체 문자열의 끝과 일치된다 |
\b | 단어 경계를 나타낸다. 단어 경계는 단어 문자와 비단어 문자 사이의 위치를 의미한다. 여기서 단어 문자는 보통 알파벳, 숫자, 밑줄 문자를 포함하며, 비단어 문자는 그 외의 문자이다. \b는 백스페이스를 의미하므로 단어 구분자로 사용되기 위해서는 Raw String임을 알려주는 r을 반드시 붙여줘야 한다 |
\B | \b와 반대로 단어 앞뒤가 공백으로 구분된 단어가 아닌 경우에만 일치된다 |
▶ |
p = re.compile('Crow|Servo') # 둘중에 하나만 일치하면 일치
m = p.match('ServoHello')
print(m) # <re.Match object; span=(0, 5), match='Servo'>
▶ $
print(re.search('short$', 'Life is too short')) # short으로 끝나는지 여부
print(re.search('short$', 'Life is too short, you need python'))
▶ \A
p = re.compile('\Apython\s\w+', re.MULTILINE)
text = '''python one
life too short
python two
you need python
python three'''
m = p.findall(text)
print(m)
▶ \b
p = re.compile(r'\bclass\b') # 공백을 포함하지 않고 매치 결과 반환
print(p.search('no class at all')) # <re.Match object; span=(3, 8), match='class'>
p = re.compile(r'\sclass\s') # 공백을 포함해서 매치 결과 반환
print(p.search('no class at all')) # <re.Match object; span=(2, 9), match=' class '>
p = re.compile(r'\bclass\b')
print(p.search('class at all')) # <re.Match object; span=(0, 5), match='class'>
print(p.search('one subclass')) # None
▶ \
- 정규 표현식에서 사용하는 문자 그대로 표현하려면 앞에 \를 붙여준다.
- 즉, 문자열 안에 포함된 메타 문자(. ? $ 등)를 원래 문자로 사용
# '안녕하세요?'는 ?앞에 문자가 하나 있거나 없으면 매치
# 즉, '안녕하세요', '안녕하세' 문자열과 일치
m = re.search('안녕하세요?', '여러분 안녕하세요?')
print(m) # <re.Match object; span=(4, 9), match='안녕하세요'> 물음표 없음
m = re.search('안녕하세요\?', '여러분 안녕하세요?') # ?는 메타 문자가 아닌 문자 자체로 인식
print(m) # <re.Match object; span=(4, 10), match='안녕하세요?'>
■ 그룹핑
- () 메타 문자는 그룹을 만든다
- 그룹을 만들면 group() 함수를 사용하여 그룹핑된 부분의 문자열만 뽑아낼 수 있다
group(인덱스) | 설명 |
group(0) | 일치된 전체 문자열, group()와 동일 |
group(1) | 첫 번쩨 그룹에 해당하는 문자열 |
group(n) | n 번째 그룹에 해당하는 문자열 |
- '\번호'를 이용하면 번호에 해당하는 그룹을 재참조 한다. 표현식은 Raw String으로 선언해야 한다.
- 그룹핑에 이름 붙이기 : (?p<그룹 이름>)
import re
p = re.compile('(ABC)+') # ABC가 한번 이상 반복
m = p.search('ABCABCABC OK?')
print(m) # <re.Match object; span=(0, 9), match='ABCABCABC'>
print(m.group()) # or group(0), 일치된 전체 문자열 : ABCABCABC
print(m.group(1)) # 첫 번쨰 그룹에 해당하는 문자열 : ABC
# 이름과 전화번호를 그룹으로 지정하여 분리
s = 'park 010-1234-5678'
# 문자나 숫자 하나 이상, 공백 하나 이상, 숫자 하나 이상
p = re.compile('(\w+)\s+(\d+[-]\d+[-]\d+)')
m = p.search(s)
print(m.group()) # 전체 출력
print(m.group(1)) # 이름만 출력
print(m.group(2)) # 전화번호만 출력
# 국번만 추출하고자 할 경우
# 그룹이 중첩되어 있는 경우는 바깥쪽부터 시작하여 안쪽으로 들어갈수록 인덱스가 증가
s = 'park 010-1234-5678'
p = re.compile('(\w+)\s+((\d+)[-]\d+[-]\d+)') # 그룹 : 이름이 1번, 전체 전화번호가 2번, 국번이 3번
m = p.search(s)
print(m.group(3))
- '\번호'를 이용한 재참조
- Raw String을 이용해야 함
re.match(r'(a)(b)\1\2', 'abab') # abab와 일치하는지
# 동일한 단어가 연속적으로 사용된 문자열 찾기
# \b(\w+)\b : 단어 경계를 기준으로 문자와 숫자로 이루어진 한 단어를 찾는다. 그리고 그 단어를 그룹으로 지정
# \s+ : 하나 이상의 공백 문자를 찾는다.
# \b\1\b : 단어 경계를 기준으로 한 단어를 찾으며, 그룹 참조를 통해 이전에 그룹화된 단어와 동일한 단어를 찾는다.
p = re.compile(r'\b(\w+)\b\s+\b\1\b')
m = p.search('I have a dog dog in my house')
print(m.group()) # dog dog
# 그룹에 이름 붙이기
# (?P<그룹 이름>)
p = re.compile('(?P<name>\w+)\s+(\d+[-]\d+[-]\d+)')
m = p.search('park 010-1111-2222')
print(m.group('name'))
■ 전방 탐색
정규식 | 종류 | 설명 |
표현식1(?=표현식2) | 긍정 전방 탐색 | 표현식 1 뒤의 문자열이 표현식2와 매치되면 표현식1 일치. 표현식2의 문자열은 결과로 반환하지 않는다 |
표현식1(?!표현식2) | 부정 전방 탐색 | 표현식1 뒤의 문자열이 표현식2와 매치되지 않으면 표현식1 일치. 표현식2의 문자열은 결과로 반환하지 않는다 |
- 긍정 전방 탐색
import re
# URL에서 프로토콜 이름만 검색
p = re.compile('.+(?=:)') # .+ 어떤 문자든 한 글자 이상, 앞에서부터 : 전까지 찾음
m = p.search('http://www.google.com')
print(m.group()) # http
- 부정 전방 탐색
# 파일 이름의 확장자 중 bat 파일만 제외하고 추출
file_names = ['autoexe.bat', 'python.exe', 'sysinfo.cf']
p = re.compile('[a-zA-Z]+\w*[.](?!bat)[a-zA-Z]+')
for file in file_names:
m = p.search(file)
if m: # 만약 검색이 되면
print(m.group())
■ 후방 탐색
- 텍스트를 반환하기 전에 뒤쪽을 탐색하는 것
정규식 | 종류 | 설명 |
(?<=표현식2) 표현식1 | 긍정 후방 탐색 | 표현식 1 앞의 문자열이 표현식2와 매치되면 표현식1 일치. 표현식2의 문자열은 결과로 반환하지 않는다 |
(?<!표현식2) 표현식1 | 부정 후방 탐색 | 표현식1 앞의 문자열이 표현식2와 매치되지 않으면 표현식1 일치. 표현식2의 문자열은 결과로 반환하지 않는다 |
- 긍정 후방 탐색
p = re.compile('(?<=\$)[0-9]+[.][0-9]+') # 뒤에서부터 $가 나오기 전까지
m = p.search('ABC01: $23.45')
print(m.group()) # 23.45
# 금액에 소수점이 없는 숫자인 경우를 고려한 패턴
p = re.compile('(?<=\$)[0-9]+[.]?[0-9]*')
m = p.search('ABC01: $23')
print(m.group())
- 전후방 동시 탐색
# <p> ~ </p> 태그 사이의 문자열 추출하기
# 뒤에서 탐색했을 시 <p>까지 일치가 되어야 하고, 앞에서 탐색했을 시 </p>까지 일치가 되어야 함.
p = re.compile('(?<=<p>)\w+(?=</p>)')
m = p.search('kakao <p>ryan</p> keep a straight face.')
print(m.group()) # ryan
■ 문자열 바꾸기
# Pattern 객체.sub(바꿀 문자열, 대상 문자열, 바꿀 횟수)
p = re.compile('(blue|white|red)')
# count = 0 이면 전체를 다 바꾼다
p.sub('color', 'blue socks and red shoes', count=0)
# 'color socks and color shoes'
[문제] 이메일 형식 검증
- 사용자로부터 입력 받은 임의의 이메일 주소에 대해 유효한 형식인지를 검증하는 함수를 작성하세요.
def valid_email(email):
return '이메일 검증 결과'
일반적인 이메일은 '알파벳 및 숫자@도메인 이름'으로 되어 있다.
위 함수는 다음의 이메일을 올바른 이메일 형식으로 결과 값을 반환한다.
- mike@korea.co.kr
- mike@daum.net
- mike.kim@gmail.com
위 함수는 다음의 이메일을 올바른 이메일 형식이 아님을 결과 겂으로 반환한다.
- mysite.com
- mike@good
import re
def valid_email(email):
regex = '[a-zA-Z]+\w*[.]?\w*[@]\w+[.]\w+[.]?\w{2,3}'
valid = re.match(regex, email)
if valid:
return 'valid email'
else:
return 'invalid email'
input_email = input('이메일 입력:')
print(input_email, end=': ')
print(valid_email(input_email))
[실습] 한글 찾기 / 한글 제거
s = '한글이에요. good morning. 안녕하세요'
m = re.findall('[ㄱ-힣]+', s) # 한글만 찾음
print(m) # ['한글이에요', '안녕하세요']
s = '한글이에요. good morning. 안녕하세요'
m = re.findall('[a-zA-Z]+', s) # 한글만 제거
print(m) # ['good', 'morning']
[문제] 주민번호의 유효성 검증
사용자로부터 입력 받은 임의의 주민등록번호가 올바른 형식인지를 검증하는 정규 표현식을 작성하세요.
주민번호 형식: 생년월일(6자리)-성별(1자리)나머지 6자리 숫자
- 년도는 숫자 2자리
- 월은 앞자리가 0일 경우 뒷자리는 1~9까지 허용 또는 앞자리가 1일 경우 뒷자리는 0~2까지 허용
- 일은 앞자리가 0일 경우 뒷자리는 1~9까지 허용 또는 앞자리가 1 또는 2일 경우 뒷자리는 0~9까지 허용, 앞자리가 3일 경우 뒷자리는 0~1만 하용
- 성별 숫자는 1~4까지 허용
- 나머지 6자리는 숫자
jumin = input('주민등록번호 입력:')
p = re.compile('\d{2}(0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[0-1])[-][1-4]\d{6}')
m = p.match(jumin)
if m:
print('유효한 주민번호 형식입니다.')
else:
print('유효하지 않은 주민번호 형식입니다.')
'데이터 분석 > 웹 스크래핑' 카테고리의 다른 글
[Web Scrapping] BeautifulSoup 모듈 (0) | 2023.10.31 |
---|---|
[Web Scrapping] DOM (Document Object Model) (0) | 2023.10.31 |
[Web Scrapping] request 모듈 (0) | 2023.10.30 |
[Web Scrapping] HTTP 요청 / 응답 (0) | 2023.10.30 |
[Web Scrapping] Web 구성 요소 - 3 (1) | 2023.10.30 |