PythonRegExによる日付検出

Oct 09 2020

「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)。
  • 番号付きのキャプチャグループを名前付きに置き換え、不要なグループがある場合は、を使用してキャプチャしません?:。したがって、finditerとgroupdictが使用され、日は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つの個別のリストを作成する代わりに、常に同じインデックスでこれらの部分にアクセスするため、作成できるリストは1つだけです。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、両方のループをマージすることでさらに簡略化できるため、データの収集を不必要に2回繰り返す必要はありません。