Rilevamento della data con Python RegEx
è un progetto pratico tratto dal libro "Automate the Boring stuff with Python". sono un programmatore Python di livello intermedio e ho cercato di risolvere questo problema con meno codice possibile. Questo codice non prenderà in considerazione alcuna data errata, ad esempio: 29-02-2002 non verrà selezionato perché il 2002 non è un anno bisestile e solo gli anni bisestili hanno il 29 febbraio. non ho aggiunto codice per rilevare anche le date con mesi scritti in parole, potrei farlo anche io ma voglio mantenere le cose semplici per ora e inoltre non ho usato il modulo pyperclip per rilevare le date dal testo copiato negli appunti perché non voglio confondere qualsiasi principiante che vuole anche imparare guardando il mio codice. Voglio che i programmatori esperti rivedano il mio codice e se il loro è un altro modo possibile per rilevare le date, per favore pubblica le tue soluzioni. Inoltre apprezzerei qualsiasi consiglio e critica positiva, quindi so dove mi trovo in questo momento e cosa devo migliorare. Grazie. Il codice è il seguente:
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)
```
Risposte
Suggerisco alcuni piccoli miglioramenti nell'espressione regolare:
- assicurarsi che lo stesso separatore sia utilizzato tra giorno e mese e tra mese e anno con un backreference
(?P=sep), - sostituire i gruppi di cattura numerati con nomi e creare gruppi non necessari, se ce ne fossero, non di cattura con
?:. Di conseguenza,finditeregroupdictvengono utilizzati, e il giorno è ottenuto dal match conint(date['day']), ecc Questo renderà il codice di un po 'più umano.
Ancora più importante, vi suggerisco di sbarazzarsi di days, monthse yearsle liste del tutto. Questi dati possono essere memorizzati nei dizionari datesnell'elenco e filtrati prima di essere aggiunti a dates.
Di conseguenza, non avrai bisogno di un loop over range(len(days)).
Le condizioni di convalida possono essere combinate con OR senza perdere chiarezza e propongo di renderle una funzione separata date_is_valid(day: int, month: int, year: int) -> bool.
Inoltre, l'unico parametre in date_detectorpuò essere effettuato digitato: def date_detector(text: str):.
Per riassumere le modifiche suggerite:
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)
```
So che questo fa parte di un esercizio, ma sembra che si reinventino molte ruote in cui è possibile sfruttare le funzionalità di Python integrate per la convalida della data:
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
invece di creare 3 elenchi separati per anni , mesi e giorni , puoi creare un solo elenco, poiché accedi sempre a queste parti allo stesso indice. Ciò semplifica anche il
forciclo che ti dà i valori direttamente invece di darti un indice a cui vuoi accedere in questi elenchi.Python è un linguaggio dinamico in cui vengono valutate le raccolte vuote
False, quindi quando vuoi controllare se un elenco ha elementi, non devi farlo esplicitamente tramiteif len(list) > 0, ma puoi farloif list:. Allo scopo di stampare gli elementi nell'elenco, puoi fare un ulteriore passo avanti e omettere completamente la condizione poiché l'iterazione di un elenco vuoto non stampa nulla. Snippet prima / dopo:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
tutti i suggerimenti applicati:
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)
- questo può essere ulteriormente semplificato unendo entrambi i
forloop insieme, in modo da non iterare la raccolta dei dati due volte inutilmente.