Python RegEx ile Tarih Algılama
"Python ile Sıkıcı şeyleri Otomatikleştir" kitabından bir uygulama projesidir. Orta seviye bir Python programcısıyım ve bu sorunu olabildiğince az kodla çözmeye çalıştım. Bu kod herhangi bir yanlış tarihi dikkate almayacaktır, örneğin: 29-02-2002 seçilmeyecektir çünkü 2002 artık bir yıl değildir ve yalnızca artık yıllarda 29 Şubat vardır. Aylarca yazılan tarihleri de tespit etmek için kod eklemedim, bunu da yapabilirdim ama şimdilik işleri basit tutmak istiyorum ve ayrıca kopyalanan metinden panoya tarihleri algılamak için pyperclip modülünü kullanmadım çünkü istemiyorum Kodumu izleyerek öğrenmek isteyen yeni başlayanların kafasını karıştır. Usta programcıların kodumu gözden geçirmesini istiyorum ve eğer onların tarihleri saptamanın başka bir yolu varsa, lütfen çözümlerinizi gönderin. Ayrıca herhangi bir tavsiye ve olumlu eleştiriyi de takdir ederim, bu yüzden şu anda nerede durduğumu ve neleri iyileştirmem gerektiğini biliyorum. Teşekkürler. Kod aşağıdaki gibidir:
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)
```
Yanıtlar
Normal ifadede bazı küçük iyileştirmeler öneririm:
- Aynı ayırıcının gün ve ay arasında ve ay ile yıl arasında bir geri referansla kullanıldığından emin olun
(?P=sep), - numaralandırılmış yakalama gruplarını adlandırılmış olarak değiştirin ve varsa yakalamayan gerekli olmayan grupları oluşturun
?:. Sonuç olarak,finditervegroupdictkullanılır ve gün maçında elde edilirint(date['day']), vs. Bu kod biraz daha insan yapacaktır.
Daha da önemlisi, sana kurtulmak düşündürmektedir days, monthsve yearstamamen listeleri. Bu veriler, dateslistedeki sözlüklerde saklanabilir ve eklenmeden önce filtrelenebilir dates.
Sonuç olarak, bir döngüye ihtiyacınız olmayacak range(len(days)).
Doğrulama koşulları, netliği kaybetmeden birlikte OR'lanabilir ve bunu ayrı bir işlev yapmayı öneriyorum date_is_valid(day: int, month: int, year: int) -> bool.
Ayrıca, sadece paramètre date_detectorYazılan yapılabilir: def date_detector(text: str):.
Önerilen değişiklikleri özetlemek için:
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)
```
Bunun bir egzersizin parçası olduğunu biliyorum, ancak tarih doğrulaması için yerleşik Python yeteneklerinden yararlanabileceğiniz çok sayıda tekerleği yeniden keşfetme gibi geliyor:
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
Yıllar , aylar ve günler için 3 ayrı liste oluşturmak yerine, bu bölümlere her zaman aynı dizinden eriştiğiniz için tek bir liste oluşturabilirsiniz. Bu ayrıca
for, size bu listelerde erişmek istediğiniz bir dizin vermek yerine doğrudan değerleri veren döngüyü basitleştirir .Python, boş koleksiyonların değerlendirildiği dinamik bir dildir
False, bu nedenle bir listede herhangi bir öğe olup olmadığını kontrol etmek istediğinizde, bunu açıkça yapmak zorunda değilsinizif len(list) > 0, ancak yapabilirsinizif list:. Listedeki öğeleri yazdırmak amacıyla, bir adım daha ileri gidebilir ve boş bir listeyi yineleyerek hiçbir şey yazdırmayacağınız için koşulu tamamen atlayabilirsiniz. Snippet öncesi / sonrası:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
tüm öneri uygulandı:
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)
- bu, her iki
fordöngüyü bir araya getirerek daha da basitleştirilebilir , böylece gereksiz yere veri toplama işlemini iki kez yinelemezsiniz.