การตรวจจับวันที่ด้วย Python RegEx
เป็นโครงการฝึกปฏิบัติจากหนังสือ "Automate the Boring stuff with Python" ฉันเป็นโปรแกรมเมอร์ Python ระดับกลางและฉันพยายามแก้ปัญหานี้ด้วยรหัสน้อยที่สุด รหัสนี้จะไม่นำวันที่ผิดพลาดมาพิจารณาเช่นวันที่ 29-02-2002 จะไม่ถูกเลือกเนื่องจากปี 2545 ไม่ใช่ปีอธิกสุรทินและมีเพียงปีอธิกสุรทินเท่านั้นที่มีวันที่ 29 กุมภาพันธ์ ฉันไม่ได้เพิ่มรหัสเพื่อตรวจจับวันที่ที่มีเดือนที่เขียนเป็นคำฉันสามารถทำได้เช่นกัน แต่ตอนนี้ฉันต้องการให้ทุกอย่างง่ายขึ้นและฉันยังไม่ได้ใช้โมดูล pyperclip เพื่อตรวจจับวันที่จากข้อความที่คัดลอกไปยังคลิปบอร์ดเพราะฉันไม่ต้องการ สร้างความสับสนให้กับผู้เริ่มต้นที่ต้องการเรียนรู้จากการดูรหัสของฉัน ฉันต้องการให้โปรแกรมเมอร์หลักตรวจสอบรหัสของฉันและหากพวกเขาเป็นอีกวิธีหนึ่งที่เป็นไปได้ในการตรวจจับวันที่โปรดโพสต์วิธีแก้ปัญหาของคุณ นอกจากนี้ฉันจะขอบคุณคำแนะนำและคำวิจารณ์ในเชิงบวกดังนั้นฉันรู้ว่าตอนนี้ฉันยืนอยู่ที่ไหนและฉันต้องปรับปรุงอะไร ขอบคุณ. รหัสมีดังนี้:
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)
```
คำตอบ
ฉันขอแนะนำการปรับปรุงเล็กน้อยในนิพจน์ทั่วไป:
- ให้แน่ใจว่าตัวคั่นเดียวกันจะใช้ระหว่างวันเดือนและระหว่างเดือนและปีที่มี backreference
(?P=sep), - แทนที่กลุ่มจับหมายเลขที่มีชื่อและทำให้กลุ่มที่ไม่จำเป็นถ้ามี wer ใด ๆ
?:ที่ไม่ใช่การจับกับ ดังนั้นfinditerและgroupdictใช้และวันที่ได้รับจากการจับคู่กับint(date['day'])ฯลฯ สิ่งนี้จะทำให้โค้ดมีความเป็นมนุษย์มากขึ้น
ที่สำคัญผมขอแนะนำว่าคุณได้รับการกำจัดของdays, monthsและyearsรายการทั้งหมด ข้อมูลเหล่านี้จะถูกเก็บไว้ในพจนานุกรมในรายการและกรองก่อนที่จะผนวกกับdatesdates
range(len(days))เป็นผลให้คุณจะไม่ต้องห่วงมากกว่า
เงื่อนไขการตรวจสอบสามารถ OR'ed date_is_valid(day: int, month: int, year: int) -> boolร่วมกันโดยไม่สูญเสียความคมชัดและผมเสนอจะทำให้มันเป็นฟังก์ชั่นที่แยกต่างหาก
นอกจากนี้เพียง parametre ในสามารถทำพิมพ์:date_detectordef date_detector(text: str):
ในการสรุปการแก้ไขที่แนะนำ:
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)
```
ฉันรู้ว่านี่เป็นส่วนหนึ่งของแบบฝึกหัด แต่รู้สึกเหมือนมีการคิดค้นล้อใหม่มากมายที่คุณสามารถใช้ประโยชน์จากความสามารถของ Python ในตัวสำหรับการตรวจสอบวันที่:
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
แทนการสร้าง 3 รายการแยกต่างหากสำหรับปี , เดือนและวันที่คุณสามารถสร้างเพียงหนึ่งรายการเนื่องจากคุณเสมอเข้าถึงชิ้นส่วนเหล่านี้ที่ดัชนีเดียวกัน นอกจากนี้ยังช่วยลดความซับซ้อนของการ
forวนซ้ำซึ่งให้ค่าโดยตรงแทนการให้ดัชนีที่คุณต้องการเข้าถึงในรายการเหล่านี้งูหลามเป็นภาษาแบบไดนามิกที่คอลเลกชันที่ว่างเปล่าจะมีการประเมินการ
Falseดังนั้นเมื่อคุณต้องการตรวจสอบว่ารายการมีรายการใดที่คุณไม่ต้องทำมันอย่างชัดเจนผ่านแต่ที่คุณสามารถทำได้if len(list) > 0if list:สำหรับวัตถุประสงค์ในการพิมพ์รายการในรายการคุณสามารถดำเนินการต่อไปอีกขั้นหนึ่งและละเว้นเงื่อนไขโดยสิ้นเชิงเนื่องจากการวนซ้ำผ่านรายการว่างโดยไม่ต้องพิมพ์อะไรเลย ตัวอย่างข้อมูลก่อน / หลัง:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
ใช้คำแนะนำทั้งหมด:
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)
- สิ่งนี้สามารถทำให้ง่ายขึ้นโดยการรวม
forลูปทั้งสองเข้าด้วยกันดังนั้นคุณจึงไม่ต้องทำซ้ำผ่านการรวบรวมข้อมูลสองครั้งโดยไม่จำเป็น