본문 바로가기

Data_Analysis_Track_33/Python

Python_10-2(파이썬 정규 표현식)

전시간 복습 ex) (a{3}b) -> a가 m개이고 뒤에 b가 나와야 조건 충족, aaab 는 조건 충족이지만 aaa가 는 조건충족이 되지 않는다.

검색함수
- match(), search() : 패턴과 일치하는 문장이 있는지 여부 확인할 때 사용
- findall() : 패턴과 일치하는 문장을 찾을 때 사용

match 객체 
- 검색 결과를 담아 반환되는 객체
   - match(), search()의 반환타입
- 패턴과 일치한 문자열과 대상문자열 내에서의 위치를 가지고 있는 객체
- 주요 메소드
   - group() : 매치된 문자열들을 튜플로 반환
   - group(subgroup 번호) : 패턴에 하위그룹이 지정된 경우 특정 그룹의 문자열 반환
   - start(), end() : 대상 문자열내에서 시작, 끝 index 반환
   - span() : 대상 문자열 내에서 시작, 끝 index를 tuple로 반환

match(대상문자열 [, pos=0])
- 대상 문자열의 시작 부터 정규식과 일치하는 것이 있는지 조회
- pos : 시작 index 지정
- 반환값
   - Match 객체: 일치하는 문자열이 있는 경우
   - None: 일치하는 문자열이 없는 경우

 

import re

txt = "안녕하세요. 반갑습니다. 제 나이는 20세입니다." # 찾을 경우
# txt = "반갑습니다. 안녕하세요. 제 나이는 20세입니다." # 못찾을 경우(실행해보기)

# 객체지향 방식 -> Pattern객체를 생성한 뒤 그 메소드를 이용해 패턴의 대상문자열들을 처리.
## 1. 패턴 객체를 생성
p = re.compile(r"\w*하세요")  # 패턴객체 생성, \w{2} - 일반문자, 숫자, _ 두글자
print(type(p))
result = p.match(txt) # txt가 p의 패턴으로 시작하는지 여부
if result is not None: # 찾았으면
    print(type(result))
    print(result)
    print("위치(index):", result.span())
    print(f"시작위치: {result.start()}, 종료위치(index): {result.end()}")
    print(f"찾은 문자열: {result.group()}")
else:
    print(result)
    print("지정한 패턴으로 시작하지 않는 문자열입니다.")

 

txt = "안녕하세요. 반갑습니다. 제 나이는 20세입니다." # 찾을 경우
# txt = "반갑습니다. 안녕하세요. 제 나이는 20세입니다." # 못찾을 경우

# 함수를 이용한 방식
# re 모듈의 match()함수를 호출 -> pattern객체의 match()메소드와 동일한 기능
result = re.match(r"\w{2}하세요", txt) # (패턴, 대상문자열)
print(result)
if result:
    print(result.group()) # 매치된 문자열 출력
else:
    print("찾는 패턴으로 시작하지 않습니다.")


search(대상문자열 [, pos=0])
- 대상문자열 전체 안에서 정규식과 일치하는 것이 있는지 조회
- pos: 찾기 시작하는 index 지정
- 반환값
   - Match 객체: 일치하는 문자열이 있는 경우
   - None: 일치하는 문자열이 없는 경우|

txt = "안녕하세요. 반갑습니다. 제 나이는 20세입니다."

pattern = r"\d{2}" # 숫자 두개

result = re.search(pattern, txt) # 전체 문장안에 패턴이 있는지 확인.
print(result)

# 전체 문장 안에 패턴이 있는지 확인. (다 찾지 않는다. 첫번째 것을 찾으면 더이상 찾지 않음
result2 = re.search(pattern, txt)
print(result2)
print(txt[result2.start()]) # 매치된 문자열의 시작 index 출력

 

txt = "가격은 4000, 5000, 15000원 입니다."

# p = re.compile(r"\d") -> \d 는 숫자 1개만 찾으면 끝난다.
# p = re.compile(r"\d+") # 숫자 1개 이상이 연결 , \d+ -> 첫번째 숫자부터 숫자로 연결되는 위치까지 찾는다.
# p = re.compile(r"\d{5}") # 숫자 5개가 연결
p = re.compile(r"\d{4,7}")  # 숫자가 4개 이상, 7개 이하 연결 -> 천 ~ 백만단위


result = p.search(txt)
if result:
    print(result)
    print(result.group())
    print(result.span())
else:
    print("찾는 패턴이 없습니다.")


findall(대상문자열)
- 대상문자열에서 정규식과 매칭되는 문자열들을 리스트로 반환
- 반환값
   - 리스트(List) : 일치하는 문자열들을 가진 리스트를 반환
   - 일치하는 문자열이 없는 경우 빈 리스트 반환

txt = "가격은 4000, 5000, 15000원 입니다. 물건은 각각 10, 20, 100개 있습니다."

p = re.compile(r"\d+") # 숫자 하나 이상 \d{1,}와 동일
# p = re.compile(r"\d{10}") # 패턴의 문장(일치하는 문자열)이 없을 경우 빈 리스트 반환
result = p.findall(txt)
print(type(result))
print(result)

 

# finditer(대상문자열) -> 찾은 결과를 조회할 수 있는 Iterator를 반환
# Iterator는 각 결과를 Match객체로 반환한다.
result = p.finditer(txt)

print(type(result))
for match in result:
    print(match)


finditer(대상문자열) 
- 찾은 결과를 조회할 수 있는 Iterator를 반환
- Iterator는 각 결과를 Match객체로 반환한다.

https://regexr.com/3bfsi -> 정규표현식에 대한 많은 데이터가 있다.

여기서 이메일, 전화번호 등 많은 정규표현식을 표현하는 방법들을 확인할 수 있다.

문자열 변경
- sub(): 변경된 문자열 반환
- subn(): 변경된 문자열, 변경개수 반환

sub(바꿀문자열, 대상문자열 [, count=양수])
- 대상문자열에서 패턴과 일치하는 것을 바꿀문자열로 변경한다.
- count: 변경할 개수를 지정. 기본: 매칭되는 문자열은 다 변경
- 반환값: 변경된 문자열

subn(바꿀문자열, 대상문자열 [, count=양수])
- sub()와 동일한 역할.
- 반환값 : (변경된 문자열, 변경된문자열개수) 를 tuple로 반환

 

txt = "   오늘은  화요일   입니다.   " .strip() # strip() -> 좌우공백제거
print(txt)
# 여러개의 공백을 한 개의 공백으로 변환
# \s -> 공백문자, tab, 엔터 포함
pattern = r" {2,}" # -> 공백 두글자 이상

result = re.sub(pattern, " ", txt) # 함수(패턴표현식, 바꿀문자열, 대상문자열)
print(result)

 

txt = """오늘은   화요일   입니다.
내일은   수요일   입니다.
모래는   목요일   입니다.
글피는   금요일   입니다.
사흘 후는   토요일   입니다.
"""
# pattern = r"\s+" # 공백, 엔터, tab을 모두 공백 한개로 변환
pattern = r" {2,}"
p = re.compile(pattern)
result = p.sub(" ", txt)
print(result)

 

txt = "test1, test2, test3, test4"
# 숫자 빼고 다 제거

p = re.compile(r"\D")
result = p.sub("", txt)  # "" -> 제거한다는 의미이다.
print(result)  # '1234'

txt = """오늘은   화요일   입니다.
내일은   수요일   입니다.
모래는   목요일   입니다.
글피는   금요일   입니다.
사흘 후는   토요일   입니다.
"""
# pattern = r"\s+" # 공백, 엔터, tab을 모두 공백 한개로 변환
pattern = r" {2,}"
p = re.compile(pattern)
result, cnt = p.subn(" ", txt) # subn() 사용
print(result)
print("변경된 개수", cnt)


나누기(토큰화)
split(대상문자열)
- pattern을 구분자로 문장을 나눈다.
- 반환: 나눈 문자열을 원소로 하는 리스트

 

fruits = '사과,배+복숭아,수박{파인애플' # -> 나누는 문자열이 여러개가 필요하다.
# fruits.split(", ") 
result = re.split(r"[,+{]", fruits) # split을 사용해 패턴을 구분자로 문장을 나눈다.
result


패턴내 하위패턴 만들기 (Grouping)
- 전체 패턴에서 일부 패턴들을 묶어 하위패턴으로 만든다.
- 구문: (패턴)

그룹핑 (전체 패턴 내에서 일부 패턴을 조회)

 

import re
# subgroup(하위 그룹)을 이용
tel = "tel:010-1234-5678"
pattern = r"(0\d{1,2})-(\d{3,4})-(\d{4})" 
# (1번하위그룹)-(2번하위그룹)-(3번하위그룹)
p = re.compile(pattern)
result = p.search(tel) # 전체 패턴으로 찾는다.
if result: # 조회 결과가 있다면
    print(result)
    print("찾은문자열:", result.group(), result.group(0)) # group(양의정수:그룹번호)
    print(result.group(1),result.group(2),result.group(3))
    # # 국번
    # l = result.group().split('-')
    # print("국번:",l[1])
else:
    print("조회결과 없음")

emails = "abc@daum.net, test@naver.com, mymail@gmail.com"
# email 주소 : 계정@도메인 -> 계정과 도메인을 각각 출력

pattern = r"([a-zA-Z0-9._+-]+)@([a-zA-Z0-9]+\.[a-zA-Z]{2,4})\b"

p = re.compile(pattern)
result = p.findall(emails) # 리스트 반환

for v in result:
    print(v, v[0], v[1])

 

result_iter = p.finditer(emails) # finditer 사용
print(result_iter)
for match in result_iter:
    print(match)
    print(match.group())
    print(match.group(1), match.group(2))

 

# next 활용
result_iter = p.finditer(emails)
# print(result_iter)
print(next(result_iter))
next(result_iter) # StopIteration 오류 발생까지 반복 실행해보기, 보통 데이터 분류에서 for in 문을 더 많이 사용한다.


패턴 내에서 하위그룹 참조
- \번호
- 지정한 '번호' 번째 패턴으로 매칭된 문자열과 같은 문자열을 의미

 

txt = """010-1111-2222
010-2222-2222
010-3333-1234
010-3456-3456
"""
# 국번과 뒷자리번호가 같은 전화번호를 조회
tel_pattern = r"\d{3}-(\d{3,4})-\1"
# 번호-(번호:subgroup 1)-\1          \1 -> 1번 subgroup으로 찾은 값과 같은 값 -> 중간 네자리값과 끝 네자리값이 같은 전화번호를 찾는다.
p = re.compile(tel_pattern)
for m in p.finditer(txt):
    print(m.group())


패턴 내의 특정 부분만 변경

 

print(info) 
# 주민번호 뒷자리 중 뒤의 6자리 감추기 -> #으로 변경
# 주민번호를 조회한 뒤에 뒤 6자리만 변경
jumin_pattern = r"(\d{6}-[012349])\d{6}" # 변경안될부분을 subgroup으로 묶어준다.
p = re.compile(jumin_pattern)
result = p.sub(f"\g<1>XXXXXX", info) # \g<1> 1번 subgroup으로 찾은 것을 그대로 사용
print(result)
print(info) # 실제값은 변경되지 않는다.


group으로 묶인 것 참조(조회)
- 패턴 안에서 참조
   - \번호 , r'(\d{3}) \1' => 중복되는 것을 패턴으로 표현할 때.
- match 조회
   - match객체.group(번호)
- sub() 함수에서 대체 문자로 참조
   - \g<번호>

 

# subgroup의 번호
# 하위그룹이 중첩된 경우 번호?
# outer가 우선, 왼쪽이 우선
tel = "010-1111-2222"
pattern = r"((\d{2,3})-((\d{3,4})-(\d{4})))"
# (1 (2) - (3 (4) - (5)))
p = re.compile(pattern)
result = p.search(tel)
print(result.group())
print("group1 : ", result.group(1))
print("group2 : ", result.group(2))
print("group3 : ", result.group(3))
print("group4 : ", result.group(4))
print("group5 : ", result.group(5))


Greedy 와 Non-Greedy
- Greedy(탐욕스러운-최대일치) 의미
   - 주어진 패턴에 만족하는 문자열을 최대한 넓게(길게) 잡아 찾는다.
   - 매칭시 기본 방식
- Non-Greedy(최소일치)
   - 주어진 패턴에 만족하는 문자열을 최초의 일치하는 위치까지 찾는다
   - 개수를 나타내는 메타문자(수량자)에 **?**를 붙인다.
   - *?, +?, {m,n}?

 

txt = "<div>파이썬 <b>정규표현식</b></div>"
# 태그만 조회 : <div> <b> </b> </div> 들을 조회하려고 한다.
# pattern = r"<[\w/]+>"
pattern = r"<.+>" # -> Greedy 탐색 : 최대한 길게 잡아서 찾는다. (default 방식)
p = re.compile(pattern)
result = p.findall(txt)
len(result), result

txt = "<div>파이썬 <b>정규표현식</b></div>"
# 태그만 조회 : <div> <b> </b> </div> 들을 조회하려고 한다.
pattern = r"<.+?>" # Non-Greedy 탐색
p = re.compile(pattern)
result = p.findall(txt)
len(result), result


전방/후방 탐색
- 패턴과 일치하는 문자열을 찾을 때는 사용하되 반환(소비) 되지 않도록 하는 패턴이 있을 때 사용.
- 전방탐색

   - 반환(소비)될 문자열들이 앞에 있는 경우.
   - 긍정 전방탐색
      - %%%(?=패턴) : %%%-반환될 패턴
   - 부정 전방탐색
      - %%%(?!패턴) : 부정은 =를 !로 바꾼다.
- 후방탐색
   - 반환(소비)될 문자열이 뒤에 있는 경우.
   - 긍정 후방탐색
      - (?<=패턴)%%%
   - 부정 후방탐색
      - (?<!패턴)%%%

 

# 전방탐색
info = """TV 30000원 30개
컴퓨터 32000원 50개
모니터 15000원 70개
"""

# info에서 가격만 조회
pattern = r"\d+(?=원)" # (? 패턴) -> subgroup의 소괄호가 아니고 단순히 묶어주는것이다.
# \d+(?=원) -> 받을때 : \d+원, 찾은값에서 사용할 것 : \d+ . (?='패턴') 이 '패턴'으로 찾은 값은 사용하지 않는다.
p = re.compile(pattern)
print(p.findall(info))
for price in p.findall(info):
    print(int(price) * 0.8)

 

# 후방탐색
info = """TV $30000 30개
컴퓨터 $32000 50개
모니터 $15000 70개
"""
#  $는 메타문자 -> literal 사용할 경우 '\'를 붙인다.
pattern = r"(?<=\$)\d+" # (?<=패턴1)패턴2 -> 패턴1은 조회할 때는 포함, 값을 사용할 때는 제거하고 뒤의 패턴2의 값만 사용
p = re.compile(pattern)
print(p.findall(info))
for price in p.findall(info):
    print(int(price) * 0.8)