news 2026/11/4 1:08:35

中文日期解析实战:从星期校验到日历提醒的完整工具链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中文日期解析实战:从星期校验到日历提醒的完整工具链

前两天朋友甩给我一条日程标题:2026年09月20日星期日。她说想在那天安排一场家庭聚会,可翻了好几个App,有的显示周日,有的显示周一,她差点把行程定错。我一边吐槽她该换工具,一边意识到这种困惑其实特别常见——很多人拿到一串写着年月日和星期几的中文文本,第一反应就是上网再查一遍,而不是自己动手验证。

作为一个常年和日期、定时任务打交道的开发者,我决定把“2026年09月20日星期日”当成一个完整样本,写一套从中文日期解析、星期校验、农历节气查询,到倒计时和日历提醒的小工具。整个过程里踩了不少坑,也沉淀出很多可以直接复用的经验。这篇分享就是完整的复盘,希望对那些正在写爬虫日期抽取、日历应用、提醒服务,或者单纯想搞懂“这个日期到底是不是周日”的朋友有点帮助。

1. 一个平平无奇的日期标题,为什么要专门写个工具处理它

1.1 搜索“2026年09月20日星期日”的人,背后都有哪些诉求

先说结论:这种中文日期文本,比大部分人想象中常见得多。网站标题、活动海报、爬虫抓回来的页面正文、别人随手发来的日程截图,到处都是类似格式。我细拆了一下,围绕“2026年09月20日星期日”这个标题,真实需求其实能分成四类。

第一类是最朴素的:确认这是星期几。很多人不确定某个未来日期落在周几,得反复去查日历,尤其当这个日期要用来定婚礼、订场地、约会议的时候,星期错了后面全乱。第二类是想知道还有多少天,也就是倒计时。目标日期可能是生日、纪念日、考试日、项目上线日,人天然会对“距离那一天还有多久”产生兴趣。第三类更中国化一点:查农历、节气、宜忌,看看这个日子适不适合办酒席、搬家、签合同。第四类是程序员的视角:批量解析这类文本,把“2026年09月20日星期日”转成结构化数据,存进数据库或者生成日历事件。

这些需求单独看都不难,但放在一起就变成一个完整的小工程:文本解析、日期校验、星期计算、农历转换、倒计时、日历导出、自动提醒,每一环都有细节。我决定把它们全部串成一条流水线。

1.2 把需求翻译成技术模块和输出物

动手之前,我把需求映射成了下面这组技术模块,后面所有代码都是按这个结构写的。

用户疑问对应技术模块主要依赖/方法
这是星期几?日期解析 + 星期校验Pythondatetime、蔡勒公式
是农历几月几号?公历转农历lunar_python或sxtwl
有什么节气、宜忌?农历库扩展查询农历库自带的节气、黄历接口
还有几天到?倒计时计算datetime.date+zoneinfo
怎么存进日历/自动提醒?日历事件 + 推送ICS 文件、飞书Webhook、cron

整个处理流程是这样走的:输入一行中文日期文本 → 正则抽取年月日和星期字段 → 用datetime校验合法性 → 计算星期并和文本里的“星期日”比对 → 附加农历、节气、宜忌 → 算出倒计时天数 → 导出 ICS 日历文件或触发提醒。每个环节都有可复现的代码,下面逐个说。

2. 中文日期文本解析:从字符串校验到星期比对

2.1 正则表达式搞定“2026年09月20日星期日”

解析的第一步是让程序读懂这串话。看似简单,实际写正则时你会遇到各种变体:“2026年09月20日星期日”“2026-09-20 周日”“2026年9月20号星期天”,甚至还有“二〇二六年九月二十日”这种纯中文写法。

我先给一个兼容度比较高的解析函数,核心是两组正则:一组处理阿拉伯数字加中文年月日,一组处理纯中文数字。代码里我保留了两个分支,优先级先按阿拉伯数字来。

import re from datetime import date CN_NUMS = { "零": 0, "〇": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, } def cn_num_to_int(text): num = 0 for ch in text: if ch in CN_NUMS: num = num * 10 + CN_NUMS[ch] return num if num else None def parse_chinese_date(text): year = month = day = None week_text = None # 第一优先:阿拉伯数字格式,例如 2026年09月20日星期日 m = re.search( r"(\d{4})\s*[年.\-/]\s*(\d{1,2})\s*[月.\-/]\s*(\d{1,2})" r"\s*[日号]?\s*(星期[一二三四五六日天]|周[一二三四五六日天]|礼拜[一二三四五六日天])?", text ) if m: year, month, day = int(m.group(1)), int(m.group(2)), int(m.group(3)) week_text = m.group(4) else: # 第二优先:纯中文数字,例如 二〇二六年九月二十日星期日 m2 = re.match( r"([〇零一二三四五六七八九两]{4})年" r"([〇零一二三四五六七八九两]{1,2})月" r"([〇零一二三四五六七八九两]{1,3})\s*[日号]?" r"\s*(星期[一二三四五六日天]|周[一二三四五六日天]|礼拜[一二三四五六日天])?", text ) if m2: year = cn_num_to_int(m2.group(1)) month = cn_num_to_int(m2.group(2)) day = cn_num_to_int(m2.group(3)) week_text = m2.group(4) if year is None: raise ValueError(f"无法识别的日期格式: {text}") try: d = date(year, month, day) except ValueError as e: raise ValueError(f"非法日期: {year}-{month:02d}-{day:02d}") from e return d, week_text

这个函数里我故意让正则允许“2026-09-20”这种带横线或点号的写法,也允许中间有全角空格。实测下来,网页标题里最常见的几种格式都能命中,基本够用。

2.2 合法性校验:2月30日不会出现在任何年份

很多人拿到“2026年09月20日”只会做正则提取,提取完直接组装字符串,不校验。这是一个很隐蔽的坑。

举个例子,“2026-02-30”这种文本,正则能完美匹配出 year=2026, month=2, day=30,但 2026 年 2 月根本没有 30 号。如果不做校验,后面的星期计算、倒计时计算全部会拿一个不存在的日期做运算,轻则报错,重则静默产生错误结果。

上面的代码里我用了date(year, month, day)来构造日期,Python 的datetime会在遇到非法日期时抛出ValueError,然后我把它包装成带原始输入信息的异常传出去。这样就完成了“格式可解析”和“日期真实存在”两层校验。

顺带说一句,正则匹配时我只匹配月前后各一段数字,不会出现把“2026-09-20”拆成 month=20、day=9 这种错误。日期文本处理里月份和日期顺序搞反是很常见的 bug,固定字段顺序能少掉一半的坑。

2.3 星期字段核验:“星期日”和程序算出来的周日对不对得上

标题里带了“星期日”,那程序必须回答一个问题:这个“星期日”是题目自带的说法,还是真的对应 2026-09-20 这个日期?

我会先把中文星期映射成数字:星期一对应 0,星期二对应 1,依此类推,星期日对应 6。然后用d.weekday()算出真实星期,两个值一比对就知道题目文本有没有写错。

WEEK_MAP = { "一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6, "天": 6, } def verify_week(d, week_text): if not week_text: return None # 取最后一个字,兼容“周日”“星期日”“星期天” w = WEEK_MAP[week_text[-1]] actual = d.weekday() if w != actual: return f"星期字段不匹配: 文本说{week_text},实际是星期{'一二三四五六日'[actual]}" return f"星期校验通过: {week_text}"

拿“2026年09月20日星期日”跑一遍:date(2026, 9, 20).weekday()返回 6,对应星期日,和文本一致。这也算是顺手验证了标题本身没写错。

3. 星期计算的底层原理:蔡勒公式前因后果与标准库源码

3.1 用Python标准库一行算星期,但它内部做了什么

日常开发里根本不用自己写星期算法,date.weekday()一行就出结果。比如date(2026, 9, 20).weekday()会返回 6,代表星期日;date.isoweekday()会返回 7,也是星期日。

但“会用”和“懂它”是两回事。Python 的date类型基于“投影格里高利历”,也就是说它把现行公历规则往前无限投影,从公元 1 年 1 月 1 日开始连续编号,再根据编号取模算出星期。对于 1582 年之后实际使用的日期,这套算法没有任何问题;对于 1582 年 10 月 15 日之前的日期,严格历史意义上会有偏差,不过日常开发不会碰到那个量级。

标准库最大的价值不是算法本身,而是它把所有边界都处理好了:闰年、月末、世纪年、日期格式化。所以我的建议是:只要目标平台有可靠的日期库,优先用标准库,不要自己造轮子。自己写算法,是为了理解原理、做跨语言移植,或者应付“不能用 datetime 但必须算星期”的极端环境。

3.2 蔡勒公式:手写星期计算的经典解法

如果确实要手写,蔡勒公式(Zeller's congruence)是最经典的方案,专门用于格里高利历。它的核心思路是把年月日映射到一个模 7 的数,然后映射成星期几。

公式我用的这个版本:

def zeller_weekday(year, month, day): # 1月、2月视为上一年的13月、14月 if month < 3: month += 12 year -= 1 K = year % 100 # 年份后两位 J = year // 100 # 年份前两位 # h: 0=Saturday, 1=Sunday, ..., 6=Friday h = (day + (13 * (month + 1)) // 5 + K + K // 4 + J // 4 + 5 * J) % 7 week_names = [ "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ] # 转成 Python weekday 约定:0=Monday ... 6=Sunday weekday = (h + 5) % 7 return week_names[h], weekday

验证一下“2026年09月20日星期日”:

  • 9 月不需要调整,year=2026, month=9, day=20。
  • K = 2026 % 100 = 26,J = 2026 // 100 = 20。
  • h = (20 + (13 * 10) // 5 + 26 + 26 // 4 + 20 // 4 + 5 * 20) % 7 = (20 + 26 + 26 + 6 + 5 + 100) % 7 = 183 % 7 = 1。

h=1 对应 Sunday,(h + 5) % 7 = 6,和date.weekday()结果一致。这个公式跑出来的结果可以用标准库做交叉验证,两个独立来源互相印证,基本稳了。

3.3 边界情况:1582年、1月2月、闰年百年规则

蔡勒公式最容易翻车的三个点,我依次说清楚。

第一,1月2月的特判。公式里把 1 月当上一年 13 月、2 月当上一年 14 月,原因是为了让月份参与计算时保持某种规律,否则结果会偏。写代码时这个判断必须在最前面。

第二,闰年百年规则。能被 4 整除的年份是闰年,但整百年份必须能被 400 整除才算闰年。所以 2000 年是闰年,1900 年不是闰年,2100 年也不是闰年。测试用例里一定要带一个 1900-02-29 的非法用例,很多新手在这里翻车。

第三,格里高利历的适用范围。蔡勒公式默认采用的是格里高利历规则,如果拿去算 1582 年 10 月之前的历史日期,理论上会有偏移。真碰到这种需求,别硬用公式,去找历史的儒略历算法。日常业务里 99.9% 的场景都落在现代日期范围内,不必过度担心。

4. 农历、节气与黄历信息:给日期附上“人文上下文”

4.1 为什么公历解析还不够

公历日期是客观的、线性的,但人对日期的感知往往是“文化”的。“2026年09月20日”在公历里就是普通一天,但如果要问“农历几月几号”“是不是某个节气”“宜不宜搬家”,光靠公历算不出来。

中国用户很常见的需求是:生日按农历过、长辈挑吉日要看黄历、节日要看二十四节气。所以我想让这个工具不只输出一个干巴巴的 ISO 日期,而是把农历、节气、宜忌一起带出来。这一步本身不复杂,难点在选对农历库。

4.2 两个主流Python农历库的选型对比

我用过的农历库主要有两个,各有优缺点。

lunar_python是纯 Python 实现,API 是中文风格,直接调用Solar.fromYmd()然后getLunar()就能拿到农历对象,宜忌、节气、干支都有现成方法,写业务代码非常顺手。适合做 Web 服务、脚本工具这种开发效率优先的场景。

sxtwl基于寿星天文历的数据,底层是 C++,处理速度快,数据覆盖面大,适合高并发或者要遍历几千年的场景。但它的 API 更底层,字段名偏“天文历”风格,上手成本高一点。

对比项lunar_pythonsxtwl
语言纯 PythonC++ 扩展
中文API友好一般
性能够用更优
文档中文文档较清晰需要看 README
适合场景业务脚本、Web服务高并发、大规模计算

如果只是给自己做个提醒小工具,我推荐lunar_python。安装命令:

pip install lunar-python

4.3 实际接入:公历转农历、节气判断与宜忌输出

用lunar_python把“2026-09-20”转成农历,核心代码很短:

from lunar_python import Solar solar = Solar.fromYmd(2026, 9, 20) lunar = solar.getLunar() print("干支年:", lunar.getYearInGanZhi()) print("农历月:", lunar.getMonthInChinese()) print("农历日:", lunar.getDayInChinese()) print("宜:", lunar.getDayYi()) print("忌:", lunar.getDayJi())

注意一点:不同版本的农历库,同一日期的农历输出可能差一天。这不是库写错了,而是农历的“时区边界”和“历算数据版本”在不同实现里可能有差别。所以我在这里不贴死运行结果,建议你本地跑一次,以实际输出为准,这个习惯在历法数据处理里非常重要。

节气判断时,日期没落在节气当天,接口返回空值是正常的。黄历宜忌的数据来源是传统通书,属于民俗约定,我只把它当展示数据用,不构成任何决策建议。如果要做严肃的择日参考,还是得找专门的正版历书数据源。

5. 从倒计时到日历事件:把目标日期接入自动化提醒

5.1 倒计时计算的两个时区陷阱

算“还有多少天到2026年09月20日”看起来只是一次减法,但时区问题能让你悄悄差出一天。

最直观的错误写法是这样:

from datetime import date days_left = (date(2026, 9, 20) - date.today()).days

这段代码在本地跑没问题,但部署到云服务器或云函数后,date.today()取的是服务器时区的日期。如果服务器是 UTC 时间,而你在北京时间下午 8 点之后运行,UTC 还是同一天,倒计时就会感觉“变慢了”;反过来,UTC 已经跨天而本地还没跨天时,又会“提前一天”。

正确做法是明确指定目标时区,用zoneinfo拿到本地时间,再取日期:

from datetime import date, datetime from zoneinfo import ZoneInfo today = datetime.now(ZoneInfo("Asia/Shanghai")).date() target = date(2026, 9, 20) days_left = (target - today).days print(f"距离 {target.isoformat()} 还有 {days_left} 天")

这种按天粒度的计算,核心原则是“先定时区,再取日期,最后做差”。顺序反了,结果就是错的。

5.2 生成ICS日历事件:别人可以直接导入日历

倒计时算出来只是第一步,真正方便的是把这个日期变成一个标准的日历事件文件,这样同事、家人可以直接导入 Outlook、Google Calendar 或苹果日历。

ICS 文件本质是纯文本,自己拼也不难。我写了一个很简洁的生成函数:

from datetime import date, datetime, timedelta def make_ics(event_title, start_date, end_date=None): if end_date is None: end_date = start_date + timedelta(days=1) dt = start_date.strftime("%Y%m%d") dt_end = end_date.strftime("%Y%m%d") lines = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//date-title-tool//CN//", "BEGIN:VEVENT", f"UID:{start_date.isoformat()}@date-title-tool", f"DTSTAMP:{datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')}", f"DTSTART;VALUE=DATE:{dt}", f"DTEND;VALUE=DATE:{dt_end}", f"SUMMARY:{event_title}", "END:VEVENT", "END:VCALENDAR", ] return "\r\n".join(lines) ics_content = make_ics( "家庭聚会", date(2026, 9, 20) ) with open("2026-09-20-family.ics", "w", encoding="utf-8") as f: f.write(ics_content)

注意DTEND我设置成了“次日”。全天的日历事件结束日期在 ICS 协议里必须用排他性日期写法,也就是说结束那天本身不算事件日。这个细节很多人第一次写都会搞错,导致日历上事件多显示一天。

5.3 接入飞书/邮件/定时任务

ICS 文件解决的是“存进日历”的问题,但你还得让工具在合适的时间主动提醒。我的方案是 cron 定时任务 + 消息推送。

先写一个提醒脚本,逻辑是每天检查一次,如果目标日期在 3 天以内就发一条通知:

from datetime import date, datetime from zoneinfo import ZoneInfo import requests def notify(title, text): # 替换成你自己的飞书机器人 Webhook webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/你的密钥" requests.post(webhook, json={ "msg_type": "text", "content": {"text": f"{title}\n{text}"} }) target = date(2026, 9, 20) today = datetime.now(ZoneInfo("Asia/Shanghai")).date() days_left = (target - today).days if 0 <= days_left <= 3: notify("日期提醒", f"距离 2026年09月20日 还有 {days_left} 天") elif days_left < 0: notify("日期提醒", "目标日期已过")

然后加一条 crontab,每天早上 9 点跑一次:

0 9 * * * cd /opt/date-title-tool && python remind.py >> /var/log/date-title.log 2>&1

邮件提醒同理,把requests.post换成smtplib发信就行。这里不展开全部代码,核心思路和飞书一样:定时触发、条件判断、推送结果。

6. 部署、测试与踩坑记录:我这套小工具翻过的车

6.1 容器时区导致倒计时全部延误一天

第一次把这个工具部署到 Docker 容器时,我遇到了一个非常隐蔽的 bug:所有倒计时都差了一天。

排查了很久才发现,容器默认时区是 UTC,而容器里跑的业务逻辑全是按中国时区设计的。datetime.now()在容器里拿到的是 UTC 时间,计算出来的“今天”和宿主机上的“今天”不一致,导致目标日期在晚上 8 点之后会提前触发或者延后触发。

解决办法很简单,在 Dockerfile 里显式设置时区环境变量:

ENV TZ=Asia/Shanghai

或者运行时挂载时间配置。这个教训后来被我写进了团队的部署规范里:凡是涉及“今天”逻辑的应用,启动时必须明确时区,不能依赖宿主机默认值。

6.2 农历库版本锁定:今天能用,明天依赖更新就崩

农历库的 API 变化比我想象中频繁。lunar_python在不同版本里,某些方法名和返回类型都有过调整;sxtwl也一样,安装包的小版本更新也可能带来行为差异。

我遇到过最尴尬的情况是:昨天还正常运行的脚本,今天一更新依赖,getDayYi()直接报AttributeError。后来我把所有依赖版本锁死,才彻底避免这类问题。

pip freeze > requirements.txt

部署时用pip install -r requirements.txt安装,而不是每次都装最新版。如果哪天确实要升级,先跑一遍完整测试用例再上生产。

6.3 测试用例固化日期边界问题

日期处理最容易出bug,所以我给这套工具写了一批测试用例,专门覆盖正常日期、非法日期、闰年、中文数字格式。

输入期望结果
2026年09月20日星期日合法,2026-09-20,weekday=6
2026-09-20合法,星期日
2026-02-30抛异常,非法日期
2024-02-29合法,星期四
1900-02-29抛异常,1900不是闰年
2026年9月20号周日归一化后同样是9月20日星期日

对应的 pytest 测试长这样:

import pytest from date_title import parse_chinese_date def test_parse_valid(): d, week_text = parse_chinese_date("2026年09月20日星期日") assert d.isoformat() == "2026-09-20" assert d.weekday() == 6 assert week_text == "星期日" def test_invalid_feb_30(): with pytest.raises(ValueError): parse_chinese_date("2026-02-30") def test_leap_year(): d, _ = parse_chinese_date("2024-02-29") assert d.weekday() == 3

这些用例看着简单,但每一条都曾经是我踩过的真实坑。把边界固化下来之后,后面改正则、换农历库都不怕回归。

最后再分享一点个人体会:日期这种数据,看起来是技术领域里最基础的一层,但真正把它做成产品功能时,你会发现它同时牵扯着历法、时区、文化和通信协议。如果你只是偶尔想知道“2026年09月20日”是星期几,搜索引擎就够了;但如果你要把日期当成工具的一部分,那文本解析、时区处理、边界用例这三件事,越早安排妥当越好。我写完这套小工具,最大的收获不是代码,而是终于明白了为什么日期库会成为每个语言的标准配置。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 1:06:48

STM32平衡车代码从标准库到HAL库移植实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:04:41

MQTT物联网实战:从协议原理到EMQX生产部署与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:04:30

EtherCAT背板方案:高速高精工业自动化通信的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:04:30

FreeRTOS在STM32 Blue Pill上的轻量级实时系统实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:03:00

STM32 ADC电池电压采样可靠性设计全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:02:59

STM32 HAL库重构平衡车:从寄存器操作到实时系统架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华