import re
12 정규표현식(Regular Expression)과 re
모듈
정규 표현식(regular expression)은 텍스트 패턴을 정의하고, 이 패턴을 이용하여 검색, 치환 등 텍스트를 처리하는 방법을 말한다. 문법이 복잡하고 어려운 편이지만, 텍스트를 처리하는 데 매우 유용한 도구이다. 정규 표현식은 텍스트의 구조를 이해하고, 특정 패턴을 찾거나 치환하는 데 사용된다.
파이썬 정규 표현식은 re
모듈에 구현되어 있다. 파이썬 공식 홈페이지에 re
모듈의 개론과 사용법이 설명되어 있다.
참고 문헌: 『Regular Expressions Cookbook, 2nd Edition』
12.1 re
모듈을 사용하는 방법
파이썬 정규 표현식을 사용할 때는 먼저 re
모듈을 로딩한다.
그 다음 과정은 보통 텍스트 패턴을 정의하는 것이다. 텍스트 패턴을 정의할 때는 보통 raw string
을 사용한다. 왜냐하면 정규 표현식에서 \d
(하나의 숫자)와 같이 역슬래쉬를 사용하는 경우가 많아, 일반 문자열을 사용하는 경우 역슬래쉬를 이스케이핑(escaping) 시켜야 하기 때문에 읽기에도 혼란스럽고 번거럽다. 그래서 정규 표현식을 만들 때는 r"......"
의 형태를 가지는 raw string을 사용하여 정의하는 것이 권장된다.
이렇게 만들어진 패턴 객체를 re.compile()
함수로 넘기면, 함수가 텍스트를 컴파일하여 텍스트 패턴을 만든다.
= re.compile(r'Hello')
RE type(RE)
re.Pattern
이렇게 만들어지는 텍스트 패턴 객체는 여러 메서드를 가지고 있다. 예를 들어, re.search()
메서드로 타깃 텍스트를 검색할 수 있다.
if RE.search("Hello, World!"):
print("포함됨.")
else:
print("포함되지 않음.")
포함됨.
re.search()
함수는 타깃 텍스트 안에 패턴과 매칭되는 텍스트가 있는 경우에 Match 객체를 반환하고 매칭되는 것이 없으면 None
을 반환한다.
= RE.search("Hello, World!")
k k
<re.Match object; span=(0, 5), match='Hello'>
이 Match 객체에는 여러 정보들이 들어 있다. span=(0, 5)
은 "Hello, World!"
문자열에서 매칭되는 부분을 알려준다. 매칭되는 텍스트를 추출하려면 group()
함수를 사용한다.
k.group()
'Hello'
start()
, end()
함수로 스팬의 시작과 끝을 가지고 올 수 있기 때문에, 이 값들을 슬라이싱에 이용할 수도 있겠다.
"Hello, World!"[k.start():k.end()]
'Hello'
정규 표현식은 디폴트로 대소문자를 구분한다.
if RE.search('hello, world'):
print("검색됨")
else:
print("검색되지 않음.")
검색되지 않음.
re.IGNORECASE
또는 re.I
는 대소문자를 구분하지 않게 만든다. 이렇게 정규 표현식 엔진에 영향을 줘서 그 기능을 바꾸는 것을 플래그(flags)라고 하고, re.I
말고도 여러 개가 마련되어 있다.
= re.compile(r"Hello", re.I)
RE2 if RE2.search('hello, world'):
print("검색됨")
else:
print("검색되지 않음.")
검색됨
12.2 텍스트 패턴을 보는 방법
정규 표현식은 근본적으로 논리(logic)에 바탕을 둔 도구이기 때문에 논리적으로 생각할 필요가 있다.
- 문자열처럼 한 글자, 한 글자씩 본다.
- 왼쪽에서 오른쪽으로 진행한다.
- 텍스트 파일과 같이 여러 행(line)을 가지고 있는 경우, 하나의 문자열(string)을 대상으로 할지, 하나의 행을 대상으로 할지 주의한다.
- 영어의 경우 기본으로 대소문자를 구분한다.
- 정규 표현식은 자체 엔진을 사용하여 데이터를 처리하기 때문에 파이썬 문법과는 별도로 생각해야 한다.
- 글자의 종류와 집합 관계에 주의를 기울인다.
- Characters는 letters와 digits, whitespace, 문장부호 등을 모두 포함한다.
- Letters는
a
,b
,c
… 와A
,B
,C
… 등을 말한다. 파이썬 문자열은 Uncode code point이기 때문에 사실은 한글과 같은 문자열도 모두 여기에 포함된다. 영문인 경우에 이렇다는 뜻임을 이해할 필요가 있다. - Digits은
0, 1, 2, 3,...9
을 말한다. - Whitespaces는 인쇄했을 때 표시되지 않기는 하지만 이것들도 characters의 일부이라는 점을 기억하자.
- 빈칸(
" "
) - 탭(
\t
) - 줄바꿈(
\n
또는\r\n
) 등
- 빈칸(
- 논리(logic) 관계에 주의한다.
- 파이썬 정규 표현식은 Unicode character로 구성되는
str
, 바이너리 값으로 구성되는bytes
에 모두 적용된다.
12.3 정규 표현식의 기초 문법
정규 표현식을 만들려면 다음과 같은 기초 문법을 이해할 필요가 있다.
12.3.1 리터럴(literals)
리터럴은 해당 문자 자체를 의미한다.
r"Hello, world!"
:H
(대문자), 그 다음e
, 그 다음l
, 그 다음l
, 그 다음o
이다. 그 다음,
, 그 다음 공백 문자, 그 다음w
, 그 다음o
, 그 다음r
, 그 다음ㅣ
, 그 다음d
, 그 다음!
이 오는 문자열, 즉"Hello, world!"
과 매칭된다.
= re.compile(r"Hello, world!")
hw = "Welcome to 'Hello, world!'"
text hw.search(text)
<re.Match object; span=(12, 25), match='Hello, world!'>
여기서 문자 하나하나를 연결시키는 것은 일종의 “AND” 논리가 적용됨을 알 수 있다. 그리고, 패턴안의 공백 역시 텍스트의 패턴에 매칭됨을 알 수 있다.
12.3.2 리터럴 의미로 사용될 수 있게 이스케이핑이 필요한 메타캐랙터 12개
정규 표현식에서 특별한 의미로 사용되는 문자를 metacharacters
라고 한다.
그런데, 이런 문자를 문자 자체로 사용하고자 할 경우에는 백슬래쉬를 앞에 붙여서 이스케이핑시켜야 한다.
처음에 이것을 다 외울 필요는 없고, 이후 설명하게 되는 특별한 의미를 먼저 알고 나면 쉽게 이해가 갈 것이다.
- Backslash
\
- Caret
^
- Dollar sign
$
- Dot
.
- Pipe symbol
|
- Question mark
?
- Asterisk
*
- Plus sign
+
- Opening parenthesis
(
- Closing parenthesis
)
- Opening square bracket
[
- The opening curly brace
{
12.3.3 Character Class
[]
는 캐랙터 클래스(class)를 만들 때 사용된다. 이것은 안에 들어있는 문자들 가운데 하나를 표시할 때 사용된다. 즉, [abc]
라고 하면 a
또는 b
또는 c
라는 의미이다. []
안에 들어 있는 문자들 가운데 하나의 character에 매칭된다는 점에 주의한다.
import re
= re.compile(r"gr[ae]y")
p = ["I like gray color.", "I like grey color.", "No gry color"]
text for s in text] [p.search(s)
[<re.Match object; span=(7, 11), match='gray'>,
<re.Match object; span=(7, 11), match='grey'>,
None]
위 경우 gr[ae]y
이므로 gray
또는 grey
에 매칭된다.
12.3.3.1 []
안에서 하이픈(-
)으로 범위(range
) 만들기
[]
안에서 -
를 중간에 넣어서 범위(range)를 만들 수 있다. [0-9]
라고 하면 0
에서 9
까지 숫자 가운데 하나, [a-z]
는 소문자, [A-Z]
라고 하면 대문자 가운데 하나가 된다. [1-3]
이라고 하면 1에서 3까지 숫자 가운데 하나이다. 이런 범위는 여러 개 한꺼번에 쓸 수 있다. [a-zA-Z0-9_]
등과 같이 사용할 수 있다.
12.3.3.2 [^...]
으로 여집합: 포함되지 않은 character
[^abc]
는 a
, b
, c
가 아닌 하나의 문자와 매칭된다. 주의할 점은 ^
이 이런 의미로 사용할 때는 [
바로 다음에 써 주어야 한다. 만약 다른 곳에 쓰면 그것은 그냥 ^
에 매칭된다. [^0-9]
라고 하면 숫자가 아닌 문자에 매칭된다.
다음은 영어의 모음이 아닌 문자를 추출한다.
import re
= re.compile(r'[^aeiou]')
p = "Hello, World!"
t p.findall(t)
['H', 'l', 'l', ',', ' ', 'W', 'r', 'l', 'd', '!']
12.3.3.3 단축형 캐랙터 클래스: shorthand character class
\d
-
\d
는 하나의 아라비아 숫자(digit
)에 매칭된다.[0-9]
와 같은 의미이다. 그 여집합은 숫자가 아닌 것으로\D
이다.
= re.compile(r"\d\d\d.\d\d\d.\d\d\d\d") p = ["010-123-4567", "1011234567", "010/123/4567", "010 123 4567"] phone for s in phone] [p.search(s)
[<re.Match object; span=(0, 12), match='010-123-4567'>, None, <re.Match object; span=(0, 12), match='010/123/4567'>, <re.Match object; span=(0, 12), match='010 123 4567'>]
r"\d\d\d.\d\d\d.\d\d\d\d"
의 경우 3개의 숫자, 그 다음 어떤 chacter 하나, 그 다음 숫자 3개 , 그 다음 어떤 character 하나, 그 다음 숫자 4개와 매칭된다.\w
-
\w
는 하나의 Unicode word character를 의미하는데, 여기는 모든 alphanumeric characters와 underscore(_
) 가운데 한 글자를 의미한다(letters, digits, underscore).[a-zA-Z0-9_]
를 의미한다. 이것의 여집합은\W
이다.
import re = re.compile(r"\w\w\w\w") pattern = ["abcd", "a bc", "대한민국", "s125","s_class", "s_*class"] text for s in text] [pattern.search(s)
[<re.Match object; span=(0, 4), match='abcd'>, None, <re.Match object; span=(0, 4), match='대한민국'>, <re.Match object; span=(0, 4), match='s125'>, <re.Match object; span=(0, 4), match='s_cl'>, <re.Match object; span=(3, 7), match='clas'>]
\s
-
출력시 공백으로 보이게 되는 문자(whitespace character),
[ \t\n\r\f\v]
(제일 앞에 공백이 둔 것이다)을 의미한다. 공백, 탭문자, 줄바꿈문자 등 가운데 하나라는 의미이다. 이것의 여집합은\S
이다.
= re.compile(r"\w\s\w") pattern = ["abcd", "a bc", "대한민국", "s\t125","s\nclass", "s_class"] texts for s in texts] [pattern.search(s)
[None, <re.Match object; span=(0, 3), match='a b'>, None, <re.Match object; span=(0, 3), match='s\t1'>, <re.Match object; span=(0, 3), match='s\nc'>, None]
다음 경우와 같이
[]
안에 단축 캐랙터 클래스를 넣을 수 있다. 다음은digits
이거나_
인 하나의 문자를 정의한다.import re = re.compile(r"[\d-]") pattern = "seoul-2024-가-1234" text pattern.findall(text)
['-', '2', '0', '2', '4', '-', '-', '1', '2', '3', '4']
12.3.4 Dot
.
은 줄바꿈 문자를 제외한 하나의 character와 매칭된다(any single character except newline).
다음 예를 보자.
import re
= re.compile(r"...")
pattern = [" ", "abc", "1234", "///"," @#$", "대한민국", "ab\ncde"]
texts for s in texts] [pattern.search(s)
[<re.Match object; span=(0, 3), match=' '>,
<re.Match object; span=(0, 3), match='abc'>,
<re.Match object; span=(0, 3), match='123'>,
<re.Match object; span=(0, 3), match='///'>,
<re.Match object; span=(0, 3), match=' @#'>,
<re.Match object; span=(0, 3), match='대한민'>,
<re.Match object; span=(3, 6), match='cde'>]
이 경우 텍스트 패턴이 ...
로 되어 있어서, 공백 문자등을 모두 포함하여 3개의 character로 연결된 경우라면 모두 매칭된다. 줄바꿈(\n
)인 경우는 제외되므로 마지막 "ab\ncde"
에서는 cde
가 매칭된다.
\n
)을 제외하는 이유
정규 표현식은 텍스트 파일을 가지고 작업하는 경우가 많은데 Unix 시스템에서는 이런 경우 파일은 내용은 행들(lines)으로 구성된 것으로 보고, 행 단위로 데이터를 처리한다. 따라서 이런 관례를 따른 것이다.
12.3.5 Alternation
파이썬 정규 표현식 alternation
은 OR의 의미를 가지며, 파이프 기호(|
) 사용하여 만든다. THE|the
는 THE
라는 정규 표현식 또는 the
라는 정규 표현식을 의미한다. OR의 의미를 가지는 character class와 alternation의 차이는 다음과 같다.
- character class: a single character out of several possible characters
- alternation: a single regular expression out of serveral possible regular expression
여기서 눈여겨볼 것은 |
의 왼쪽, 오른쪽 것이 모두 하나의 문자가 아니라 정규 표현식이라는 것이다. 만약 정규 표현식이 찾을 것: THE|the
라고 했다면 이것은 찾을 것: THE
와 the
라는 정규 표현식 가운데 하나라는 뜻이다.1 이렇게 하지 않고 찾을 것: THE
와 찾을 것: the
로 나눌 의도라면, 그룹핑(grouping) 방법을 써서 찾을 것 (THE|the)
라고 해야 한다. 그룹핑의 개념은 뒤에서 다시 설명한다.
그리고 |
을 더 사용하여 더 많은 정규 표현식을 포함시킬 수 있다. 예를 들어 THE|The|the
라고 할 수 있다.
논리적으로 중요한 점 가운데 하나는 앞에서 정규 표현식 매칭되어 “참”이 되는 경우에는 뒤의 것은 무시된다는 점이다 따라서 ab|abc
라고 된 정규 표현식은 절대 "abc"
와 매칭되지 않는다. 어떤 텍스트가 "abc"
가 들어 있는데 여기에 이 정규 표현식이 적용될 때 ab
가 먼저 적용되어 참으로 나오기 때문에 abc
는 평가되지 않는다.
import re
= re.compile(r"Yes|No")
pattern = 'He asked, "Yes or NO?"'
text pattern.findall(text)
['Yes']
만약 alternation을 통해서 작업해야 할 단어 리스트가 있다면 str.join()
함수를 사용하여 쉽게 정규 표현식을 만들 수 있다.
= ['cat', 'dog', 'fox']
words '|'.join(words)
'cat|dog|fox'
12.3.6 Quantifiers
앞의 것을 몇 번 반복할지 지정하는 것을 quantafiers
라고 하고, 다음 기호를 사용한다.
*
: 앞의 것을 0번에서 그 이상 반복 가능+
: 앞의 것을 1번에서 그 이상 반복 가능?
: optional의 의미로 0번 또는 1회
이들 앞에 오는 것이란 다음과 같은 것이다.
- 하나의 문자(character)
- 하나의 메타캐랙터(metacharacter)
- 하나의 캐랙터 클래스(character class)
- 하나의 그룹(group)
다음은 숫자를 사용한 예들이다.
import re
= re.compile(r"\d*")
p = ["a", "1", "12", "123"]
text for s in text] [p.search(s)
[<re.Match object; span=(0, 0), match=''>,
<re.Match object; span=(0, 1), match='1'>,
<re.Match object; span=(0, 2), match='12'>,
<re.Match object; span=(0, 3), match='123'>]
r"\d*"
은 숫자 하나가 0번 이상 나오는 나오는 경우이기 때문에 4가지 경우 모두 매칭된다.
import re
= re.compile(r"\d+")
p = ["a", "1", "12", "123"]
text for s in text] [p.search(s)
[None,
<re.Match object; span=(0, 1), match='1'>,
<re.Match object; span=(0, 2), match='12'>,
<re.Match object; span=(0, 3), match='123'>]
r"\d+"
은 숫자가 1회 이상 나오는 경우이기 때문에, 첫 번째 경우는 매칭되지 않는다.
import re
= re.compile(r"\d?")
p = ["a", "1", "12", "123"]
text for s in text] [p.search(s)
[<re.Match object; span=(0, 0), match=''>,
<re.Match object; span=(0, 1), match='1'>,
<re.Match object; span=(0, 1), match='1'>,
<re.Match object; span=(0, 1), match='1'>]
r"\d?"
은 숫자가 0번 또는 1회 나오는 경우이다. 모두 매칭은 되지만 그 값은 없거나 하나이다.
앞에서 ?
, *
, +
quantifiers를 보았다. 이들 외에 정확히 몇번, 또는 몇번 이상, 몇번에서 볓번까지를 지정하기 위해서는 {}
을 사용한다.
{m, n}
: m회에서 n회까지{m}
: m번{m,}
: m번 이상{,n}
: n번까지
import re
= re.compile(r'\d{2,4}')
p = "123 45 6789 0 12345"
text p.findall(text)
['123', '45', '6789', '1234']
import re
= re.compile(r'\w{3,5}')
pattern = "ab abc defg hijkl mnopqr stuvwxyz"
text pattern.findall(text)
['abc', 'defg', 'hijkl', 'mnopq', 'stuvw', 'xyz']
12.3.6.1 몇 가지 자주 사용되는 패턴들
이런 quantifier를 조합하여 흔히 사용되는 패턴이 만들어진다.
.*
은 줄바꿈 문자를 제외한 모든 문자를 0번에서 무한대까지를 말한다. 따라서 문자열 전체를 의미한다. 줄바꿈이 있는 경우는 줄바꿈 되기 전까지 전부이다..+
패턴은 줄바꿈 문자를 제외한 모든 문자를 1번에서 무한대까지 이어지는 경우이다. 앞과 다른 것은+
가 하나 이상을 의미하기 때문에 빈문장은 매칭되지 않는다는 점이다.
import re
= re.compile(r".*")
p = ["", "Hello, world!", "I love\tPython.", "Python is\nGreat. And\nEasy to Learn"]
texts for s in texts] [p.search(s)
[<re.Match object; span=(0, 0), match=''>,
<re.Match object; span=(0, 13), match='Hello, world!'>,
<re.Match object; span=(0, 14), match='I love\tPython.'>,
<re.Match object; span=(0, 9), match='Python is'>]
import re
= re.compile(r".+")
p = ["", "Hello, world!", "I love\tPython.", "Python is\nGreat. And\nEasy to Learn"]
texts for s in texts] [p.search(s)
[None,
<re.Match object; span=(0, 13), match='Hello, world!'>,
<re.Match object; span=(0, 14), match='I love\tPython.'>,
<re.Match object; span=(0, 9), match='Python is'>]
\w+
은 letter, digit, underscore가 하나 이상 있는 것이다. 공백은 포함되지 않기 때문에 문자열을 구성하는 단어들이 모두 여기에 해당한다.
import re
= re.compile(r"\w+")
p = '함수는 요청을 받았을 때 실행되는 일련의 문장이다.'
text p.findall(text)
['함수는', '요청을', '받았을', '때', '실행되는', '일련의', '문장이다']
.*
,.+
: 문자열 전체(약간의 차이는 본문 참고한다)\w+
: 문자열을 구성하는 단어들
12.3.6.2 Quantifier의 속성: greedy
와 non-greedy
Quantifiers는 디폴트로 greedy 모드로 작동한다. 그 의미는 가능하면 많이 가지려고 한다는 뜻이다.
import re
= re.compile(r'\w{3,5}')
pattern = "ab abc defg hijkl mnopqr stuvwxyz"
text pattern.findall(text)
['abc', 'defg', 'hijkl', 'mnopq', 'stuvw', 'xyz']
Greedy 모드에서는 가능하면 5개까지 가지려고 한다. 마지막 stuvwxyz
인 경우 5개를 가지고 나서도 3개가 남아서 다시 이 3개가 선택된다.
Quantifiers 뒤에 ?
를 붙이면 non-greedy 모드로 되는데, 가능하면 적게 가지게 한다는 뜻이다. 그 차이를 보자.
*?
,??
,+?
,{m, n}f
import re
= re.compile(r'\w{3,5}?')
pattern = "ab abc defg hijkl mnopqr stuvwxyz"
text pattern.findall(text)
['abc', 'def', 'hij', 'mno', 'pqr', 'stu', 'vwx']
Non-greedy 모드에서는 가능하면 적게 가지려고 한다. 마지막 stuvwxyz
에서는 적게 매칭되어 stu
를 가지고 나서, vwxyz
가 남아서 이 가운데 3개를 가지고 와서 vwx
가 되는 것이다.
약간 다른 관점으로 greedy와 non-gredy 개념을 다시 살펴보자. 다음은 모두 x
에서 시작하여 y
까지 문자열을 추출하고자 하는 것인데, "x is happy and more fun than y"
이라는 문장은 2가지 방법이 조건을 만족한다.
greedy
:"x is happy and more fun than y"
non-greedy
:"x is happy"
실제 코드를 보면 다음과 같다.
# Greedy
= re.compile(r"x.+y")
pattern = "x is happy and more fun than y"
text pattern.findall(text)
['x is happy and more fun than y']
# Non-greedy
= re.compile(r"x.+?y")
pattern = "x is happy and more fun than y"
text pattern.findall(text)
['x is happy']
12.3.7 Anchors
매칭의 위치를 지정하는 정규 표현식을 anchors
라고 한다.
Anchors를 잘 이해하기 위해선 우선 string과 line을 구분해서 생각할 수 있어야 한다.
String은 타깃 문자열 전체를 의미한다. 여기에 줄바꿈 문자 \n
이 들어간 경우 이 문자열은 여러 행(line)을 가진다. 다음 예에서 poem
str 변수는 하나의 string이면서 4개의 lines으로 구성되어 있는 것으로 생각해야 한다.
= """죽는 날까지 하늘을 우러러
poem 한 점 부끄럼이 없기를,
잎새에 이는 바람에도
나는 괴로워했다."""
poem
'죽는 날까지 하늘을 우러러\n한 점 부끄럼이 없기를,\n잎새에 이는 바람에도\n나는 괴로워했다.'
이를 고려하여, 다음과 같은 anchors가 준비되어 있다.
\A
: String의 가장 앞쪽에서 매칭\Z
: String의 가장 뒷쪽에서 매칭^
: Line의 가장 앞쪽에서 매칭 (여러 line에서 작동하도록 할 때는re.MULTILINE
플래그 적용)$
: Line의 가장 뒷쪽에서 매칭 (여러 line에서 작동하도록 할 때는re.MULTILINE
플래그 적용)
그래서 어떤 사람은 \A
와 \Z
를 string anchors라고 하고, ^
와 $
을 line anchors라고도 한다2.
만약 타깃 텍스트가 하나의 line으로 된 하나의 string이라면, string이냐 line이냐에 상관없이 \A
와 ^
, \Z
와 $
는 같은 의미를 가진다.
= re.compile(r"^cat")
pattern1 = re.compile(r"dog$")
pattern2 = 'cat and dog'
pets print(pattern1.search(pets))
print(pattern2.search(pets))
<re.Match object; span=(0, 3), match='cat'>
<re.Match object; span=(8, 11), match='dog'>
12.3.7.1 the whole string 매칭
만약 하나의 정규 표현식이 ^...$
와 같이 처음과 끝이 명시되는 경우에는 이 표현식은 한 줄이 모두(the whole string) 매칭되어야 한다는 뜻이 된다. r.fullmath()
함수도 전체 줄을 매칭할 때 사용된다.
= re.compile(r'\d{3}-\d{2}-\d{4}')
pattern1 = re.compile(r'^\d{3}-\d{2}-\d{4}$')
pattern2 = "123-45-6789"
text1 = "123-45-6789 이다."
text2 = pattern1.fullmatch(text1)
match1 = pattern1.fullmatch(text2)
match2 = pattern2.search(text1)
match3 = pattern2.search(text2)
match4 print(match1)
print(match2)
print(match3)
print(match4)
<re.Match object; span=(0, 11), match='123-45-6789'>
None
<re.Match object; span=(0, 11), match='123-45-6789'>
None
그러니까 anchor 없이 정규 표현식을 만들고 r.fullmath()
함수를 쓰거나, anchor를 사용하여 정규 표현식을 만들고 r.search()
함수를 쓸 수 있다.
12.3.7.2 Strings과 Lines
이와 같은 개념은 텍스트 파일을 파이썬에서 읽고 처리하는 데도 적용된다. file
객체에 대하여 file.read()
함수는 파일의 내용을 하나의 string으로 읽고, 이 안에 여러 line들을 가지게 된다. 반면 file.readline()
은 파일의 내용을 한줄씩(line-by-line)으로 읽기 때문에 파일의 모든 행을 처리하기 위해서는 루프가 필요하다.
with open('data/zen2.txt', 'rt') as file:
= file.read()
content print(content)
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
with open('data/zen2.txt', 'rt') as file:
while True:
= file.readline()
line if not line:
break
print(line, end="")
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
위 개념을 염두에 두고, 우리가 이 파일의 각 행에서 첫 번째 단어를 골라 이것들을 리스트를 만들어 보는 작업을 해 보려고 한다.
먼저 첫 번째 시도에서는 file.read()
함수를 사용한 경우이다.
= []
first_letter = re.compile(r"^\w+")
pattern with open('data/zen2.txt', 'rt') as file:
= file.read()
content = pattern.findall(content)
match += match
first_letter print(first_letter)
['Beautiful']
위 코드는 원하는 결과를 내지 않는다. file.read()
함수가 파일의 내용을 하나의 string을 반환하고(안에 여러 lines이 있음), "^\w+"
를 사용하여 첫 번째 단어를 매칭한 때, 별도의 지시가 없는 한 ^
는 하나의 행에서 가장 앞에서 매칭이 이뤄져 하나의 단어만 가지게 된다.
이런 경우 우리의 목적을 이루기 위해 모든 lines에서 이 매칭이 이뤄지게 하려면 re.MULTILINE
플래그를 적용할 필요가 있다.
re.M
또는 re.MULTILINE
이 플래그는 ^
또는 $
anchor를 사용하는 경우, 모든 줄에서 작동하도록 지시할 때 사용한다.
= []
first_letter = re.compile(r"^\w+", re.MULTILINE)
pattern with open('data/zen2.txt', 'rt') as file:
= file.read()
content = pattern.findall(content)
match += match
first_letter print(first_letter)
['Beautiful', 'Explicit', 'Simple', 'Complex', 'Flat', 'Sparse', 'Readability']
다음은 file.readline()
함수를 사용한 경우이다. 이 함수는 파일의 내용을 한줄씩 반환하기 때문에 re.MULTILINE
플래그가 필요없다. 다만 하나의 line을 처리하는 코드를 작성하면 된다.
= []
first_letter = re.compile(r"^\w+")
pattern with open('data/zen2.txt', 'rt') as file:
while True:
= file.readline()
line if not line:
break
+= pattern.findall(line)
first_letter print(first_letter)
['Beautiful', 'Explicit', 'Simple', 'Complex', 'Flat', 'Sparse', 'Readability']
다음은 file
객체가 iterator의 일종이기 때문에 이를 for
루프에서 사용한 예이다.
= []
first_letter = re.compile(r"^\w+")
pattern with open('data/zen2.txt', 'rt') as file:
for line in file:
+= pattern.findall(line)
first_letter print(first_letter)
['Beautiful', 'Explicit', 'Simple', 'Complex', 'Flat', 'Sparse', 'Readability']
12.3.7.3 Word boundary
"cat"
이라는 정규 표현식은 독립적인 "cat"
에도 매칭되지만 "vocation"
의 "cat"
에도 매칭된다.
= re.compile(r"cat")
pattern = ["cat", "vocations" ]
texts for s in texts] [pattern.search(s)
[<re.Match object; span=(0, 3), match='cat'>,
<re.Match object; span=(2, 5), match='cat'>]
"cat"
이라는 단어 자체에 매칭시키고자 한다면 word boundary를 사용한다. 이것은 \b
로 표현한다.
= re.compile(r"\bcat\b")
pattern = ["cat", "vocations" ]
texts for s in texts] [pattern.search(s)
[<re.Match object; span=(0, 3), match='cat'>, None]
12.3.8 그룹핑과 백레퍼런스
정규 표현식에서 그룹은 전체 정규 표현식를 구성하는 일부 표현식을 의미하고, ()
로 만든다. 그룹은 왼쪽부터 1
번부터 차례로 번호가 부여된다. 번호 대신 이름을 부여할 수도 있다. 특정 그룹을 언급하는 것을 백레퍼런스라고 하고 \1
, \2
등과 같이 사용될 수 있다. 이 번호는 매치 객체의 group()
메서드에서 사용되어 해당 부분의 텍스트만 추출할 수도 있다.
다음은 두 개의 그룹을 사용한 정규 표현식이다.
import re
= re.compile(r'(Hello), (World)')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group(0))
print(match.group(1))
print(match.group(2))
Hello, World
Hello
World
그룹을 이름을 부여하는 문법은 (?P<name>...)
이다. name
위치에 이름을 쓰고 ...
위치에 표현식을 쓴다. 예를 들면 (?P<greeting>Hello)
과 같이 사용한다. 여기서 (?...)
은 확장 문법(extension notation)을 의미한다. P
는 Python
의 P
이다. 매칭된 텍스트를 추출할 때 이 이름을 group()
메서드의 인자로 사용할 수 있다. 이렇게 이름이 있는 그룹을 named group
이라고 한다.
= re.compile(r'(?P<greeting>Hello), (?P<target>World)')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group('greeting'))
print(match.group('target'))
Hello
World
그룹핑으로 해결할 수 있는 문제들도 많다. 이것은 뒤에서 다시 다룬다.
12.3.9 Lookahead and Lookbehind Assertions
정규 표현식에서 lookahead와 lookbehind는 특정 패턴이 뒤에 오거나 앞에 오는지 확인하는 방법이다. 이들은 매칭되는 텍스트를 포함하지 않고, 단지 조건을 확인하는 데 사용된다.
- Lookahead:
(?=...)
형태로 사용되며, 특정 패턴이 뒤에 오는지 확인한다. - Lookbehind:
(?<=...)
형태로 사용되며, 특정 패턴이 앞에 오는지 확인한다.
예를 들어, (?=abc)
는 “abc”가 뒤에 오는지 확인하는 lookahead assertion이다. 이 경우 “abc”는 매칭 결과에 포함되지 않는다.
import re
= re.compile(r'Hello(?=, World)')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group(0)) # "Hello"가 출력됨
Hello
위 경우에는 뒤에 , World
가 오는 경우는 Hello
가 매칭된다.
= re.compile(r'(?<=Hello), World')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group(0)) # ", World"가 출력됨
, World
!
을 사용하여 negative lookahead(?!)
를 만들 수 있다. 이 경우는 뒤에 오는 것이 , World
가 아닌 경우를 찾는다.
= re.compile(r'Hello(?!=, World)')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group(0)) # "Hello"가 출력됨
Hello
negative lookbehind도 마찬가지로 (?<!...)
형태로 사용된다. 이 경우는 앞에 오는 것이 Hello
가 아닌 경우를 찾는다.
= re.compile(r'(?<!Hello), World')
pattern = "Hello, World! Hello again."
text = pattern.search(text)
match if match:
print(match.group(0)) # 아무 것도 출력되지 않음
12.4 주요 옵션 플래그(Flags)
re.compile()
함수를 사용하여 패턴 객체를 만들 때, 옵션 플래그를 추가하여 정규 표현식의 행동을 조절할 수 있다. 이 값은 re
모듈의 attributes로 지정되어 있으며, 많은 경우 단축형도 가지고 있다. 예를 들어 re.MULTILINE
또는 re.M
등으로 사용할 수 있다.
- 대소문자를 구분하지 않게 한다:
re.IGNORECASE
또는re.I
import re
= re.compile(r"although", re.I)
pattern = """Although that way may not be obvious at first unless you're Dutch.
text Now is better than never.
Although never is often better than *right* now."""
pattern.search(text)
<re.Match object; span=(0, 8), match='Although'>
^
또는$
을 사용한 정규 표현식이 행(line)마다 실행되게 한다:re.MULTILINE
,re.M
|
을 사용하여 플래그들이 OR 로직으로 연결시킬 수 있다.
= re.compile(r"^Although", re.MULTILINE|re.I)
pattern for m in pattern.finditer(text):
print(m)
<re.Match object; span=(0, 8), match='Although'>
<re.Match object; span=(93, 101), match='Although'>
re.DOTALL
또는re.S
는 줄바꿈(\n
)을 무시하게 만든다. 즉 이것은 보통 와일드 카드.
가 “any single character except new line”에서 “new line”까지 포함게 한다.
다음은 이 플래그를 사용하지 않은 경우이다.
= re.compile(r".*")
pattern print(pattern.search(text).group())
Although that way may not be obvious at first unless you're Dutch.
re.DOTALL
을 사용하면 .
이 줄바꿈 문자까지 포함하게 된다. 따라서 다음과 같은 결과를 얻는다.
= re.compile(r".*", re.DOTALL)
pattern print(pattern.search(text).group())
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
이는
|
가 정규 표현식 엔진에서 가장 낮은 우선순위(precedence)를 가지고 있기 때문이다.↩︎https://learnbyexample.github.io/py_regular_expressions/anchors.html↩︎