Datumserkennung mit Python RegEx
Es ist ein Übungsprojekt aus dem Buch "Automatisiere das langweilige Zeug mit Python". Ich bin ein Python-Programmierer der Mittelstufe und habe versucht, dieses Problem mit weniger Code als möglich zu lösen. Dieser Code berücksichtigt kein falsches Datum, z. B.: 29-02-2002 wird nicht ausgewählt, da 2002 kein Schaltjahr ist und nur Schaltjahre den 29. Februar haben. Ich habe keinen Code hinzugefügt, um auch Daten mit Monaten in Worten zu erkennen. Ich könnte das auch tun, aber ich möchte die Dinge vorerst einfach halten, und ich habe auch kein Pyperclip-Modul verwendet, um Daten aus kopiertem Text in die Zwischenablage zu erkennen, weil ich nicht möchte verwirren Sie jeden Anfänger, der auch lernen möchte, meinen Code zu sehen. Ich möchte, dass Master-Programmierer meinen Code überprüfen. Wenn dies eine andere Möglichkeit ist, Daten zu erkennen, veröffentlichen Sie bitte Ihre Lösungen. Ich würde mich auch über Ratschläge und positive Kritik freuen, damit ich weiß, wo ich gerade stehe und was ich verbessern muss. Vielen Dank. Der Code lautet wie folgt:
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)
```
Antworten
Ich schlage einige kleinere Verbesserungen im regulären Ausdruck vor:
- Stellen Sie sicher, dass zwischen Tag und Monat sowie zwischen Monat und Jahr mit einem Rückverweis dasselbe Trennzeichen verwendet
(?P=sep)wird. - Ersetzen Sie nummerierte Erfassungsgruppen durch benannte und erstellen Sie nicht benötigte Gruppen, falls vorhanden, nicht mit
?:. Folglichfinditerundgroupdictwerden verwendet, und der Tag wird aus der Übereinstimmung mitint(date['day'])usw. erhalten. Dies macht den Code etwas menschlicher.
Noch wichtiger ist , schlage ich vor, dass Sie loswerden days, monthsund yearsinsgesamt Listen. Diese Daten können in Wörterbüchern in der datesListe gespeichert und vor dem Anhängen an gefiltert werden dates.
Infolgedessen benötigen Sie keine Schleife range(len(days)).
Die Validierungsbedingungen können zusammen ODER-verknüpft werden, ohne an Klarheit zu verlieren, und ich schlage vor, sie zu einer separaten Funktion zu machen date_is_valid(day: int, month: int, year: int) -> bool.
Außerdem kann der einzige Parameter date_detectoreingegeben werden : def date_detector(text: str):.
Um die vorgeschlagenen Änderungen zusammenzufassen:
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)
```
Ich weiß, dass dies Teil einer Übung ist, aber es scheint eine Menge Rad neu zu erfinden, bei dem Sie integrierte Python-Funktionen für die Datumsüberprüfung nutzen können:
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
Anstatt drei separate Listen für Jahre , Monate und Tage zu erstellen, können Sie nur eine Liste erstellen, da Sie immer mit demselben Index auf diese Teile zugreifen. Dies vereinfacht auch die
forSchleife, in der Sie die Werte direkt erhalten, anstatt einen Index zu erhalten, auf den Sie in diesen Listen zugreifen möchten.Python ist eine dynamische Sprache, in der leere Sammlungen ausgewertet
Falsewerden. Wenn Sie also überprüfen möchten, ob eine Liste Elemente enthält, müssen Sie dies nicht explizit über tunif len(list) > 0, sondern können dies tunif list:. Um Elemente in einer Liste zu drucken, können Sie noch einen Schritt weiter gehen und die Bedingung vollständig weglassen, da beim Durchlaufen einer leeren Liste nichts gedruckt wird. Ausschnitt vorher / nachher:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
Alle Vorschläge angewendet:
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)
- Dies kann weiter vereinfacht werden, indem beide
forSchleifen zusammengeführt werden, sodass die Datenerfassung nicht zweimal unnötig durchlaufen wird.