Deteksi Tanggal dengan Python RegEx
ini adalah proyek latihan dari buku "Automate the Boring stuff with Python". saya seorang programmer Python tingkat menengah dan saya mencoba untuk memecahkan masalah ini dengan kode yang lebih sedikit mungkin. Kode ini tidak akan mengambil tanggal yang salah sebagai pertimbangan misalnya: 29-02-2002 tidak akan dipilih karena tahun 2002 bukanlah tahun kabisat dan hanya tahun kabisat yang memiliki tanggal 29 Februari. saya tidak menambahkan kode untuk juga mendeteksi tanggal dengan bulan yang ditulis dengan kata-kata, saya dapat melakukannya juga tetapi saya ingin membuatnya sederhana untuk saat ini dan saya juga tidak menggunakan modul pyperclip untuk mendeteksi tanggal dari teks yang disalin ke clipboard karena saya tidak mau membingungkan setiap pemula yang juga ingin belajar dari menonton kode saya. Saya ingin programmer master meninjau kode saya dan jika mereka adalah cara lain yang memungkinkan untuk mendeteksi tanggal, silakan posting solusi Anda. Juga saya akan menghargai setiap saran dan kritik positif, jadi saya tahu di mana saya berdiri sekarang dan apa yang perlu saya perbaiki. Terima kasih. Kode adalah sebagai berikut:
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)
```
Jawaban
Saya menyarankan beberapa perbaikan kecil pada ekspresi reguler:
- pastikan bahwa pemisah yang sama digunakan antara hari dan bulan dan antara bulan dan tahun dengan referensi balik
(?P=sep), - ganti grup penangkapan bernomor dengan bernama, dan buat grup yang tidak diperlukan, jika ada, non-capture dengan
?:. Akibatnya,finditerdangroupdictdigunakan, dan hari diperoleh dari pertandingan denganint(date['day']), dll. Ini akan membuat kode menjadi lebih manusiawi.
Lebih penting lagi, saya menyarankan agar Anda menyingkirkan days, monthsdan yearsdaftar sama sekali. Data ini dapat disimpan dalam kamus dalam datesdaftar dan disaring sebelum ditambahkan ke dates.
Akibatnya, Anda tidak perlu mengulang range(len(days)).
Kondisi validasi dapat di-OR bersama-sama tanpa kehilangan kejelasan, dan saya mengusulkan untuk menjadikannya fungsi terpisah date_is_valid(day: int, month: int, year: int) -> bool.
Juga, satu-satunya parametre di date_detectordapat dibuat diketik: def date_detector(text: str):.
Untuk meringkas modifikasi yang disarankan:
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)
```
Saya tahu ini adalah bagian dari latihan, tetapi rasanya seperti banyak roda yang menemukan kembali di mana Anda dapat memanfaatkan kemampuan Python bawaan untuk validasi tanggal:
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
daripada membuat 3 daftar terpisah untuk tahun , bulan dan hari , Anda hanya dapat membuat satu daftar, karena Anda selalu mengakses bagian ini di indeks yang sama. Itu juga menyederhanakan
forperulangan yang memberi Anda nilai secara langsung daripada memberi Anda indeks yang ingin Anda akses dalam daftar ini.Python adalah bahasa dinamis di mana koleksi kosong dievaluasi
False, jadi ketika Anda ingin memeriksa apakah suatu daftar memiliki item, Anda tidak harus melakukannya secara eksplisitif len(list) > 0, tetapi Anda bisa melakukannyaif list:. Untuk tujuan mencetak item dalam daftar, Anda dapat mengambil satu langkah lebih jauh dan menghilangkan kondisi sepenuhnya karena iterasi melalui daftar kosong tidak akan mencetak apa pun. Cuplikan sebelum / sesudah:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
semua saran diterapkan:
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)
- ini dapat disederhanakan lebih lanjut dengan menggabungkan kedua
forloop bersama-sama, sehingga Anda tidak mengulang pengumpulan data dua kali tanpa perlu.