전체 페이지뷰

2016년 12월 3일 토요일

Regular expression with Python, Grouping

Grouping






그룹핑은 매우 유용한 도구입니다.
1. qunatifier를 subgroup에 적용하기 편리합니다.
2. 전체 식으로부터 변형해야 할 정확한 부위를 지정할 수 있습니다.
3. 매칭된 패턴으로부터 정보를 얻어낼 수 있습니다.
4. 식 안에서 추출된 정보를 반복 사용할 수 있습니다.


Introduction

앞에서 우리는 이미 그룹핑을 수차례 적용해 봤습니다. ()라는 두개의 메타캐릭터를 이용함으로써 그룹핑이 이루어집니다. 예를 들어, 다음과 같은  상품코드 '1-a2-b' 가 있다고 가정해 봅시다.

>>> re.match(r"(\d-\w){2,3}", r"1-a2-b")
<_sre.SRE_Match object; span=(0, 6), match='1-a2-b'>

패턴 내부의 ( )는 매칭되는 정보가 하나의 유닛으로서 다루어져야 된다는 것을 알려줍니다.

다음 예를 봅시다.

>>> re.search(r"(ab)+c", r"ababc")
<_sre.SRE_Match object; span=(0, 5), match='ababc'>
>>> re.search(r"(ab)+c", r"abbc")
>>> 

(ab)가 하나 이상 있고 c로 이어지는 패턴을 찾아야 할때, 우리는 선행 조건인 ab를 하나로 묶어 정확히 지정 할 수가 있습니다.

변형의 범위를 정확히 지정해 주는 다른 예를 봅시다.
스페인을 뜻하는 스페인어는 España 혹은 Español입니다(혼동을 피하기 위해 ñ은 n으로 표기하기로 하겠습니다).
>>> re.search("Espana|ol", "Espanol")
<_sre.SRE_Match object; span=(5, 7), match='ol'>
>>> re.search("Espana|ol", "Espana")
<_sre.SRE_Match object; span=(0, 6), match='Espana'>

패턴의 가운데 a|o는 a나 o 중 하나가 온다는 뜻이므로 매칭되는 결과가 나타납니다.
그러나, 문제는 이것이 'ol'에도 매칭된다는 것입니다.

>>> re.search("Espana|ol", "ol")
<_sre.SRE_Match object; span=(0, 2), match='ol'>

그래서 패턴을 바꿔보도록 하겠습니다.

>>> re.search("Espan[aol]", "Espanol")
<_sre.SRE_Match object; span=(0, 6), match='Espano'>
>>> re.search("Espan[aol]", "Espana")
<_sre.SRE_Match object; span=(0, 6), match='Espana'>

성공입니다. 그러나 또 문제가 생겼습니다. 스페인어로 아무런 의미도 없는 "Espano"와 "Espanl"도 매칭이 된다는 점입니다.

>>> re.search("Espan[a|ol]", "Espano")
<_sre.SRE_Match object; span=(0, 6), match='Espano'>

해답은 괄호를 사용하는 것입니다.

>>> re.search("Espan(a|ol)", "Espana")
<_sre.SRE_Match object; span=(0, 6), match='Espana'>
>>> re.search("Espan(a|ol)", "Espanol")
<_sre.SRE_Match object; span=(0, 7), match='Espanol'>
>>> re.search("Espan(a|ol)", "Espan")
>>> re.search("Espan(a|ol)", "Espano")
>>> re.search("Espan(a|ol)", "ol")

Grouping의 또다른 중요한 속성은 "capturing"입니다. 그룹은 매칭된 패턴을 캡쳐해 주어 다른 연산을 수행할 수 있게 도와줍니다.

예를 들어 이러한 상품코드가 있다고 가정해봅시다. 이 코드는 "국가를 표시하는 숫자-DB의 ID로 쓰이는 영문자"로 구성되어 있습니다. 이 중 국가코드를 추출해 달라는 요청을 받았습니다.

>>> pattern = re.compile(r"(\d+)-\w+")
>>> it = pattern.finditer(r"1-a\n20-baer\n34-afcr")
>>> match= next(it)
>>> match.group(1)
'1'
>>> match= next(it)
>>> match.group(1)
'20'
>>> match= next(it)
>>> match.group(1)
'34'

위와 같이 국가 넘버에 해당하는 부위를 ( )로 묶어 그룹으로 표시할 수 있습니다.

Backreferences

패턴의 뒤에 backreference(재참조)를 주어 그룹 연산을 더 유용하게 만들 수 있습니다.

>>> pattern = re.compile(r"(\w+) \1")
>>> match = pattern.search(r"hello hello world")
>>> match.group()
'hello hello'

한개 이상의 문자가 그룹으로 지정되고 ㅡ 이후에 공백이 오는 패턴입니다. 끝에 '\1'이라는 backreference를 주었습니다. 패턴과 매칭되는 첫번째 그룹을 또 다시 참조하라는 뜻이 되므로 hello가 두 개가 됩니다. 따라서 같은 단어가 연속된 부분을 찾으라는 말이 됩니다. 여기서 재참조는 99까지 줄 수 있습니다.

여기서 아까의 그 국가-DB 상품코드의 예를 다시 들어 봅시다.
이 코드의 순서를 바꾸어 DB-국가의 순서로 수정하고자 합니다.

>>> pattern = re.compile(r"(\d+)-(\w+)")
>>> pattern.sub(r"\2-\1", "1-a\n20-baer\n34-afcr")
'a-1\nbaer-20\nafcr-34'

굉장히 간결하고 함축적이지 않습니까?
국가넘버를 그룹으로 캡쳐하여 1번 레퍼런스로 받고, DB 아이디를 2번 레퍼런스로 받아서 sub함수로 순서를 바꾸어 줍니다.

Named group

캡쳐되는 그룹이 많아지고 참조되는 backreference가 많아지면 점점 사용하기가 복잡해집니다. 이 문제를 풀기 위해 Guido Van Rossum은 1997년 Python1.5에 named group을 도입하였고, 지금은 거의 대부분의 언어에서 사용하고 있습니다.

Named group을 사용하기 위해서는 ?P<name>pattern의 문법을 사용합니다.

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)")
>>> match = pattern.search("Hello world")
>>> match.group("first")
'Hello'
>>> match.group("second")
'world'

그래서 backrefernce를 사용하기도 훨씬 편해집니다.
sub연산에서 그룹이름을 사용하기 위해서는 \g<name>을 사용하면 됩니다.

>>> pattern = re.compile(r"(?P<country>\d+)-(?P<id>\w+)")
>>> pattern.sub(r"\g<id>-\g<country>", "1-a\n20-baer\n34-afcr")
'a-1\nbaer-20\nafcr-34'

Non-capturing group

내용을 캡쳐하는 것만이 group을 사용하는 목적은 아닙니다. Group을 사용하고 싶지만 내용 추출에는 관심이 없는 경우가 있는데 alternation이 그 예입니다.

다음과 같이 subexpression을 만들기 위해 그룹을 사용한 예를 봅시다.

>>> re.search("Españ(a|ol)", "Español")
<_sre.SRE_Match object; span=(0, 7), match='Español'>
>>> re.search("Españ(a|ol)", "Español").groups()
('ol',)

캡쳐된 내용에는 관심이 없지만 그냥 저절로 캡쳐되어집니다. 
내용 캡쳐 없이 그룹을 사용하고 싶다면 ?:pattern의 문법을 사용하면 됩니다.

>>> re.search("Españ(?:a|ol)", "Español")
<_sre.SRE_Match object; span=(0, 7), match='Español'>
>>> re.search("Españ(?:a|ol)", "Español").groups()
()

같은 내용이지만 그룹으로 캡쳐된 내용이 존재하지 않음을 알 수 있습니다.

yes-pattern/no-pattern

이 패턴은 첫번째 언어의 if-else 구문과 비슷합니다.

(?(id/name)yes-pattern|no-pattern)
의 문법을  사용하는데, id와 매칭되는 그룹이 존재한다면 yes-pattern을 매칭하고 존재하지 않는다면 no-pattern을 매칭하라는 뜻입니다(if-else와 마찬가지로 if만 쓰일때는 no-pattern을 생략하면 됩니다).

역시 예를 들어 보겠습니다. 전과 같은 상품코드인데 이번엔 조금 다르게 설정합니다.
1."34-adrl-01"처럼 두 자리수 국가코드-3,4개의 문자-두자리수 지역코드
2. 단순히 adrl처럼 3-4자리의 문자로만 이루어진 상품코드
가 혼재된 리스트를 가지고 있다고 가정합니다.

>>> pattern = re.compile(r"(\d\d-)?(\w{3,4})(?(1)(-\d\d))")
>>> pattern.match("34-erte-22")
<_sre.SRE_Match object; span=(0, 10), match='34-erte-22'>
>>> pattern.search("erte")
<_sre.SRE_Match object; span=(0, 4), match='erte'>

패턴을 살펴봅시다. ?(id)에 해당하는 부분이 뒤에 있어서 좀 혼동될 수 있을텐데요,
(\d\d-)로 표현되는 국가코드 그룹1, (\w{3, 4})로 해당하는 문자부 그룹2, 마지막으로 지역코드인 (-\d\d)그룹 3가 있습니다. 조건이 쓰인건 그 중 지역코드 매칭 부분입니다.

?(1)(-\d\d) 라는 것은
그룹1이 있을때만 (-\d\d)에 해당하는 지역코드를 매칭하라는 소리입니다. 

따라서 첫번 째 "34-erte-22"에는 국가코드가 있으므로 지역코드까지 매칭했고,
두번째 "erte"에는 국가코드가 없으므로 지역코드도 매칭하지 않았습니다.

>>> pattern.match("34-erte")
None

위의 예는 yes-pattern만 있는 경우입니다. No-pattern을 살펴보기 위해 상품코드 형식을 조금 바꿔보겠습니다.
1. 위와 똑같이 "34-adrl-01"처럼 두 자리수 국가코드-3,4개의 문자-두자리수 지역코드
2. 3-4개의 영문자-3-4개 영문자(adrl-sala 같은)

>>> pattern = re.compile(r"(\d\d-)?(\w{3,4})-(?(1)(\d\d)|[a-z]{3,4})$")
>>> pattern.match("34-erte-22")
<_sre.SRE_Match object; span=(0, 10), match='34-erte-22'>

패턴을 살펴봅시다.
좀 더 복잡해졌습니다만 조건부분만 뗴어서 보겠습니다.

(?(1)(\d\d)|[a-z]{3,4})$

그룹1이 있을 때 | 앞에 있는 (\d\d)를 매칭하고, 그룹 1이 없으면 문자3-4개를 매칭하라는 뜻이 되겠습니다. 따라서 국가코드가 있으면 뒤에 숫자(지역코드)가 오도록 매칭하고, 국가코드가 없으면 문자3-4개를 매칭하라는 뜻이 됩니다.

>>> pattern.match("34-erte")
>>> pattern.match("erte-abcd")
<_sre.SRE_Match object; span=(0, 9), match='erte-abcd'>

Overlapping group

많은 사람들이 다음과 같은 예에서 큰 혼란을 겪게 됩니다.
>>> re.findall(r'(a|b)+', 'abaca')
['a', 'a']

왜 결과가 ['aba', 'a']가 아닐까요?

이 과정을 순차적으로 들여다 보기로 합시다.


분명 aba와 a가 매칭 됩니다만 캡쳐된 그룹은 그림에서 보듯이 가장 마지막 글자에 머물게 됩니다. 그럼 aba,a 가 매칭되게 하려면 어떻게 해야 할까요?

>>> re.findall(r'((?:a|b)+)', 'abaca')
['aba', 'a']

그럼 한 글자씩 매칭시키려면?

>>> re.findall(r'(a|b)', 'abaca')
['a', 'b', 'a', 'a']


위와 같이 조합하여 매칭된 결과의 부분 혹은 전체를 얻어낼 수 있습니다.

댓글 없음:

댓글 쓰기