Python 정규 표현식

정규 표현식은 텍스트 패턴 일치를 위한 강력한 언어입니다. 이 페이지에서는 Python 실습에 충분한 정규 표현식 자체에 대한 기본적인 사항을 소개하고 Python에서 정규 표현식이 작동하는 방식을 보여줍니다. Python 're' 모듈은 정규 표현식을 지원합니다.

Python에서 정규 표현식 검색은 일반적으로 다음과 같이 작성됩니다.

match = re.search(pat, str)

re.search() 메서드는 정규 표현식 패턴과 문자열을 가져와서 문자열에서 해당 패턴을 검색합니다. 검색에 성공하면 search()는 일치 객체를 반환하거나, 그러지 않으면 None을 반환합니다. 따라서 일반적으로 'word:' 패턴 다음에 3자리 단어를 검색하는 다음 예와 같이 검색에 성공했는지 테스트하기 위한 if 문이 바로 뒤이어집니다 (아래 세부정보 참고).

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

match = re.search(pat, str) 코드는 'match'라는 변수에 검색결과를 저장합니다. 그런 다음 if 문이 일치를 테스트합니다. true인 경우 검색이 성공하고 match.group()이 일치하는 텍스트입니다 (예: 'word:cat'). 일치가 false이면 (더 구체적으로 설명하지 않음) 검색에 실패한 것이며, 일치하는 텍스트가 없는 것입니다.

패턴 문자열 시작 부분의 'r'은 변경 없이 백슬래시를 통과하는 Python 'raw' 문자열을 지정하므로 정규 표현식에서 매우 편리합니다 (Java에는 이 기능이 꼭 필요합니다!). 항상 습관처럼 'r'을 사용하여 패턴 문자열을 작성하는 것이 좋습니다.

기본 패턴

정규 표현식의 강점은 고정 문자뿐 아니라 패턴을 지정할 수 있다는 것입니다. 다음은 단일 문자와 일치하는 가장 기본적인 패턴입니다.

  • a, X, 9, < -- 일반 문자가 정확히 일치합니다. 특별한 의미를 갖기 때문에 자신과 일치하지 않는 메타 문자는 다음과 같습니다. ^ $ * + ? { [ ] \ | ( ) (아래 세부정보 참고)
  • . (마침표) -- 줄바꿈 '\n'을 제외한 모든 단일 문자와 일치합니다.
  • \w -- (소문자 w)는 '단어' 문자(문자, 숫자 또는 밑줄 [a-zA-Z0-9_])와 일치합니다. 'word'가 연상 표현이기는 하지만 전체 단어가 아닌 단일 단어 문자와만 일치합니다. \W (대문자 W)는 단어가 아닌 문자와 일치합니다.
  • \b -- 단어와 비단어 사이의 경계
  • \s -- (소문자 s)는 단일 공백 문자(공백, 줄바꿈, return, 탭, 양식[ \n\r\t\f])와 일치합니다. \S (대문자 S)는 공백이 아닌 문자와 일치합니다.
  • \t, \n, \r -- 탭, 줄바꿈, 반환
  • \d -- 십진수 [0-9] (일부 이전 정규식 유틸리티는 \d를 지원하지 않지만 모두 \w 및 \s를 지원함)
  • ^ = start, $ = end -- 문자열의 시작 또는 끝과 일치합니다.
  • \ -- 캐릭터의 '특별함'을 억제합니다. 예를 들어 마침표와 일치시키려면 \. 를 사용하고 슬래시와 일치시키려면 \\를 사용하세요. 문자가 '@'과 같이 특별한 의미를 갖는지 확실하지 않은 경우 문자 앞에 슬래시(\@)를 넣어 보세요. \c와 같이 유효한 이스케이프 시퀀스가 아니면 Python 프로그램이 오류와 함께 중단됩니다.

기본 예시

농담: 눈이 세 개인 돼지를 뭐라고 불러? 안녕!

문자열 내의 패턴을 위한 정규 표현식 검색의 기본 규칙은 다음과 같습니다.

  • 검색은 문자열을 처음부터 끝까지 진행하며 발견된 첫 번째 일치 항목에서 중지됩니다.
  • 문자열 전체가 아닌 모든 패턴이 일치해야 합니다.
  • match = re.search(pat, str)가 성공하면 match는 None이 아니며 특히 match.group()은 일치하는 텍스트입니다.
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

반복

+와 * 를 사용하여 패턴의 반복을 지정하면 더 흥미로운 점이 있습니다.

  • + -- 패턴이 1회 이상 왼쪽에 표시됩니다.예: 'i+' = i가 하나 이상입니다.
  • * -- 왼쪽에 패턴이 0번 이상 나타납니다.
  • ? -- 패턴이 왼쪽에 일치하는 항목을 0개 또는 1개 일치

가장 왼쪽 및 가장 크게

먼저 패턴과 일치하는 가장 왼쪽 항목을 찾은 다음, 문자열을 최대한 많이 사용합니다. 즉, +와 * 를 최대한 많이 사용합니다 (+와 * 를 '탐욕스럽다'고 합니다).

반복 예시

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

이메일 예

'xyz alice-b@google.com 보라 monkey' 문자열에서 이메일 주소를 찾으려 한다고 가정해 보겠습니다. 이 예시를 실행 예로 사용하여 더 많은 정규 표현식 기능을 보여 드리겠습니다. 다음은 r'\w+@\w+' 패턴을 사용하여 시도한 것입니다.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

여기서는 \w가 주소의 '-' 또는 '.'와 일치하지 않기 때문에 전체 이메일 주소가 검색되지 않습니다. 아래의 정규 표현식 기능을 사용하여 이 문제를 해결해 보겠습니다.

대괄호

대괄호는 문자 집합을 나타내는 데 사용할 수 있으므로 [abc] 는 'a' 또는 'b' 또는 'c'와 일치합니다. \w, \s 등 코드는 대괄호 안에 사용할 수 있습니다. 단, 점 (.)이 문자 그대로의 점만 의미하는 것은 예외입니다. 이메일 문제의 경우 대괄호를 사용하면 '.' 및 '-'를 문자 집합에 추가할 수 있습니다. 문자 집합은 r'[\w.-]+@[\w.-]+' 패턴으로 @ 주위에 표시되어 전체 이메일 주소를 가져올 수 있습니다.

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(더 많은 대괄호 포함) 대시를 사용하여 범위를 표시할 수도 있으므로 [a-z] 는 모든 소문자와 일치합니다. 범위를 표시하지 않고 대시를 사용하려면 대시를 마지막에 넣으세요(예: [abc-]). 대괄호 집합 시작 부분의 up-hat(^)이 있으면 이를 반전시키므로 [^ab] 는 'a' 또는 'b'를 제외한 모든 문자를 의미합니다.

그룹 추출

정규 표현식의 '그룹' 기능을 사용하면 일치하는 텍스트 부분을 선택할 수 있습니다. 이메일 문제에서 사용자 이름과 호스트를 따로 추출하려 한다고 가정해 보겠습니다. 이렇게 하려면 r'([\w.-]+)@([\w.-]+)'와 같이 사용자 이름과 호스트 주위에 괄호( )를 추가합니다. 예를 들어 괄호는 패턴과 일치할 대상을 변경하지 않고 일치 텍스트 내부에 논리적 '그룹'을 설정합니다. 성공적인 검색에서 match.group(1)은 첫 번째 왼쪽 괄호에 해당하는 일치 텍스트이고 match.group(2)은 두 번째 왼쪽 괄호에 해당하는 텍스트입니다. 일반 match.group()은 여전히 평소와 같이 전체 일치 텍스트입니다.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

정규 표현식을 사용하는 일반적인 워크플로는 찾고 있는 항목의 패턴을 작성하고 괄호 그룹을 추가하여 원하는 부분을 추출하는 것입니다.

Findall

findall()은 re 모듈에서 가장 강력한 단일 함수일 것입니다. 위에서 re.search()를 사용하여 패턴과 일치하는 첫 번째 항목을 찾았습니다. findall()은 *모든* 일치 항목을 찾아 이를 문자열 목록으로 반환하며, 각 문자열은 하나의 일치 항목을 나타냅니다.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

파일이 있는 findall

파일의 경우 루프를 작성하여 파일의 줄을 반복하고 각 줄에서 findall()을 호출할 수 있습니다. 대신 findall()이 반복을 수행하도록 하면 훨씬 더 좋습니다. 전체 파일 텍스트를 findall()에 피드하고 단일 단계에서 모든 일치 항목 목록을 반환하도록 하기만 하면 됩니다. f.read()는 파일의 전체 텍스트를 단일 문자열로 반환한다는 점을 기억하세요.

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall 및 그룹스

괄호 ( ) 그룹 메커니즘은 findall()과 결합될 수 있습니다. 패턴에 2개 이상의 괄호 그룹이 포함된 경우, 문자열 목록을 반환하는 대신 findall()은 *튜플*의 목록을 반환합니다. 각 튜플은 패턴의 일치 하나를 나타내며 튜플 안에는 group(1), group(2) .. 데이터가 있습니다. 따라서 이메일 패턴에 2개의 괄호 그룹이 추가되면 findall()은 사용자 이름과 호스트를 포함하는 길이 2인 튜플 목록을 반환합니다(예: ('alice', 'google.com')).

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

튜플 목록이 있으면 이를 루프 처리하여 각 튜플에 대한 계산을 수행할 수 있습니다. 패턴에 괄호가 없으면 이전 예와 같이 findall()이 찾은 문자열 목록을 반환합니다. 패턴에 괄호 한 쌍이 포함되어 있으면 findall()은 단일 그룹에 해당하는 문자열 목록을 반환합니다. (모호한 선택적 특성: 패턴에 괄호 ( ) 그룹이 있는데 추출하지 않으려는 경우가 있습니다. 이 경우 시작 부분에 ?: 를 포함하는 괄호를 작성합니다(예: (?: )). 그러면 왼쪽 괄호는 그룹 결과로 집계되지 않습니다.)

RE 워크플로 및 디버그

정규 표현식 패턴은 적은 수의 문자로 많은 의미를 지니지만 매우 조밀하기 때문에 패턴을 디버깅하는 데 많은 시간을 할애할 수 있습니다. 예를 들어 작은 테스트 텍스트에서 실행하고 findall()의 결과를 출력하여 패턴을 실행하고 일치하는 항목을 쉽게 출력할 수 있도록 런타임을 설정합니다. 패턴이 아무것도 일치하지 않으면 패턴을 약화시키고 일부를 삭제하여 일치 항목이 너무 많아지도록 해보세요. 일치하는 항목이 없으면 확인할 구체적인 내용이 없기 때문에 진행이 불가능합니다. 너무 지저분해지면 조금 더 조여서 원하는 값을 얻을 수 있습니다.

옵션

re 함수는 패턴 일치 동작을 수정하는 옵션을 사용합니다. 옵션 플래그는 search() 또는 findall() 등에 추가 인수로 추가됩니다(예: re.search(pat, str, re.IGNORECASE)).

  • IGNORECASE -- 일치 시 대소문자를 구분하지 않으므로 'a'는 'a' 및 'A'와 모두 일치합니다.
  • DOTALL -- 점 (.)이 줄바꿈과 일치하도록 허용합니다. 일반적으로는 줄바꿈이 아닌 모든 단어와 일치합니다. 이런 경우 실수가 발생할 수 있습니다. .* 는 모든 항목과 일치한다고 생각할 수 있지만 기본적으로 줄의 끝을 넘지 않습니다. \s (공백)에는 줄바꿈이 포함되므로 줄바꿈이 포함될 수 있는 공백을 일치시키려면 \s*를 사용하면 됩니다.
  • MULTILINE -- 여러 줄로 구성된 문자열 내에서 ^ 및 $ 가 각 줄의 시작과 끝과 일치하도록 허용합니다. 일반적으로 ^/$ 는 전체 문자열의 시작과 끝과 일치합니다.

그리디 및 그리디 (선택사항)

연습에 필요하지 않은 고급 정규 표현식 기법을 보여주는 선택적 섹션입니다.

<b>foo</b> 등의 태그가 포함된 텍스트가 있다고 가정해 보겠습니다.

각 태그를 '(<.*>)' 패턴과 일치한다고 가정해 보겠습니다. 이 태그가 가장 먼저 일치하는 항목은?

결과는 약간 놀랍지만 .* 의 탐욕적 측면으로 인해 전체 '<b>foo</b> 및 <i>so on</i>'이 하나의 큰 일치 항목으로 간주됩니다. 문제는 .* 가 첫 번째 >에서 멈추지 않고 '탐욕스럽다'는 것입니다.

? .*? 또는 .+?와 같이 그 끝을 탐욕스럽지 않은 것으로 변경합니다. 이제 최대한 빨리 중지합니다. 따라서 '(<.*?>)' 패턴은 첫 번째 일치 항목으로 '<b>', 두 번째 일치 항목으로 '</b>'만 가져오며 각 <..> 쌍을 차례로 가져옵니다. 스타일은 일반적으로 .*? 실행 바로 뒤에 구체적인 마커(이 경우 >)를 사용하여 .*? 실행을 강제로 확장하게 하는 것입니다.

*? 확장자는 Perl에서 유래했으며 Perl의 확장 프로그램을 포함하는 정규 표현식을 Perl 호환 정규 표현식 -- pcre라고 합니다. Python에는 pcre 지원이 포함됩니다. 많은 명령줄 유틸리티 등에는 pcre 패턴을 허용하는 플래그가 있습니다.

'X에서 멈추는 경우를 제외한 모든 문자'라는 아이디어를 코딩하는 데 오래되었지만 널리 사용되는 기법에서는 대괄호 스타일을 사용합니다. 위의 경우 패턴을 작성할 수 있지만 .* 대신 모든 문자를 가져오려면 [^>]* 를 사용하세요. 그러면 >가 아닌 모든 문자를 건너뜁니다. 선행 ^는 대괄호 집합을 '반전'하므로 대괄호 안에 없는 모든 문자와 일치합니다.

대체 (선택사항)

re.sub(pat, replacement, str) 함수는 지정된 문자열에서 패턴의 모든 인스턴스를 검색하여 바꿉니다. 대체 문자열에는 일치하는 원래 텍스트의 그룹(1), 그룹(2) 등의 텍스트를 참조하는 '\1', '\2'가 포함될 수 있습니다.

다음은 모든 이메일 주소를 검색하여 사용자 (\1)를 유지하지만 호스트로 yo-yo-dyne.com을 보유하도록 변경하는 예입니다.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

연습

정규 표현식을 연습하려면 아기 이름 운동을 참조하세요.