지난 시간에 이어서 regular expression을 계속 다뤄보겠습니다. 만약 ‘cat’이나 ‘dog’가 포함된 문장을 찾고 싶으면 어떻게 해야 할까요? []를 써서는 안된다는 거 아시죠? (‘c’, ‘a’, ‘t’, ‘d’, ‘o’, ‘g’ 이렇게 찾게 될 테니까요..) 이럴때는 | 파이프를 통해서 해결할 수 있습니다.

>>> import re

# '|'를 통해 패턴 자체에 or를 적용할 수 있습니다.
>>> pairs = [('cat|dog', 'cats are here'),
         ('cat|dog', 'dogs are there')]
>>> for pattern, string in pairs:
...    s = re.search(pattern, string)
...    if s:
...        print s.start(), s.end(), string[s.start():s.end()]
...    else:
...        print "Not Found"
0 3 cat
0 3 dog

그렇다면 ‘guppy’라는 단어를 생각해봅시다. 복수형은 ‘guppies’가 될테고, 이 때 두 가지 모두를 고려해서 찾고 싶으면 어떻게 해야 할까요? |는 패턴 하나에 적용이 되지 한 글자에 적용되는 것이 아니므로 주의하셔야 합니다!

# ()를 통해서 disjunction이 적용될 범위를 구할 수 있습니다.
>>> pairs = [('guppy|ies', 'guppies'),
         ('gupp(y|ies)', 'guppies')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]
...     else:
...         print "Not Found"
4 7 ies
0 7 guppies

결과를 보시면 아시겠지만, 괄호 ‘()’를 치지 않은 경우에는 ‘guppy’라는 단어 또는 ‘ies’라는 단어를 찾게 됩니다. 우리가 원하는 결과를 얻으려면 'gupp(y|ies)'라고 해 주어야 합니다.

반면에 '*'는 원래는 한 문자에만 적용되는 것입니다. 하지만 ()를 통해서 전체 패턴을 중복으로 검색할 수 있습니다.

# '*'는 원래 한 문자에만 적용되는 것이지만, ()를 통해서 전체 중복으로 검색할 수 있습니다.
>>> pairs = [('Column [0-9]+ *', 'Column 1 Column 2 Column 3'),
         ('(Column [0-9]+ *)*', 'Column 1 Column 2 Column 3')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]
...     else:
...         print "Not Found"
0 9 Column 1 
0 26 Column 1 Column 2 Column 3

처음 패턴은 띄어쓰기만이 *의 영향권 아래에 있기 때문에 Column 1만 출력하고 끝나지만, 괄호로 패턴을 묶어준 후 *를 하게 되면 동일한 패턴의 반복을 붙여서 찾을 수 있게 됩니다. 이를 통해서 우리는 연산자에 처리 우선순위가 있다는 것을 알 수 있죠.

연산자 처리 우선순위
1. Parenthesis ()
2. Counters * + ? {}
3. Sequences and anchors the ^my end$
4. Disjunction |

그럼 이제 좀 더 어려운 example들을 다뤄볼까요?

# Advanced examples
>>> pairs = [('\$[0-9]+', '500MHz 3.5Gb 32 Megabytes, Mac $999.99 or $1000'),
         ('\$[0-9]+\.[0-9][0-9]', '500MHz 3.5Gb 32 Megabytes, Mac $999.99 or $1000'),
         ('\$[0-9]+(\.[0-9][0-9])?', '500MHz 3.5Gb 32 Megabytes, Mac $999.99 or $1000'),
         (r'\b[0-9]+ *(M[hH]z|[Mm]egahertz|G[hH]z|[gG]igahertz)\b', '500MHz 3.5Gb 32 Megabytes, Mac $999.99 or $1000'),
         (r'\b[0-9]+(\.[0-9]+)? *([mM]b|[Mm]egabytes?|[gG]b|[gG]igabytes?)\b', '500MHz 3.5Gb 32 Megabytes, Mac $999.99 or $1000')]
>>> for pattern, string in pairs:
...     s = re.finditer(pattern, string)
...     for obj in s:
...         print obj.start(), obj.end(), string[obj.start():obj.end()]
...     print ''

31 35 $999
42 47 $1000

31 38 $999.99

31 38 $999.99
42 47 $1000

0 6 500MHz

7 12 3.5Gb
13 25 32 Megabytes

먼저 첫번째 예제에서는 달러 표시($)를 포함한 연속된 숫자만 잡아내게 되어있습니다. 따라서 소수점은 잡아내지 못했죠. 반면 두번째 예제에서는 소수점을 포함한 소수 둘째자리까지 잡아내기 때문에 $999.99만 잡아내고 $1000은 잡아내지 못합니다. 정확히 우리가 의도한 것을 해내기 위해서는 ()를 통해 소수점부터 두 숫자를 grouping하여 있어도 되고 없어도 되는 ?로 처리해줘야 할 것입니다. 세번째 예제가 이를 증명하고 있죠.

네번째 예제는 ‘MHz’또는 ‘GHz’에 대한 항목을 잡아내는 부분입니다. 이제는 딱 보면 어떤 패턴을 잡아낼 지 예상이 되시죠?

마지막 다섯번째 예제는 용량 부분을 잡아내는 것인데, 소수점 이하의 자리가 하나 이상으로 처리된 부분이 조금 특별하네요. 이제 어느정도 우리도 원하는 패턴을 찾아낼 정도가 된 것 같네요 :)