Python RegEx로 날짜 감지

Oct 09 2020

"Automate the Boring stuff with Python"책의 연습 프로젝트입니다. 저는 중급 Python 프로그래머이며 가능한 한 적은 코드 로이 문제를 해결하려고 노력했습니다. 이 코드는 잘못된 날짜를 고려하지 않습니다. 예 : 2002 년은 윤년이 아니고 윤년 만 2 월 29 일이기 때문에 2002 년 2 월 29 일은 선택되지 않습니다. 나는 또한 단어로 쓰여진 달을 가진 날짜를 감지하는 코드를 추가하지 않았으며, 나도 그렇게 할 수 있지만 지금은 간단하게 유지하고 싶고 pyperclip 모듈을 사용하여 복사 된 텍스트에서 클립 보드로 날짜를 감지하지 않았습니다. 내 코드를 보면서 배우고 싶은 초보자를 혼동하십시오. 마스터 프로그래머가 내 코드를 검토하고 날짜를 감지 할 수있는 또 다른 방법이라면 솔루션을 게시하십시오. 또한 조언과 긍정적 인 비판에 감사드립니다. 그래서 지금 제가 서있는 곳과 개선해야 할 점을 알고 있습니다. 감사. 코드는 다음과 같습니다.

    import re


    def date_detector(text):
        date_pattern = re.compile('''
        ([12][0-9]|3[0-1]|0?[1-9])             # to detect days from 1 to 31
        ([./-])                                # to detect different separations
        (1[0-2]|0?[1-9])                       # to detect number of months
        ([./-])                                # to detect different seperations
        (2?1?[0-9][0-9][0-9])                  # to detect number of years from 1000-2999 years
         ''', re.VERBOSE)

        days = []
        months = []
        years = []
        dates = []
        for date in date_pattern.findall(text):
            days.append(int(date[0]))
            months.append(int(date[2]))
            years.append(int(date[4]))

        for num in range(len(days)):

        # appending dates in a list that dont need any filtering to detect wrong dates
            if months[num] not in (2, 4, 6, 9, 11):
                dates.append([days[num], months[num], years[num]])

        # detecting those dates with months that have only 30 days
            elif days[num] < 31 and months[num] in (4, 6, 9, 11):
                dates.append([days[num], months[num], years[num]])

        # filtering leap years with Feb months that have 29 days
            elif months[num] == 2 and days[num] == 29:
                if years[num] % 4 == 0:
                    if years[num] % 100 == 0:
                        if years[num] % 400 == 0:
                            dates.append([days[num], months[num], years[num]])
                    else:
                        dates.append([days[num], months[num], years[num]])

        # appending Feb dates that have less than 29 days
            elif months[num] == 2 and days[num] < 29:
                dates.append([days[num], months[num], years[num]])

        if len(dates) > 0:
            for date in dates:
                print(date)


data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
```

답변

2 AlexanderMashin Oct 09 2020 at 14:56

정규 표현식에서 약간의 개선을 제안합니다.

  • 역 참조를 사용하여 일과 월 사이 및 월과 연도 사이에 동일한 구분 기호가 사용되는지 확인하십시오 (?P=sep).
  • 번호가 매겨진 캡처 그룹을 이름이 지정된 그룹으로 바꾸고 필요하지 않은 그룹이있는 경우 ?:. 따라서, finditergroupdict사용되며, 하루에 경기에서 획득 int(date['day'])등이 코드가 좀 더 인간 만들 것입니다.

더 중요한 것은, 당신이 없애 제안 days, months그리고 years모두 나열합니다. 이러한 데이터는 dates목록의 사전에 저장 하고에 추가하기 전에 필터링 할 수 있습니다 dates.

결과적으로 루프가 필요하지 않습니다 range(len(days)).

유효성 검사 조건은 명확성을 잃지 않고 OR로 연결할 수 있으며 별도의 기능으로 만들 것을 제안합니다 date_is_valid(day: int, month: int, year: int) -> bool.

또한 date_detector입력 할 수있는 유일한 매개 변수 : def date_detector(text: str):.

제안 된 수정 사항을 요약하려면 :

import re

def date_is_valid(day: int, month: int, year: int) -> bool:
    return (month not in (2, 4, 6, 9, 11)   # 31 days in month (Jan, Mar, May, Jul, Aug, Oct, Dec).
    or day < 31 and month in (4, 6, 9, 11)  # 30 days in month (Feb, Apr, Jun, Sep, Nov).
    or month == 2 and day == 29 and year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
                                            # February, 29th in a Gregorian leap year.
    or month == 2 and day < 29)             # February, 1st-28th.

def date_detector(text: str):
    date_pattern = re.compile('''
    (?P<day>[12][0-9]|3[0-1]|0?[1-9])   # to detect days from 1 to 31
    (?P<sep>[./-])                      # to detect different separations
    (?P<month>1[0-2]|0?[1-9])           # to detect number of months
    (?P=sep)                            # to detect different seperations
    (?P<year>2?1?[0-9][0-9][0-9])       # to detect number of years from 1000-2999 years
     ''', re.VERBOSE)

    dates = []
    for match in date_pattern.finditer(text):
        date = match.groupdict()                            # convert Match object to dictionary.
        del date['sep']                                     # we don't need the separator any more.
        date = {key: int(val) for key, val in date.items()} # apply int() to all items.
        
        if date_is_valid(date['day'], date['month'], date['year']):
            dates.append(date)

    if len(dates) > 0:
        for date in dates:
            print(date)

data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
```
2 yedpodtrzitko Oct 09 2020 at 15:33

이것이 연습의 일부라는 것을 알고 있지만 날짜 유효성 검사를 위해 내장 된 Python 기능을 활용할 수있는 많은 바퀴를 재창조하는 것처럼 느껴집니다.

from datetime import date

>>> date(2020, 2, 29) # leap year date works
datetime.date(2020, 2, 29)

>>> date(2002, 2, 29) # non-leap year will raise ValueError
ValueError: day is out of range for month

>>> date(2002, 9, 31) # 31th day will raise ValueError
ValueError: day is out of range for month
  • 대신 위해 3 개 별도의 목록을 만드는 , 이 항상 같은 인덱스에이 부분에 액세스하기 때문에, 당신은 하나의 목록을 만들 수 있습니다. 또한 for이러한 목록에서 액세스하려는 인덱스를 제공하는 대신 값을 직접 제공 하는 루프를 단순화 합니다.

  • Python은 빈 컬렉션이으로 평가되는 동적 언어 False이므로 목록에 항목이 있는지 확인하려는 경우을 통해 명시 적으로 if len(list) > 0수행 할 필요는 없지만 if list:. 목록의 항목을 인쇄하기 위해 한 단계 더 나아가 빈 목록을 반복하면 아무것도 인쇄되지 않으므로 조건을 완전히 생략 할 수 있습니다. 이전 / 이후 스 니펫 :

# before
if len(dates) > 0:
    for date in dates:
        print(date)

# after
for date in dates:
    print(date)

적용된 모든 제안 :

import re
from datetime import date

def date_detector(text):
    date_pattern = re.compile('''
    ([12][0-9]|3[0-1]|0?[1-9])             # to detect days from 1 to 31
    ([./-])                                # to detect different separations
    (1[0-2]|0?[1-9])                       # to detect number of months
    ([./-])                                # to detect different seperations
    (2?1?[0-9][0-9][0-9])                  # to detect number of years from 1000-2999 years
        ''', re.VERBOSE)

    # use only one list for storing all parts of match together
    parsed = []
    for match in date_pattern.findall(text):
        # year, month, day for easier passing to date()
        parsed.append([ int(match[4]), int(match[2]), int(match[0])] )

    valid = [] 
    for item in parsed:
        try:
            # pass list of [year, month, day] to date() and let it check its validity for us
            date(*item)
        except ValueError as e:
            pass # invalid date, dont do anything
        else:
            valid.append(item)

    for item in valid:
        print(item)


data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
  • for루프를 함께 병합하여 더 단순화 할 수 있으므로 불필요하게 데이터 수집을 두 번 반복하지 않습니다.