목차
- 1. 메타 문자란?
- 2. 문자 클래스
[ ]- 3. 자주 사용하는 문자 클래스
\d,\s,\w- 4. Dot
.- 5. 반복
*,+,{},?- 6. 파이썬
re모듈- 7.
match()와search()- 8.
findall()과finditer()- 9. Match 객체
- 10. 정규식 컴파일 옵션
- 11. 역슬래시와 Raw String
정규 표현식에서는 일반적인 문자 외에도 특별한 의미를 가진 문자를 사용한다.
이러한 문자를 메타 문자(Meta Character) 라고 한다.
대표적인 메타 문자는 다음과 같다.
. ^ $ * + ? { } [ ] \ | ( )
예를 들어 일반 문자열에서
*
은 단순한 별표 문자이지만 정규식에서는
앞의 문자가 0번 이상 반복
이라는 특별한 의미를 가진다.
따라서 정규식을 공부할 때는
각 메타 문자가 어떤 규칙을 표현하는지 이해하는 것이 핵심이다.
정규식에서는 매치된다라는 표현을 많이 사용한다.
매치란
문자열이 정규식에서 지정한 패턴과 일치한다
라는 의미이다.
예를 들어 정규식이
abc
이고 문자열도
abc
라면
정규식 : abc
문자열 : abc
→ Match O
라고 할 수 있다.
[ ]가장 먼저 알아볼 메타 문자는
[ ]
이다.
[]는
대괄호 안에 있는 문자 중 하나
와 매치한다.
예를 들어
[abc]
는
a 또는 b 또는 c
중 하나를 의미한다.
[abc] 예제정규식 : [abc]
문자열을 비교해 보자.
"a"
→ a가 존재
→ Match O
"banana"
→ b 또는 a가 존재
→ Match O
"dog"
→ a, b, c가 없음
→ Match X
여기서 중요한 점은
[abc]
가 문자열 "abc" 전체를 의미하는 것이 아니라는 것이다.
[abc]
= a 또는 b 또는 c 중 한 문자
이다.
[ ]에서 범위 지정하기문자를 하나씩 모두 적어야 한다면 불편할 수 있다.
예를 들어 모든 숫자를 표현하려면
[0123456789]
라고 작성할 수도 있지만 다음처럼 간단하게 표현할 수 있다.
[0-9]
-를 사용하면 범위를 지정할 수 있다.
[a-z]
a부터 z까지의 소문자
[A-Z]
A부터 Z까지의 대문자
[0-9]
0부터 9까지의 숫자
[a-zA-Z]
는
영문 소문자 또는 영문 대문자
를 의미한다.
한글 문자를 대상으로 다음과 같은 패턴도 볼 수 있다.
[가-힣]
즉 한글 음절 범위를 표현할 때 사용할 수 있다.
^문자 클래스 []의 맨 앞에서 ^를 사용하면 의미가 반대가 된다.
[^0-9]
는
숫자가 아닌 문자
를 의미한다.
즉
[0-9]
→ 숫자
[^0-9]
→ 숫자가 아닌 것
이다.
[^abc]
는
a, b, c가 아닌 문자
를 의미한다.
[^A-Z]
는
영문 대문자가 아닌 문자
를 의미한다.
정규식에서는 자주 사용하는 패턴을 더 짧게 표현할 수 있다.
| 정규식 | 의미 |
|---|---|
\d | 숫자 |
\D | 숫자가 아닌 문자 |
\s | 공백 계열 문자 |
\S | 공백 계열이 아닌 문자 |
\w | 단어를 구성하는 문자 |
\W | 단어를 구성하는 문자가 아닌 것 |
대문자로 바뀌면 대부분 반대 의미라고 생각하면 이해하기 쉽다.
\d\d는 숫자를 의미한다.
\d
일반적으로 숫자 한 글자를 찾는 데 사용할 수 있다.
예를 들어
import re
p = re.compile(r"\d")
print(p.findall("abc123"))
결과
['1', '2', '3']
이다.
현재
\d
는 숫자 한 글자씩 찾는다.
숫자가 여러 개 연속된 부분을 찾고 싶다면 뒤에서 배우는 +를 붙인다.
\d+
예를 들어
import re
p = re.compile(r"\d+")
print(p.findall("age=25 score=100"))
결과
['25', '100']
이다.
\D대문자인
\D
는 \d의 반대이다.
즉
숫자가 아닌 문자
를 의미한다.
\d → 숫자
\D → 숫자가 아닌 것
\s\s
는 Whitespace 문자와 매치한다.
대표적으로
공백
탭
줄바꿈
등이 포함된다.
예를 들어
import re
p = re.compile(r"\s")
print(p.findall("hello python"))
문자열 중간의 공백이 매치된다.
\S\S
는 \s의 반대이다.
즉
공백 문자가 아닌 것
을 의미한다.
\w\w
는 일반적으로 단어를 구성하는 문자를 찾을 때 사용한다.
예를 들어
import re
p = re.compile(r"\w+")
print(p.findall("python_2026"))
결과
['python_2026']
처럼 사용할 수 있다.
Python 3의
re에서는 기본적으로 유니코드를 사용하기 때문에\w가 영문자와 숫자뿐 아니라 한글과 같은 유니코드 문자에도 매치될 수 있다.
\W\W
는 \w의 반대이다.
즉
단어를 구성하지 않는 문자
를 의미한다.
예를 들면 공백이나 일부 특수문자를 찾는 데 활용할 수 있다.
. Dot 메타 문자정규식에서
.
은
줄바꿈
\n을 제외한 거의 모든 문자 한 개
를 의미한다.
예를 들어
a.b
라는 정규식이 있다고 해보자.
구조는
a
+
아무 문자 1개
+
b
이다.
aab
a + a + b
→ Match O
a7b
a + 7 + b
→ Match O
a-b
a + - + b
→ Match O
하지만
ab
는 중간에 문자가 없으므로 매치되지 않는다.
. 자체를 찾고 싶다면?다음 두 정규식은 의미가 다르다.
a.b
a + 아무 문자 + b
반면
a[.]b
는
a + 실제 점(.) + b
를 의미한다.
따라서
a.b
문자열 자체를 찾을 때 사용할 수 있다.
**
는
바로 앞의 문자가 0번 이상 반복
된다는 의미이다.
예를 들어
ca*t
를 살펴보자.
여기서 *는 바로 앞의 a에 적용된다.
c
+
a가 0번 이상
+
t
라는 의미이다.
| 문자열 | 결과 | 이유 |
|---|---|---|
ct | O | a가 0번 |
cat | O | a가 1번 |
caat | O | a가 2번 |
caaaaat | O | a가 여러 번 |
가장 중요한 부분은
0번도 가능
하다는 것이다.
따라서
ct
도 매치된다.
++
는
바로 앞의 문자가 1번 이상 반복
된다는 의미이다.
예를 들어
ca+t
는
c
+
a가 1번 이상
+
t
이다.
*와 비교| 정규식 | ct | cat | caaat |
|---|---|---|---|
ca*t | O | O | O |
ca+t | X | O | O |
차이는
*
0번 이상
+
1번 이상
이다.
이 차이를 꼭 기억하자.
{m}반복 횟수를 정확하게 지정하고 싶을 때
{}
를 사용할 수 있다.
예를 들어
a{3}
은
a가 정확히 3번
이라는 의미이다.
ca{2}t
는
c
+
a 두 번
+
t
이므로
caat
과 매치된다.
하지만
cat
은 a가 한 번이므로 매치되지 않는다.
{m,n}반복 횟수의 범위를 지정할 수도 있다.
{m,n}
은
최소 m번
~
최대 n번
이라는 의미이다.
예를 들어
a{2,4}
는
aa
aaa
aaaa
처럼 a가 2~4번 반복되는 경우를 의미한다.
a{3,}
a가 3번 이상
a{,3}
a가 3번 이하
라고 이해할 수 있다.
??
는
바로 앞의 문자가 0번 또는 1번
나타난다는 의미이다.
즉
있어도 되고
없어도 된다
라고 생각하면 쉽다.
예를 들어
colou?r
에서 u?는
u가 없어도 되고
u가 한 번 있어도 됨
이라는 의미이다.
따라서
color
와
colour
둘 다 매치할 수 있다.
| 메타 문자 | 의미 |
|---|---|
* | 0번 이상 |
+ | 1번 이상 |
? | 0번 또는 1번 |
{m} | 정확히 m번 |
{m,n} | m~n번 |
{m,} | m번 이상 |
{,n} | n번 이하 |
특히
*
+
?
세 개를 많이 사용한다.
*, +, ? 관계다음처럼 생각할 수도 있다.
*
{0,}
+
{1,}
?
{0,1}
즉 간단하게 자주 사용하는 반복 표현을
*
+
?
로 줄여 놓았다고 생각하면 된다.
re 모듈파이썬에서 정규식을 사용하려면
import re
를 사용한다.
re는 Regular Expression의 약자이다.
파이썬 표준 라이브러리이므로 별도의 설치는 필요 없다.
re.compile()정규 표현식을 사용할 때 다음과 같이 패턴을 만들 수 있다.
import re
p = re.compile(r"[a-z]+")
여기서
re.compile()
은 작성한 정규식을 파이썬에서 사용할 수 있는 패턴 객체로 만들어 준다.
흐름은 다음과 같다.
정규 표현식
[a-z]+
↓
re.compile()
↓
Pattern 객체
↓
검색 수행
[a-z]+ 해석하기앞에서 배운 문법으로 다음 정규식을 해석해 보자.
[a-z]+
먼저
[a-z]
는
영문 소문자 한 글자
이다.
뒤에
+
가 붙었으므로
한 번 이상 반복
이다.
따라서 최종적으로
[a-z]+
= 영문 소문자가 1글자 이상 연속된 문자열
이라는 의미이다.
컴파일한 패턴 객체에서는 대표적으로 다음 메서드를 사용할 수 있다.
| 메서드 | 의미 |
|---|---|
match() | 문자열 처음부터 검사 |
search() | 문자열 전체에서 검색 |
findall() | 매치되는 모든 문자열을 리스트로 반환 |
finditer() | 매치되는 결과를 이터레이터로 반환 |
이 네 개의 차이는 꼭 기억하자.
match()match()는
문자열의 처음부터 패턴과 일치하는지 검사
한다.
먼저 패턴을 만든다.
import re
p = re.compile(r"[a-z]+")
그리고
m = p.match("python")
을 실행한다.
python은 처음부터 영문 소문자로 이루어져 있으므로 매치된다.
m = p.match("3 python")
print(m)
결과
None
이 된다.
왜냐하면 문자열의 첫 번째 글자가
3
이기 때문이다.
현재 패턴은
[a-z]+
이므로 첫 번째 위치에서 바로 실패한다.
정규식
[a-z]+
문자열
python
↑
처음부터 확인
→ O
하지만
3 python
↑
처음 문자가 숫자
→ X
이다.
즉
match()는 문자열 시작 부분이 패턴과 일치해야 한다.
match()는 성공하면 Match 객체를 반환하고 실패하면 None을 반환한다.
따라서 다음과 같이 사용할 수 있다.
import re
p = re.compile(r"[a-z]+")
m = p.match("python")
if m:
print("매치되었습니다.")
else:
print("매치되지 않았습니다.")
search()search()는 match()와 다르게
문자열 전체를 검색
한다.
import re
p = re.compile(r"[a-z]+")
m = p.search("3 python")
print(m)
이번에는 python을 찾을 수 있다.
이 둘은 매우 중요하다.
p.match("3 python")
결과
None
하지만
p.search("3 python")
에서는
python
부분을 찾는다.
정리하면
match()
문자열 시작부터 검사
search()
문자열 전체에서 검색
이다.
문자열
3 python
01234567
↓
3 python
첫 위치부터 검사
3이 [a-z]가 아니므로 실패
3 python
↑↑↑↑↑↑
문자열 전체를 검색
python 발견
findall()findall()은
정규식과 일치하는 모든 문자열을 찾아 리스트로 반환
한다.
예를 들어
import re
p = re.compile(r"[a-z]+")
result = p.findall("python is very easy")
print(result)
결과
['python', 'is', 'very', 'easy']
이다.
search()는 일치하는 위치 하나를 찾는 데 주로 사용하는 반면
findall()
은 모든 결과가 필요할 때 유용하다.
문자열
python is very easy
↓ findall()
python
is
very
easy
↓ 리스트
['python', 'is', 'very', 'easy']
실제 사용에서는 다음처럼 활용할 수 있다.
import re
data = "WEB01=80 WAS01=8080 DB01=3306"
result = re.findall(r"\d+", data)
print(result)
결과
['01', '80', '01', '8080', '01', '3306']
처럼 숫자 패턴을 모두 추출할 수 있다.
finditer()finditer()도 매치되는 결과를 모두 찾는다.
하지만 findall()과 차이가 있다.
import re
p = re.compile(r"[a-z]+")
result = p.finditer("python is easy")
for m in result:
print(m)
각 결과가 Match 객체 형태로 반환된다.
| 구분 | findall() | finditer() |
|---|---|---|
| 모든 결과 검색 | O | O |
| 반환 | 리스트 | 이터레이터 |
| 각 값 | 문자열 중심 | Match 객체 |
| 위치 정보 사용 | 직접적이지 않음 | 가능 |
특히 위치까지 확인해야 한다면
finditer()
가 유용하다.
match(), search(), finditer() 등을 사용하면 Match 객체를 얻게 된다.
Match 객체에서는 매치 결과에 대한 정보를 확인할 수 있다.
대표적인 메서드는 다음과 같다.
| 메서드 | 설명 |
|---|---|
group() | 매치된 문자열 |
start() | 시작 위치 |
end() | 끝 위치 |
span() | (시작, 끝) |
group()import re
p = re.compile(r"[a-z]+")
m = p.search("3 python")
print(m.group())
결과
python
즉
group()
은 실제로 매치된 문자열을 반환한다.
start()print(m.start())
매치가 시작된 위치를 반환한다.
문자열이
3 python
이라면 인덱스는
3 p y t h o n
0 1 2 3 4 5 6 7
이므로 python의 시작 위치는
2
이다.
end()print(m.end())
매치가 끝나는 위치를 반환한다.
여기서 주의할 점은 마지막 문자의 인덱스가 아니라 그 다음 위치라는 것이다.
예를 들어
python
이 인덱스 2~7에 있다면
start() → 2
end() → 8
이 된다.
이 방식은 파이썬 슬라이싱과 같다.
text[2:8]
span()print(m.span())
결과는
(2, 8)
처럼 나온다.
즉
(start(), end())
를 한 번에 튜플로 반환한다.
import re
p = re.compile(r"[a-z]+")
m = p.search("3 python")
print(m.group())
print(m.start())
print(m.end())
print(m.span())
결과
python
2
8
(2, 8)
따라서
group() → 무엇을 찾았는가?
start() → 어디서 시작했는가?
end() → 어디까지인가?
span() → 시작과 끝을 한 번에
라고 기억하면 된다.
re.compile() 없이 사용하는 방법지금까지는
p = re.compile(r"[a-z]+")
m = p.match("python")
처럼 사용했다.
하지만 다음처럼 한 번에 작성할 수도 있다.
m = re.match(r"[a-z]+", "python")
즉
re.compile()
+
match()
↓
re.match()
형태로 사용할 수 있다.
같은 정규식을 여러 번 사용할 예정이라면
p = re.compile(...)
처럼 패턴 객체를 만들어 두면 코드가 깔끔하다.
간단하게 한 번 검색하는 경우라면
re.search(...)
re.findall(...)
처럼 직접 사용할 수도 있다.
정규식을 사용할 때 동작 방식을 변경하기 위한 옵션이 있다.
대표적인 옵션은 다음과 같다.
| 옵션 | 축약 | 의미 |
|---|---|---|
re.DOTALL | re.S | .이 줄바꿈까지 포함 |
re.IGNORECASE | re.I | 대소문자 무시 |
re.MULTILINE | re.M | 여러 줄에서 ^, $ 적용 |
re.VERBOSE | re.X | 정규식을 여러 줄과 주석으로 작성 |
기본적으로
.
은 줄바꿈
\n
과 매치되지 않는다.
예를 들어
import re
p = re.compile(r"a.b")
print(p.match("a\nb"))
에서는 매치되지 않는다.
다음처럼 옵션을 사용한다.
p = re.compile(r"a.b", re.DOTALL)
또는
p = re.compile(r"a.b", re.S)
그러면
.
이 줄바꿈까지 포함해 매치할 수 있다.
a
\n
b
에서도 매치할 수 있게 된다.
기본적으로 정규식에서는 대소문자를 구분한다.
python
Python
PYTHON
은 서로 다른 문자열이다.
대소문자를 구분하고 싶지 않다면
re.IGNORECASE
또는
re.I
를 사용한다.
import re
p = re.compile(r"python", re.I)
print(p.match("python"))
print(p.match("Python"))
print(p.match("PYTHON"))
모두 매치할 수 있다.
다음 메타 문자도 자주 사용한다.
^
문자열의 시작
그리고
$
는
문자열의 끝
을 의미한다.
예를 들어
^python
은
python으로 시작
이라는 의미이다.
다음 데이터가 있다고 해보자.
data = """python one
java two
python three"""
그리고
p = re.compile(r"^python")
을 사용하면 기본적으로 문자열 전체의 처음을 기준으로 한다.
하지만
p = re.compile(r"^python", re.MULTILINE)
또는
p = re.compile(r"^python", re.M)
을 사용하면
각 줄의 시작
을 기준으로 검사할 수 있다.
따라서
python one
과
python three
의 python을 각각 찾을 수 있다.
정규식이 짧을 때는 문제가 없다.
하지만 정규식이 길어지면
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$
처럼 매우 읽기 어려워진다.
이럴 때
re.VERBOSE
또는
re.X
를 사용할 수 있다.
import re
email_pattern = re.compile(
r"""
^ # 문자열 시작
[A-Za-z0-9._%+-]+ # 사용자 이름
@ # @
[A-Za-z0-9.-]+ # 도메인
\. # 실제 점
[A-Za-z]{2,} # 최상위 도메인
$ # 문자열 끝
""",
re.VERBOSE
)
이렇게 작성하면 복잡한 정규식을 여러 줄로 나누고 주석까지 작성할 수 있어 훨씬 이해하기 쉽다.
\ 문제정규식에서 가장 헷갈릴 수 있는 부분 중 하나가
\
이다.
정규식에서도 역슬래시에 특별한 의미가 있고 파이썬 문자열에서도 역슬래시는 이스케이프 문자로 사용된다.
즉
Python 문자열 해석
+
정규식 해석
두 단계가 겹칠 수 있다.
그래서 코드가 복잡해질 수 있다.
\section을 찾는다면?문자열 안에 실제로
\section
이라는 문자가 있다고 해보자.
정규식에서
\section
이라고 그대로 사용하면 문제가 발생할 수 있다.
왜냐하면 정규식 엔진은 앞부분
\s
를
Whitespace
라는 특별한 정규식으로 해석할 수 있기 때문이다.
정규식에서 실제 역슬래시를 의미하도록 만들려면 역슬래시를 이스케이프해야 한다.
즉 정규식 수준에서는
\\section
처럼 표현한다.
그런데 일반적인 파이썬 문자열에서도 \가 특별한 의미를 가지므로 코드가 더 복잡해질 수 있다.
이 문제를 쉽게 해결하는 방법이 Raw String이다.
정규식을 작성할 때 다음처럼 문자열 앞에
r
을 붙이는 경우가 많다.
r"\d+"
이것을 Raw String이라고 한다.
"\n"
은
줄바꿈
으로 처리된다.
하지만
r"\n"
은 문자열 안의 역슬래시를 정규식 작성에 보다 편리하게 사용할 수 있게 해 준다.
정규식에서는 다음처럼 쓰는 습관을 들이는 것이 좋다.
re.compile(r"\d+")
re.findall(r"[0-9]+", text)
re.search(r"\w+", text)
즉
정규식 문자열
↓
가능하면 r"..."
형태로 작성한다고 기억하면 된다.
| 표현 | 의미 |
|---|---|
[abc] | a, b, c 중 하나 |
[a-z] | a~z |
[^0-9] | 숫자가 아닌 문자 |
\d | 숫자 |
\D | 숫자가 아닌 문자 |
\s | 공백 계열 |
\S | 공백 계열이 아닌 문자 |
\w | 단어 문자 |
\W | 단어 문자가 아닌 것 |
. | 줄바꿈을 제외한 임의의 한 문자 |
* | 앞의 패턴 0번 이상 |
+ | 앞의 패턴 1번 이상 |
? | 앞의 패턴 0번 또는 1번 |
{m} | 정확히 m번 |
{m,n} | m~n번 |
다음 정규식을 살펴보자.
\d{3}-\d{4}-\d{4}
하나씩 나누면
\d{3}
↓
숫자 3개
-
↓
하이픈
\d{4}
↓
숫자 4개
-
↓
하이픈
\d{4}
↓
숫자 4개
따라서 전체적으로
숫자 3개
-
숫자 4개
-
숫자 4개
라는 형태를 찾는다.
아주 단순하게 IP처럼 생긴 문자열을 찾는다면
\d+\.\d+\.\d+\.\d+
처럼 표현할 수도 있다.
하나씩 보면
\d+
↓
숫자가 1개 이상
\.
↓
실제 점
이 구조가 반복된다.
따라서
192.168.0.1
과 같은 형태를 찾을 수 있다.
다만 이 정규식은
999.999.999.999도 매치할 수 있기 때문에 IPv4 주소의 실제 유효 범위인0~255까지 검증하는 정규식은 더 복잡하다.
예를 들어 다음 로그가 있다고 해보자.
SRC=192.168.10.20 DST=10.0.0.10 PORT=443
숫자들을 가져오려면
import re
log = "SRC=192.168.10.20 DST=10.0.0.10 PORT=443"
result = re.findall(r"\d+", log)
print(result)
결과는
['192', '168', '10', '20', '10', '0', '0', '10', '443']
처럼 나온다.
정규식을 조금씩 조합하면 나중에는 로그나 보안 이벤트에서도 원하는 패턴을 추출할 수 있다.
정규식을 실제 파이썬 코드에서 사용하면 기본적으로 다음과 같은 흐름이 된다.
정규식 작성
↓
re.compile()
↓
Pattern 객체
↓
문자열 검색
↓
match()
search()
findall()
finditer()
↓
결과 확인
또는 간단한 경우
re.match()
re.search()
re.findall()
처럼 바로 사용할 수도 있다.
[abc]
a 또는 b 또는 c
[a-z]
영문 소문자
[^0-9]
숫자가 아닌 문자
\d → 숫자
\D → 숫자가 아닌 것
\s → 공백 계열
\S → 공백 계열이 아닌 것
\w → 단어 문자
\W → 단어 문자가 아닌 것
*
0번 이상
+
1번 이상
?
0번 또는 1번
{3}
정확히 3번
{2,5}
2~5번
match()
문자열 처음부터 확인
search()
문자열 전체에서 검색
이 차이가 가장 중요하다.
re.findall(r"\d+", "abc 100 def 200")
['100', '200']
매치되는 모든 문자열을 리스트로 반환한다.
모든 결과를 찾지만 각각의 결과를 Match 객체 형태로 다룰 수 있다.
group()
→ 매치된 문자열
start()
→ 시작 위치
end()
→ 끝 위치
span()
→ (시작 위치, 끝 위치)
re.S
→ .에 줄바꿈 포함
re.I
→ 대소문자 무시
re.M
→ 각 줄에 ^, $ 적용
re.X
→ 복잡한 정규식을 여러 줄과 주석으로 작성
정규식에서는 역슬래시가 자주 등장하므로
r"\d+"
처럼 r"..." 형태로 작성하는 습관을 들이면 좋다.
문자열에서 원하는 규칙 정의
↓
정규 표현식 작성
↓
[ ]
.
*
+
?
{}
\d
\s
\w
↓
re.compile()
↓
Pattern 객체 생성
↓
┌───────────┬───────────┬────────────┬────────────┐
│ │ │ │
match() search() findall() finditer()
│ │ │ │
처음부터 전체 검색 모두 리스트 모두 Iterator
그리고 match()나 search()로 얻은 Match 객체에서는
group()
start()
end()
span()
을 이용해
무엇이 매치되었는지
+
어디에서 매치되었는지
확인할 수 있다.
08-2의 핵심은 정규식 기호를 단순히 외우는 것이 아니라
[a-z]+처럼 하나씩 분해해서 읽는 습관을 만드는 것이다.
예를 들어
[a-z]+
를 보면 바로
[a-z]
→ 영문 소문자
+
→ 1번 이상
따라서
영문 소문자가 1글자 이상 연속
이라고 해석할 수 있어야 한다.