Phát hiện ngày với Python RegEx
nó là một dự án thực hành từ cuốn sách "Tự động hóa công việc nhàm chán với Python". tôi là một lập trình viên Python trình độ trung cấp và tôi đã cố gắng giải quyết vấn đề này với càng ít mã càng tốt. Mã này sẽ không xem xét sai ngày nào, ví dụ: 29-02-2002 sẽ không được chọn vì năm 2002 không phải là năm nhuận và chỉ những năm nhuận mới có ngày 29 tháng feb. Tôi đã không thêm mã để phát hiện ngày cũng như tháng được viết bằng chữ, tôi cũng có thể làm điều đó nhưng tôi muốn giữ mọi thứ đơn giản ngay bây giờ và tôi cũng không sử dụng mô-đun pyperclip để phát hiện ngày từ văn bản được sao chép vào khay nhớ tạm vì tôi không muốn làm bối rối bất kỳ người mới bắt đầu nào cũng muốn học hỏi từ việc xem mã của tôi. Tôi muốn các lập trình viên bậc thầy xem lại mã của tôi và nếu họ là một cách khác có thể để phát hiện ngày tháng thì vui lòng đăng các giải pháp của bạn. Ngoài ra, tôi sẽ đánh giá cao bất kỳ lời khuyên và lời phê bình tích cực nào, vì vậy tôi biết mình đang đứng ở đâu ngay bây giờ và những gì tôi cần cải thiện. Cảm ơn. Mã như sau:
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)
```
Trả lời
Tôi đề xuất một số cải tiến nhỏ trong biểu thức chính quy:
- đảm bảo rằng cùng một dấu phân tách được sử dụng giữa ngày và tháng và giữa tháng và năm với một tham chiếu ngược
(?P=sep), - thay thế các nhóm chụp được đánh số bằng các nhóm được đặt tên và tạo các nhóm không cần thiết, nếu có, không bắt bằng
?:. Do đó,finditervàgroupdictđược sử dụng, và ngày thu được từ trận đấu vớiint(date['day']), v.v. Điều này sẽ làm cho mã có phần giống con người hơn.
Quan trọng hơn, tôi đề nghị bạn nên thoát khỏi days, monthsvà yearsdanh sách hoàn toàn. Những dữ liệu này có thể được lưu trữ trong từ điển trong datesdanh sách và được lọc trước khi thêm vào dates.
Do đó, bạn sẽ không cần lặp lại range(len(days)).
Các điều kiện xác thực có thể được HOẶC cùng nhau mà không làm mất đi sự rõ ràng và tôi đề xuất đặt nó thành một chức năng riêng biệt date_is_valid(day: int, month: int, year: int) -> bool.
Ngoài ra, các chỉ parametre trong date_detectorcó thể được thực hiện gõ: def date_detector(text: str):.
Để tổng hợp các sửa đổi được đề xuất:
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)
```
Tôi biết đây là một phần của bài tập, nhưng có vẻ như rất nhiều bánh xe phải phát minh lại, nơi bạn có thể tận dụng các khả năng của Python nội tại để xác thực ngày:
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
thay vì tạo 3 danh sách riêng biệt cho năm , tháng và ngày , bạn chỉ có thể tạo một danh sách, vì bạn luôn truy cập các phần này ở cùng một chỉ mục. Điều đó cũng đơn giản hóa
forvòng lặp cung cấp cho bạn các giá trị trực tiếp thay vì cung cấp cho bạn một chỉ mục mà bạn muốn truy cập trong các danh sách này.Python là một ngôn ngữ động, nơi các tập hợp trống được đánh giá
False, vì vậy khi bạn muốn kiểm tra xem danh sách có bất kỳ mục nào hay không, bạn không cần phải làm điều đó một cách rõ ràng thông quaif len(list) > 0, nhưng bạn có thể làmif list:. Đối với mục đích in các mục trong danh sách, bạn có thể thực hiện thêm một bước nữa và bỏ qua hoàn toàn điều kiện khi lặp qua danh sách trống sẽ không in bất kỳ thứ gì. Đoạn mã trước / sau:
# before
if len(dates) > 0:
for date in dates:
print(date)
# after
for date in dates:
print(date)
tất cả đề xuất được áp dụng:
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)
- điều này có thể được đơn giản hóa hơn nữa bằng cách hợp nhất cả hai
forvòng lại với nhau, vì vậy bạn không cần lặp lại quá trình thu thập dữ liệu hai lần.