Detecção de data com Python RegEx
é um projeto prático do livro "Automate the Boring stuff with Python". Sou um programador Python de nível intermediário e tentei resolver esse problema com o mínimo de código possível. Este código não levará nenhuma data errada em consideração, por exemplo: 29-02-2002 não será selecionado porque 2002 não é um ano bissexto e apenas os anos bissextos têm 29 de fev. Eu não adicionei código para detectar datas com meses escritos em palavras, eu poderia fazer isso também, mas quero manter as coisas simples por agora e também não usei o módulo pyperclip para detectar datas do texto copiado para a área de transferência porque não quero confundir qualquer iniciante que também queira aprender observando meu código. Quero que programadores mestres revisem meu código e se houver outra maneira possível de detectar datas, por favor, poste suas soluções. Também gostaria de receber qualquer conselho e crítica positiva, então eu sei onde estou agora e o que preciso melhorar. Obrigado. O código é o seguinte:
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)
```
Respostas
Sugiro algumas pequenas melhorias na expressão regular:
- certifique-se de que o mesmo separador seja usado entre dia e mês e entre mês e ano com uma referência anterior
(?P=sep), - substitua os grupos de captura numerados por nomeados e crie grupos desnecessários, se houver algum, não capturando com
?:. Consequentemente,finditeregroupdictsão usados, e o dia é obtido a partir da correspondência comint(date['day']), etc. Isso tornará o código um pouco mais humano.
Mais importante, eu sugiro que você se livrar days, monthse yearslistas completamente. Esses dados podem ser armazenados em dicionários na dateslista e filtrados antes de anexar a dates.
Como consequência, você não precisará de um loop over range(len(days)).
As condições de validação podem ser combinadas com OR sem perder a clareza e proponho torná-las uma função separada date_is_valid(day: int, month: int, year: int) -> bool.
Além disso, a única paramètre no date_detectorpode ser feita digitado: def date_detector(text: str):.
Para resumir as modificações sugeridas:
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)
```
Eu sei que isso é parte de um exercício, mas parece uma grande reinvenção, onde você pode aproveitar os recursos integrados do Python para validação de 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
em vez de criar 3 listas separadas para anos , meses e dias , você pode criar apenas uma lista, já que você sempre acessa essas partes no mesmo índice. Isso também simplifica o
forloop que fornece os valores diretamente em vez de fornecer um índice que você deseja acessar nessas listas.Python é uma linguagem dinâmica em que coleções vazias são avaliadas
False, então quando você quiser verificar se uma lista possui algum item, você não precisa fazer isso explicitamente viaif len(list) > 0, mas pode fazerif list:. Com o propósito de imprimir itens na lista, você pode dar um passo adiante e omitir a condição completamente, já que iterar por meio de uma lista vazia não imprimirá nada. Snippet antes / depois:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
todas as sugestões aplicadas:
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)
- isso pode ser simplificado ainda mais pela fusão de ambos os
forloops, para que você não itere na coleta de dados duas vezes desnecessariamente.