07-1 정규 표현식 살펴보기

  • 정규 표현식(Regular Expressions), 정규식

정규 표현식은 왜 필요한가?

주민등록번호를 포함하고 있는 텍스트에서 모든 주민등록번호의 뒷자리를 * 문자로 변경하시오.

  • 정규식을 모르는 경우
    1. 전체 텍스트를 공백 문자로 나눈다(split).
    2. 나누어진 단어들이 주민번호 형식인지 조사.
    3. 단어가 주민번호 형식이면 뒷자리를 * 로 변환
    4. 나누어진 단어들을 다시 조립
In [2]:
data = '''
park 800905-1049118
kim 700905-1059119
'''

result = []

for line in data.split('\n'):
    word_result = []
    for word in line.split(' '):
        if len(word) == 14 and word[:6].isdigit() and word[6] == '-' and word[7:].isdigit():
            word = word[:7] + "*******"
        word_result.append(word)
    result.append(" ".join(word_result))
    
print("\n".join(result))
park 800905-*******
kim 700905-*******

In [3]:
# 정규식을 사용하면

import re

data = '''
park 800905-1049118
kim 700905-1059119
'''

pat = re.compile("(\d{6})[-]\d{7}")
print(pat.sub("\g<1>-*******", data))
park 800905-*******
kim 700905-*******


07-2 정규 표현식 시작하기

정규 표현식의 기초, 메타 문자

  • 정규 표현식에는 다음과 같은 메타 문자가 있다.

. ^ $ * + ? { } [ ] \ | ( )

문자 클래스 [ ]

문자 클래스로 만들어진 정규식은 "[ 와 ] 사이의 문자들과 매치" 라는 의미를 갖는다.

문자 클래스를 만드는 메타 문자인 [ 와 ] 사이에는 어떤 문자도 들어갈 수 있다.

즉, 정규 표현식이 [abc] 라면 이 표현식의 의미는 "a, b, c 중 한개의 문자와 매치"를 뜻한다.

  • "a"는 정규식과 일치하는 문자인 "a"가 있으므로 매치
  • "before"는 정규식과 일치하는 문자인 "b"가 있으므로 매치
  • "dude"는 정규식과 일치하는 문자인 a, b, c 중 어느 하나도 포함하고 있지 않으므로 매치되지 않음

[ ] 안에 하이픈(-)을 사용하면 두 문자 사이의 범위(From - To)를 의마한다. [a-c]는 [abc]와 동일하고, [0-5]는 [012345]와 동일하다.

  • [a-zA-Z]: 알파벳 모두
  • [0-9]: 숫자

메타 문자 ^ 는 반대(not)라는 의미로 주의를 해야 한다.

  • [^0-9]: 숫자가 아닌 문자만 매치

[자주 사용하는 문자 클래스]

[0-9], [a-zA-Z] 등과 같이 자주 사용하는 정규식의 경우 다음과 같이 별도의 표기법으로 표현할 수 있다.

  • \d - 숫자와 매치, [0-9]와 동일
  • \D - 숫자가 아닌 것과 매치, [^0-9]와 동일
  • \s - whitespace 문자와 매치, [ \t\n\r\f\v]와 동일, 맨 앞은 공백 문자
  • \S - whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일
  • \w - 문자+숫자와 매치, [a-zA-Z0-9]와 동일
  • \W - 문자+숫자가 아닌 문자와 매치, [^a-zA-Z0-9]와 동일

Dot(.)

Dot(.) 메타 문자는 \n(줄바꿈 문자)를 제외한 모든 문자와 매치된다.

  • a.b - "a + 모든문자 + b"

    • aab: 매치
    • a0b: 매치
    • abc: "a"와 "b" 사이에 어떤 문자도 없기 때문에 매치되지 않는다.
  • a[.]b - "모든 문자"라는 의미가 아닌 문자 . 그대로를 의미한다.

반복(*)

  • cat - 문자 앞의 a가 무한대로 반복될 수 있다는 의미(0번 포함)
    • ct, cat, caaaat

반복(+)

최소한 한번 이상은 나와야 하는 반복

  • ca+t: cat, caaat 는 매치가 되나 ct 는 매치되지 않음

반복({m, n}, ?)

{ } 메타 문자를 사용하면 반복 횟수를 고정시킬 수 있다.

  • {m, n}: m부터 n까지 반복
  • {m, }: 반복횟수가 m 이상인 경우
  • {, n}: 반복횟구가 n 이하인 경우
  • {1, }: + 와 동일
  • {0, }: * 와 동일
  • ?: {0, 1} 과 동일

파이썬에서 정규 표현식을 지원하는 re 모듈

파이썬이 설치될 때 기본 라이브러리로 정규식을 지원하기 위한 re 모듈을 제공한다.

import re
p = re.compile('ab*')

re.compile 을 이용하여 정규식을 컴파일하고 컴파일된 패턴객체(p)를 이용하여 작업을 수행한다.

정규식을 이용한 문자열 검색

  • match(): 문자열의 처음부터 정규식과 매치되는지 조사
  • search(): 문자열 전체를 검색하여 정규식과 매치되는지 조사
  • findall(): 정규식과 매치되는 모든 문자열(substring)을 리스트로 리턴
  • finditer(): 정규식과 매치되는 모든 문자열(substring)을 iterator 객체로 리턴

match, search 는 정규식과 매치될 때에는 match 객체를 리턴하고 그렇지 않을 경우에는 None을 리턴한다.

In [2]:
import re
p = re.compile('[a-z]+')
In [2]:
# match

m = p.match("python")
print(m)
<_sre.SRE_Match object; span=(0, 6), match='python'>
In [3]:
m = p.match("3 python")
print(m)
None
In [4]:
m = p.match("string goes here")
if m:
    print("match found: ", m.group())
else:
    print("No match")
match found:  string
In [5]:
# search

m = p.search("python")
print(m)
<_sre.SRE_Match object; span=(0, 6), match='python'>
In [6]:
m = p.search("3 python") # 문자열 전체를 검색하기 때문에 매치
print(m)
<_sre.SRE_Match object; span=(2, 8), match='python'>
In [3]:
# findall

result = p.findall("life is too short")
print(result)
['life', 'is', 'too', 'short']
In [3]:
# finditer

result = p.finditer("life is too short")
print(result)
<callable_iterator object at 0x7fba643a9978>
In [4]:
for r in result: 
    print(r)
    print(r.group())
<_sre.SRE_Match object; span=(0, 4), match='life'>
life
<_sre.SRE_Match object; span=(5, 7), match='is'>
is
<_sre.SRE_Match object; span=(8, 11), match='too'>
too
<_sre.SRE_Match object; span=(12, 17), match='short'>
short

match 객체의 메서드

  • group(): 매치된 문자열을 리턴
  • start(): 매치된 문자열의 시작 위치를 리턴
  • end(): 매치된 문자열의 끝 위치를 리턴
  • span(): 매치된 문자열의 (시작, 끝)에 해당하는 튜플을 리턴
In [3]:
m = p.match("python")
In [4]:
m.group()
Out[4]:
'python'
In [5]:
m.start()
Out[5]:
0
In [6]:
m.end()
Out[6]:
6
In [7]:
m.span()
Out[7]:
(0, 6)

컴파일 옵션

  • DOTALL(S): . 이 줄바꿈 문자를 포함하도록 한다.
  • IGNORECASE(I): 대소문자를 구분없이 매치한다.
  • MULTILINE(M): 여러줄과 매치한다.
  • VERBOSE(X): verbose 모드를 사용할 수 있도록 한다.
In [8]:
# DOTALL, S

import re

p = re.compile('a.b')
m = p.match('a\nb')
print(m)
None
In [9]:
p = re.compile('a.b', re.DOTALL)
m = p.match('a\nb')
print(m)
<_sre.SRE_Match object; span=(0, 3), match='a\nb'>
In [10]:
# IGNORECASE, I

p = re.compile('[a-z]', re.I)
p.match('python')
Out[10]:
<_sre.SRE_Match object; span=(0, 1), match='p'>
In [11]:
p.match('python3')
Out[11]:
<_sre.SRE_Match object; span=(0, 1), match='p'>
In [12]:
p.match('python 3')
Out[12]:
<_sre.SRE_Match object; span=(0, 1), match='p'>
In [14]:
p.match('3 python')
In [15]:
p.match('Python')
Out[15]:
<_sre.SRE_Match object; span=(0, 1), match='P'>
In [16]:
p.match('PYTHON')
Out[16]:
<_sre.SRE_Match object; span=(0, 1), match='P'>
In [17]:
# MULTILINE, M

import re
p = re.compile("^python\s\w+")

data = """python one
life is too short
python two
you need python
python three"""

print(p.findall(data))
['python one']
In [18]:
p = re.compile("^python\s\w+", re.M)

print(p.findall(data))
['python one', 'python two', 'python three']
In [19]:
# VERBOSE, X

charref = re.compile(r'&[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')
In [20]:
charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)
  • 패턴 객체인 charref 는 동일한 역할을 한다.
  • re.VERBOSE 옵션을 사용하면 문자열에 사용된 whitespace는 컴파일 시 제거된다.([] 내에 사용된 것은 제외)

07-3 강력한 정규 표현식의 세계로

메타문자

문자열 소모가 없는 메타 문자

In [21]:
# | 메타문자는 or 의 의미돠 동일하다.

p = re.compile("Crow|Servo")
m = p.match("CrowHello")
print(m)
<_sre.SRE_Match object; span=(0, 4), match='Crow'>
In [22]:
# ^ 는 문자열의 맨 처음과 일치함을 의미한다.

print(re.search("^Life", "Life is woo short"))
<_sre.SRE_Match object; span=(0, 4), match='Life'>
In [23]:
print(re.search("^Life", "My Life"))
None
In [24]:
# $ 는 ^ 메타문자와 반대의 경우

print(re.search("short$", "Life is too short"))
<_sre.SRE_Match object; span=(12, 17), match='short'>
In [25]:
print(re.search("short$", "Life is too short, you need python"))
None

\A

  • 문자열의 처음과 매치
  • ^ 와 동일한 의미이지만, re.MULTILINE 옵션을 사용할 경우는 라인과 상관없이 전체 문자열의 처음하고만 매치

\Z

  • 문자열의 끝과 매치
  • $ 와 동일한 의미이지만, re.MULTILINE 옵션 사용 시 라인과 상관없이 전체 문자열의 끝하고만 매치
In [26]:
# \b: 단어 구분자

p = re.compile(r'\bclass\b')
In [27]:
print(p.search("no class at all"))
<_sre.SRE_Match object; span=(3, 8), match='class'>
In [28]:
print(p.search("the declassified algorithm"))
None
In [29]:
print(p.search("one subclass is"))
None
In [30]:
# \B: \b 와 반대의 경우 매치된다.

p = re.compile(r'\Bclass\B')
In [31]:
print(p.search("no class at all"))
None
In [32]:
print(p.search("the declassified algorithm"))
<_sre.SRE_Match object; span=(6, 11), match='class'>
In [33]:
print(p.search("one subclass is"))
None

그룹핑

  • 반복되는 문자열을 찾는 경우
  • 매치된 문자열 중에서 특정 부분의 문자열만 뽑아내긴 위한 경우
  • 그룹핑된 문자열 재참조
  • 그룹을 만들어 주는 메타문자는 '(', ')'
In [1]:
import re
In [2]:
p = re.compile('(ABC)+')
m = p.search('ABCABCABC OK?')
print(m)
<_sre.SRE_Match object; span=(0, 9), match='ABCABCABC'>
In [3]:
print(m.group())
ABCABCABC
In [4]:
p = re.compile(r"\w+\s+\d+[-]\d+[-]\d+")
m = p.search("park 010-1234-1234")
print(m.group())
park 010-1234-1234
In [5]:
p = re.compile(r"(\w+)\s+\d+[-]\d+[-]\d+")
m = p.search("park 010-1234-1234")
print(m.group())
park 010-1234-1234
In [6]:
print(m.group(1))
park
In [7]:
print(m.group(2))
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-7-9d9166b3aa7e> in <module>()
----> 1 print(m.group(2))

IndexError: no such group
In [8]:
print(m.group(0))
park 010-1234-1234
In [9]:
p = re.compile(r"(\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
In [10]:
print(m.group())
print(m.group(0))
print(m.group(1))
print(m.group(2))
print(m.group(3))
print(m.group(4))
park 010-1234-1234
park 010-1234-1234
park
010-1234-1234
010
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-614b6fd0421a> in <module>()
      4 print(m.group(2))
      5 print(m.group(3))
----> 6 print(m.group(4))

IndexError: no such group
In [11]:
p = re.compile(r'(\b\w+)\s+\1')
p.search('Paris in the the spring').group()
Out[11]:
'the the'

그룹핑된 문자열에 이름 붙이기

  • 정규식 내에 그룹이 많아질 경우 관리하기 위해 다음과 같이 그룹명을 지정할 수 있다.

    (?P\w+)\s+((\d+)[-]\d+[-]\d+)

  • 달라진 부분은 다음과 같다. (\w+)라는 그룹에 name 이라는 이름을 지정

    (\w+) --> (?P\w+)

In [12]:
p = re.compile(r"(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group("name"))
park
In [13]:
# 그룹명으로 재참조

p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
p.search('Paris in the the spring').group()
Out[13]:
'the the'

전방 탐색

  • 긍정형 전방 탐색((?=...)) - ... 에 해당되는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소모되지 않는다.
  • 부정형 전방 탐색((?!...)) - ...에 해당되는 정규식과 매치되지 않아야 하며 조건이 통과되어도 문자열이 소모되지 않는다.
In [14]:
p = re.compile(".+:")
m = p.search("http://google.com")
print(m.group())
http:
In [15]:
# : 제외한 결과를 얻기 위해 전방탐색 적용

p = re.compile(".+(?=:)")
m = p.search("http://google.com")
print(m.group())
http

부정형 전방 탐색

In [1]:
import re
In [3]:
p = re.compile(".*[.](?!bat$).*$")
m = p.search("test.txt")
print(m.group())
test.txt
In [4]:
m = p.search("test.bat")
print(m.group())
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-af87954517f3> in <module>()
      1 m = p.search("test.bat")
----> 2 print(m.group())

AttributeError: 'NoneType' object has no attribute 'group'
In [5]:
m = p.search("test.exe")
print(m.group())
test.exe
In [6]:
p = re.compile(".*[.](?!bat$|exe$).*$")
m = p.search("test.txt")
print(m.group())
test.txt
In [7]:
m = p.search("test.bat")
print(m.group())
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-af87954517f3> in <module>()
      1 m = p.search("test.bat")
----> 2 print(m.group())

AttributeError: 'NoneType' object has no attribute 'group'
In [8]:
m = p.search("test.exe")
print(m.group())
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-499eb26ffefe> in <module>()
      1 m = p.search("test.exe")
----> 2 print(m.group())

AttributeError: 'NoneType' object has no attribute 'group'

문자열 바꾸기

In [9]:
p = re.compile('(blue|red|white)')
p.sub('color', 'blue socks and red shoes')
Out[9]:
'color socks and color shoes'
In [10]:
# 바꾸는 횟수를 제한

p.sub('color', 'blue socks and red shoes', count=1)
Out[10]:
'color socks and red shoes'

[sub 메서드와 유사한 subn 메서드]

  • 동일한 기능이지만 결과를 튜플로 리턴한다.
  • 리턴되는 튜플의 첫번째 요소는 변경된 문자열이고, 두번째 요소는 바꾸기가 발생한 횟수이다.
In [11]:
p.subn('color', 'blue socks and red shoes')
Out[11]:
('color socks and color shoes', 2)

sub 메서드 사용 시 참조 구문 사용하기

In [1]:
import re
In [2]:
p = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
print(p.sub("\g<phone> \g<name>", "park 010-1234-1234"))
010-1234-1234 park
In [3]:
# 그룹명 대신 참조번호 이용

p = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
print(p.sub("\g<2> \g<1>", "park 010-1234-1234"))
010-1234-1234 park

sub 메서드의 입력 인수로 함수 넣기

In [4]:
def hexrepl(match):
     "Return the hex string for a decimal number"
     value = int(match.group())
     return hex(value)
In [5]:
p = re.compile(r'\d+')
p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
Out[5]:
'Call 0xffd2 for printing, 0xc000 for user code.'

Greedy vs Non-Greedy

  • * 메타문자는 매우 탐욕스러워서 매치할 수 있는 최대한의 문자열을 소모한다.
In [6]:
s = '<html><head><title>Title</title>'
len(s)
Out[6]:
32
In [7]:
print(re.match('<.*>', s).span())
(0, 32)
In [8]:
print(re.match('<.*>', s).group())
<html><head><title>Title</title>
  • < html > 이라는 문자열만 소모되도록 제한하기 위해서는 Non-Greedy 문자인 ? 를 사용한다.
  • non-greedy 문자인 ?은 *?, +?, ??, {m,n}?과 같이 사용할 수 있다.
  • 가능한 한 가장 최소한의 반복을 수행하도록 도와주는 역할을 한다.
In [9]:
print(re.match('<.*?>', s).group())
<html>