Détection de date avec Python RegEx

Oct 09 2020

C'est un projet pratique du livre "Automatiser les trucs ennuyeux avec Python". Je suis un programmeur Python de niveau intermédiaire et j'ai essayé de résoudre ce problème avec moins de code que possible. Ce code ne prendra aucune date erronée en considération, par exemple: 29-02-2002 ne sera pas sélectionné car 2002 n'est pas une année bissextile et seules les années bissextiles ont le 29 février. Je n'ai pas ajouté de code pour détecter également les dates avec des mois écrits avec des mots, je pourrais le faire aussi mais je veux garder les choses simples pour le moment et je n'ai pas non plus utilisé le module pyperclip pour détecter les dates du texte copié dans le presse-papiers parce que je ne veux pas confondre tout débutant qui souhaite également apprendre en regardant mon code. Je veux que les maîtres programmeurs examinent mon code et s'il existe un autre moyen de détecter les dates, veuillez publier vos solutions. Aussi j'apprécierais tout conseil et critique positive, donc je sais où je suis en ce moment et ce que je dois améliorer. Merci. Le code est le suivant:

    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)
```

Réponses

2 AlexanderMashin Oct 09 2020 at 14:56

Je suggère quelques améliorations mineures dans l'expression régulière:

  • assurez-vous que le même séparateur est utilisé entre le jour et le mois et entre le mois et l'année avec une référence arrière (?P=sep),
  • remplacez les groupes de capture numérotés par named et créez des groupes non nécessaires, s'il y en a, sans capture avec ?:. Par conséquent, finditeret groupdictsont utilisés, et le jour est obtenu à partir de la correspondance avec int(date['day']), etc. Cela rendra le code un peu plus humain.

Plus important encore , je vous suggère de se débarrasser de days, monthset des yearslistes tout à fait. Ces données peuvent être stockées dans des dictionnaires sous forme de datesliste et filtrées avant d'être ajoutées à dates.

En conséquence, vous n'aurez pas besoin d'une boucle range(len(days)).

Les conditions de validation peuvent être combinées par OU sans perdre de clarté, et je propose d'en faire une fonction distincte date_is_valid(day: int, month: int, year: int) -> bool.

En outre, le seul parametre dans date_detectorpeut être fait typé: def date_detector(text: str):.

Pour résumer les modifications suggérées:

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

Je sais que cela fait partie d'un exercice, mais cela ressemble à beaucoup de réinvention de la roue où vous pouvez tirer parti des capacités Python intégrées pour la validation de la date:

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
  • au lieu de créer 3 listes distinctes pour des années , des mois et des jours , vous ne pouvez créer qu'une seule liste, car vous accédez toujours à ces parties au même index. Cela simplifie également la forboucle qui vous donne directement les valeurs au lieu de vous donner un index auquel vous souhaitez accéder dans ces listes.

  • Python est un langage dynamique dans lequel les collections vides sont évaluées False, donc lorsque vous voulez vérifier si une liste contient des éléments, vous n'avez pas à le faire explicitement via if len(list) > 0, mais vous pouvez le faire if list:. Pour imprimer les éléments de la liste, vous pouvez aller plus loin et omettre complètement la condition car une itération dans une liste vide n'imprimera rien. Extrait avant / après:

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

# after
for date in dates:
    print(date)

toute suggestion appliquée:

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)
  • cela peut être simplifié davantage en fusionnant les deux forboucles ensemble, de sorte que vous n'effectuez pas une itération dans la collecte de données deux fois inutilement.