Python) pregex 로 편하게 정규 표현식 사용하기

2022. 7. 30. 14:07분석 Python/Packages

텍스트에서 원하는 부분을 추출하기 위해서 정규표현식을 작성하는데, 고수들은 다 잘할 것이겠지만, 좀 더 편하게 하는 패키지를 찾은 것 같아서 테스트도 해볼 겸 작성한다.

 

소개할 패키지는 pregex라는 패키지이다.

사람 친화적인 방식으로 정규 표현식을 쓸 수 있어서 가독성 면에서나 활용면에서 쓸만해보인다.

문제는 파이썬 3.9 이상부터 지원된다는 것이라서, 이번 기회에 기존 유저들은 자연스럽게 파이썬 버전을 올리면 좋을 것 같다 ㅎㅎ

 

설치 방법

파이썬 3.9 이상

pip install pregex

파이썬 예시

 

간단한 URL 찾기

아래 TEXT에서 URL을 찾는 행위를 한다고 해보자.

그것을 일반적으로 찾으려고 하면, 위의 그림처럼 복잡하게 코딩을 해야 하고, 의도를 파악하기 위해서는 만든 사람의 설명이 필요해 보인다.

 

하지만 해당 패키지를 사용하면, 보다 쉽게 이해할 수 있다.

 

기능으로 보면 다음과 같다.

  1.  AnyButWhitespace() : 공백 문자를 제외한 모든 문자와 일치
  2. AtLeastOnce() : 제공된 패턴을 한번 이상 일치
  3. Either : 제공된 패턴 중에서 일치하는 것들

아래 코드 의미를 파악해보면,

  1. https:// 이면서, 
  2. 공백 문자를 제외한 모든 문자와 일치한 개 한개 이상 
  3. . com이나. org가 들어가면서
  4. 그 뒤에 공백 문자를 제외한 모든 문자와 일치한 개 한개 이상 

이라는 규칙을 만족하는 것만 뽑아내려고 한다.

from pregex.classes import AnyButWhitespace
from pregex.quantifiers import AtLeastOnce
from pregex.operators import Either

text = "You can find me through GitHub https://github.com/khuyentran1401"

pre = (
    "https://"
    + AtLeastOnce(AnyButWhitespace())
    + Either(".com", ".org")
    + AtLeastOnce(AnyButWhitespace())
)

그 패턴에 대해서 보고자 하면 get_pattern을 통해 확인할 수 있다.

pre.get_pattern()
# 'https\\:\\/\\/[^\\s]+(?:\\.com|\\.org)'

결과 확인

아래 코드는 결과를 확인한 코드이다.

원하는 결과 얻기까지 정규 표현식을 아주 간단하게 사용할 수 있었다.

pre.get_matches(text)
# ['https://github.com/khuyentran1401']
pre.get_matches_and_pos(text)
# [('https://github.com/khuyentran1401', 31, 64)]

 

HTTP / HTTPS 만족하는 URL 찾기

복잡의 기준이 애매하지만, HTTPS 나 HTTP를 둘 다 찾는 것을 추가하려면 어떻게 해야 할까?

 

여기서는 Optional이라는 함수를 제공한다.

 

아래와 같이 작성해주면 HTTP 나 HTTPS 둘 중에 하나라도 만족하는 조건을 추가할 수 있다.

 

from pregex.quantifiers import Optional

text = "You can find me through GitHub http://github.com/khuyentran1401"

pre = (
    "http"
    + Optional("s")
    + "://"
    + AtLeastOnce(AnyButWhitespace())
    + Either(".com", ".org")
    + AtLeastOnce(AnyButWhitespace())
)
pre.get_matches(text)

위의  있는 코드와 비교해보면, 알 수 있듯이 어디가 바뀌었는 지를 파악하기 위해서는 코드를 집중해서 봐야 할 것 같다.

# 복잡한 URL 찾기 PATTERN 코드
pre.get_pattern()
# 'https?\\:\\/\\/[^\\s]+(?:\\.com|\\.org)[^\\s]+'
# 간단한 URL 찾기 PATTERN 코드
pre.get_pattern()
# 'https\\:\\/\\/[^\\s]+(?:\\.com|\\.org)'

스키마 없이 URL 일치

어떤 url에는 http나 https와 같은 스키마가 없을지도 모른다.

만약 나한테 이걸 regex로 따라 하면 1,2시간 고민하다가 여러 시행착오를 해보면서 찾을 것 같다.

물론 모든 정규 표현식을 이해해서 작성하는 것이 미래에 좋겠지만, 급할 때는 편한 패키지를 사용하는 것이 맘이 편하지 않을까 싶다...

 

아래 패턴을 보면, 일단 https 나 http가 Optional 하기 때문에 아래와 같은 코드로 optional_scheme를 우선 만들고, 

그다음에 기존과 동일하게 하면 된다.

 

text = "You can find me through my website mathdatasimplified.com/ or GitHub https://github.com/khuyentran1401"

at_least_one_character_except_white_space = AtLeastOnce(AnyButWhitespace())
optional_scheme = Optional("http" + Optional("s") + "://")
domain_choice = Either(".com", ".org")

pre = (
    optional_scheme
    + at_least_one_character_except_white_space
    + domain_choice
    + at_least_one_character_except_white_space
)
pre.get_matches(text)
# ['mathdatasimplified.com/', 'https://github.com/khuyentran1401']
pre.get_pattern()
# '(?:https?\\:\\/\\/)?[^\\s]+(?:\\.com|\\.org)[^\\s]+'

시간 관련 표현 찾기

  • AnyDigit() : 숫자형 변수 패턴을 찾는 함수

아래와 같이 작성하게 되면 숫자:숫자만 탐지하게 된다.

from pregex.classes import AnyDigit

text = "It is 6:00 pm now"
pre = AnyDigit() + ":" + AnyDigit()
pre.get_matches(text)
# ['6:0']

위와 같은 문제를 해결하기 위해서는 한 개 이상의 숫자를 탐지하는 것이 필요하다.

from pregex.classes import AnyDigit

text = "It is 6:00 pm now"
pre = AtLeastOnce(AnyDigit()) + ":" + AtLeastOnce(AnyDigit())
pre.get_matches(text)
# ['6:00']
pre.get_pattern()
# '[\\d]+\\:[\\d]+'

 

폰번호 찾기

폰번호가 일반적으로 어떻게 나오는 지를 확인해보자.

아래처럼 폰번호의 format은 아래와 같이 나올 것이다.

##########
###-###-####
### ### ####
###.###.####

이러한 형식에는 구두점이 있거나 숫자 사이에 아무것도 없습니다. 

AnyFrom("-", " ", ".")을 사용하여 - ,. , 또는 공백을 처리합니다.

 

pre에는 우리가 원하는 형태를 순차적으로 작성해주면 된다.

 

from pregex.classes import AnyFrom
text = "My phone number is 3452352312 or 345-235-2312 or 345 235 2312 or 345.235.2312"

punctuation = AnyFrom("-", " ", ".")
optional_punctuation = Optional(punctuation)
at_least_one_digit = AtLeastOnce(AnyDigit())

pre = (
    at_least_one_digit
    + optional_punctuation
    + at_least_one_digit
    + optional_punctuation
    + at_least_one_digit
)
pre.get_matches(text)
# ['3452352312', '345-235-2312', '345 235 2312', '345.235.2312']
pre.get_pattern()
# '[\\d]+[\\- \\.]?[\\d]+[\\- \\.]?[\\d]+'

이메일 주소 찾기

위의 그림처럼 사람이 생각하는 관점을 그대로 순차적으로 작성할 수 있다.

text = "My email is abcd@gmail.com abcd!@gmail.com"

pre = (
    AtLeastOnce(AnyButWhitespace())
    + "@"
    + AtLeastOnce(AnyButWhitespace())
    + Either(".com", ".org", ".io", ".net")
)

pre.get_matches(text)
# ['abcd@gmail.com', 'abcd!@gmail.com']

pre.get_pattern()
# '[^\\^\\-\\[\\]!"#$%&\\\'()*+,./:;<=>?@_`{|}~\\\\]+@[^\\s]+(?:\\.com|\\.org|\\.io|\\.net)'

위의 결과를 보면 좀 아쉬운 것이 있다 바로 특수문자가 포함되어 있는 주소도 나온다는 것이다.

저것을 제거하는 방법도 간단하다.

 

아직 익숙하지 않지만, operator를 사용해서 필터링할 수 있는 것 같다.

from pregex.classes import AnyButPunctuation
text = "My email is abcd@gmail.com abcd!@gmail.com"

pre = (
    AtLeastOnce(AnyButPunctuation() | AnyButWhitespace())
    + "@"
    + AtLeastOnce(AnyButWhitespace())
    + Either(".com", ".org", ".io", ".net")
)

pre.get_matches(text)

# ['abcd@gmail.com']

pre.get_pattern()

# '[^.!/_\\\\,*:#\\^|{~\\\'$=?@\\-\\s}>(+&"%);\\[`\\]<]+@[^\\s]+(?:\\.com|\\.org|\\.io|\\.net)'

 

복잡한 정규 표현식

아래 코드 조각 내에서 ". com" 또는 ". org"로 끝나는 URL과 4자리 포트 번호가 지정된 IP 주소와 일치하는 Pregex 인스턴스를 구성합니다. 또한 일치 항목이 URL인 경우 도메인 이름도 별도로 캡처됩니다.

from pregex.quantifiers import Optional, AtLeastOnce, AtLeastAtMost
from pregex.classes import AnyFrom, AnyDigit, AnyWhitespace
from pregex.groups import CapturingGroup
from pregex.tokens import Backslash
from pregex.operators import Either
from pregex.pre import Pregex

pre: Pregex = \
        Optional("http" + Optional('s') + "://") + \
        Optional("www.") + \
        Either(
            CapturingGroup(
                AtLeastOnce(~ (AnyWhitespace() | AnyFrom(":", Backslash())))
            ) +
            Either(".com", ".org"),

            3 * (AtLeastAtMost(AnyDigit(), min=1, max=3) + ".") +
            1 * AtLeastAtMost(AnyDigit(), min=1, max=3) +
            ":" + 4 * AnyDigit() 
        )

패턴이 아주 아주 복잡해진다.ㅠㅠ

'(?:https?\\:\\/\\/)?(?:www\\.)?(?:([^\\:\\\\\\s]+)(?:\\.com|\\.org)|(?:[\\d]{1,3}\\.){3}[\\d]{1,3}\\:[\\d]{4})'

정밀 분석하면 다음과 같다. 

 

http , https를 옵션으로 하고 , www.도 옵션으로 하고, 

 

공백과 : 와 백 슬래시를 포함하지 않는 그룹이면서,. com /. org인 것들을 찾는 것과

 

숫자가 최소 1개에서 최대 3개 이면서. 이 포함된  패턴이 3번 반복되면서, 숫자가 최소 1개에서 3개가 포함된 패턴 다음 : 이 있고 숫자가 4개 있는 패턴을 찾기

 

text = "text--192.168.1.1:8000--text--http://www.wikipedia.orghttps://youtube.com--text"
pre.get_matches(text)

# ['192.168.1.1:8000', 'http://www.wikipedia.org', 'https://youtube.com']

text = "text--192.168.1.1:8000--text--http://www.wikipedia.orghttps://youtube.com--text-:youtube.com"
pre.get_matches(text)
['192.168.1.1:8000',
 'http://www.wikipedia.org',
 'https://youtube.com',
 'youtube.com']

위의 결과를 보면 아이피도 잘 찾아주면서 url도 잘 찾아주는 것을 한 번에 짠 것을 볼 수 있다.

아직은 이것도 익숙하지 않지만, 다양한 것을 제공하고 활용을 어떻게 하냐에 따라 정말 잘 쓸 수 있다는 생각이 든다.

 

 

pregex 2.0.0 으로 업데이트하면, 더 많은 기능이 있긴 하다.일단 한국어 전체에 대해서 하려고 하면 아래처럼 만들면 되기는 하는데, 나중에 공식적으로 들어갔으면 좋겠다.

 

from pregex.core.classes import AnyLetter
class AnyKor(AnyLetter):

    def __init__(self) -> 'AnyKor':
        '''
        Matches any character from the Latin alphabet.
        '''
        super(AnyLetter,self).__init__('[ㄱ-ㅎ|가-힣]', is_negated=False)

3글자이면서 가와 나로 시작하는 한국어를 찾고자 한다면 아래와 같이 작성하면 된다. 

정규 표현식이 오히려 더 쉬운 것 같다. 이렇게 짜려니 생각보다 어려웠다.

pre = Either("가","나") + 2 * AnyKor()
pre = MatchAtStart(pre)
pre = MatchAtEnd(pre)
# Group( + Pregex("{2}"))
pre.get_pattern()
# '\\A(?:가|나)[|ㄱ-ㅎ가-힣]{2}\\Z'

pre.has_match("aa가가나다")
# False
pre.has_match("가가나다")
# False
pre.has_match("다나다")
# False
pre.has_match("가나다")
# True

 

 

참고

https://medium.com/towards-data-science/pregex-write-human-readable-regular-expressions-in-python-9c87d1b1335

 

PRegEx: Write Human-Readable Regular Expressions in Python

Create Complicated RegEx with Human-Readable Syntax

towardsdatascience.com

https://pregex.readthedocs.io/en/latest/index.html

 

Welcome to pregex’s documentation! — pregex 1.0.5 documentation

© Copyright 2022, Emmanouil Stoumpos. Revision 5b66f836.

pregex.readthedocs.io

 

728x90