☰
Python实例教程之检索输出月份日历表
2026/10/16 11:17:00 网站建设 项目流程

前言


题面里的「检索输出月份日历表」,重点在输出:给定「年和月」,把这个月的日历排成表。标准库的calendar模块就是干这个的,从最简的「打印一个月的文本日历」到「拿到结构化的二维列表自己渲染」,它提供了好几层接口。


先纠正一个说法:「检索」这个词在这里有点多余。calendar模块并不需要你先「检索」出什么再输出——它直接接受年和月两个整数,calendar.month(2026, 10)就返回排好版的字符串,calendar.monthcalendar(2026, 10)就返回二维数字矩阵。整个模块的输入永远是整数,不需要日期对象。


常见的误解有两个:以为必须先用datetime拼出日期才能查日历(其实两者独立,calendar只吃整数);以为monthcalendar返回的矩阵里 0 是「星期天」(其实 0 表示不属于本月的占位)。本文把calendar的接口按层次讲一遍,最后给一个能直接跑的完整实例。


一、三层接口概览


calendar模块的接口可以分成三层,按「还原度」从低到高排列:




层次代表接口产出形式适合场景



数值层monthrange、weekday、isleap整数或元组自己算逻辑

结构层monthcalendar、Calendar.monthdays2calendar嵌套列表自己渲染

文本层month、TextCalendar.formatmonth多行字符串直接打印

标记层HTMLCalendar.formatmonthHTML 字符串网页输出



选哪一层取决于你要不要把结果二次加工。只想在终端看一眼,用文本层一行搞定;要往表格组件或数据库里塞,用结构层;只想问「这个月一号是星期几」,用数值层。


二、数值层、结构层与文本层


数值层:几个纯函数


# 适用于 Python 3.8+
import calendar

print(calendar.isleap(2024)) # True
print(calendar.isleap(2026)) # False
print(calendar.leapdays(2000, 2027)) # 2000 到 2026 之间的闰年数
print(calendar.weekday(2026, 10, 7)) # 0 到 6,0 是星期一
print(calendar.monthrange(2026, 10)) # (weekday_of_first_day, days_in_month)

逐个说明:



  • calendar.isleap(year)返回布尔值。

  • calendar.leapdays(y1, y2)返回区间内闰年个数,注意区间是左闭右开:leapdays(2000, 2027)统计的是 2000 到 2026。

  • calendar.weekday(year, month, day)返回 0 到 6 的整数,0 表示星期一。这一点和datetime.date.weekday()一致,但和isoweekday()的 1 到 7 不同,混用会差一天。

  • calendar.monthrange(year, month)返回一个二元组,第一项是该月 1 号是星期几(同样 0 是周一),第二项是该月天数。


monthrange一下子解决两个问题:这个月有多少天、从周几开始排。要自己画日历表,它是最省事的入口。用它可以快速回答「这个月有几个周末」这类问题:


# 适用于 Python 3.8+
import calendar

def weekend_count(year, month):
"""统计该月周六与周日的总天数。"""
first_wd, days = calendar.monthrange(year, month)
return sum(1 for d in range(days) if (first_wd + d) % 7 in (5, 6))

print(weekend_count(2026, 10))

这里的推导是:(first_wd + d) % 7得到第d天(从 0 开始)的星期编号,5 和 6 分别是周六和周日。


结构层:monthcalendar 与 Calendar 类


calendar.monthcalendar(year, month)返回一个按周分行的二维列表,每个内层列表有 7 个整数,不属于本月的日期用 0 占位:


# 适用于 Python 3.8+
import calendar

for week in calendar.monthcalendar(2026, 10):
print(week)

得到的是一个 5 行或 6 行、每行 7 个数字的矩阵。第一行的前导 0 表示该月 1 号之前的日子,最后一行的尾部 0 表示月末之后的日子。一个星期可能整行都是 0 吗?不会——因为任何一个月至少 28 天,恰好 4 整周,monthcalendar生成的每一行至少包含本月的一天。


calendar.Calendar类提供更细粒度的迭代器:




方法返回备注



itermonthdates(year, month)迭代datetime.date对象会补齐前后月的日期

itermonthdays(year, month)迭代日号整数,非本月为 0与monthcalendar同源

itermonthdays2(year, month)迭代(日号, 星期)元组日号同样可为 0

itermonthdays3(year, month)迭代(年, 月, 日)三元组日号为 0 时年月也是 0

monthdatescalendar(year, month)二维列表,元素是date对象补齐整周

monthdays2calendar(year, month)二维列表,元素是(日号, 星期)非本月日号为 0

monthdayscalendar(year, month)二维列表,元素是日号整数非本月为 0



要「带前后月补齐的完整日历」,用monthdatescalendar;要「只关心本月的数字矩阵」,用monthcalendar。两者的差别就在补齐策略上。


# 适用于 Python 3.8+
import calendar

cal = calendar.Calendar()
for week in cal.monthdays2calendar(2026, 10):
print(week)

文本层与标记层


# 适用于 Python 3.8+
import calendar

print(calendar.month(2026, 10)) # 一个月的文本日历
print(calendar.calendar(2026)) # 一整年的文本日历
print(calendar.weekheader(3)) # 形如 Mon Tue ... 的表头

calendar.month(theyear, themonth, w=0, l=0)返回字符串,w是日期列宽,l是每周的行数。calendar.calendar(theyear, w=2, l=1, c=6, m=3)返回整年字符串,其中c是每月之间的横向间距、m是每行放几个月。要注意整年文本日历的排版假设半年跨度的月份顺序,看输出前先在心里定好预期的列数。


对应的打印版本是:


# 适用于 Python 3.8+
import calendar

calendar.prmonth(2026, 10) # 打印一个月,等价于 print(calendar.month(2026, 10))
calendar.prcal(2026) # 打印一整年

TextCalendar类把这些拆得更细,formatmonth、formatyear、formatweek、formatday、formatweekheader各自返回字符串,prmonth、pryear负责打印。


HTMLCalendar是同一个思路的网页版:formatmonth(theyear, themonth, withyear=True)返回 HTML 表格字符串,formatyear返回整年,formatyearpage连完整页面一起给。它还带一组 CSS 类名属性(如cssclass_month、cssclass_noday、cssclass_year),可以继承后覆盖,改样式不需要碰生成的标记。


三、一周从哪天开始:firstweekday


模块级的calendar.setfirstweekday(firstweekday)决定每周从星期几开始,calendar.firstweekday()读取当前设置。参数取 0 到 6,对应MONDAY到SUNDAY这几个常量。


# 适用于 Python 3.8+
import calendar

print(calendar.MONDAY, calendar.SUNDAY) # 0 6
print(calendar.firstweekday()) # 默认 0

calendar.setfirstweekday(calendar.SUNDAY)
print(calendar.firstweekday()) # 6

calendar.setfirstweekday(calendar.MONDAY) # 记得改回来

这个设置是全局的:模块级函数(month、monthcalendar、weekday的排版)和默认构造的Calendar实例都会受影响。所以要写「一个用周日起始、另一个用周一起始」的两套输出,要么用完立刻改回去,要么各建自己的实例:


# 适用于 Python 3.8+
import calendar

sunday_first = calendar.TextCalendar(calendar.SUNDAY)
monday_first = calendar.TextCalendar(calendar.MONDAY)

print(sunday_first.formatmonth(2026, 10))
print(monday_first.formatmonth(2026, 10))

从 Python 3.12 起,calendar模块引入了Day和Month两个枚举,成员以MONDAY到SUNDAY、JANUARY到DECEMBER的名字导出到模块作用域。老代码里写整数 0 到 6 仍然可用(枚举成员本身就是整数),写常量名可读性更好。


另外两个常被用到的序列:




序列内容索引约定



calendar.day_name星期全名索引 0 是星期一

calendar.day_abbr星期缩写索引 0 是星期一

calendar.month_name月份全名索引 1 是 1 月,索引 0 是空字符串

calendar.month_abbr月份缩写同上,长度 13



month_name长度是 13 而不是 12,这不是 bug:它遵循「1 月是编号 1」的惯例,month_name[0]留空,这样month_name[dt.month]能直接取到正确的名字。反过来说,day_name是从 0 开始的,day_name[0]是 Monday。一个从 0 开始、一个从 1 开始,这是最容易踩的一处。


四、完整实例:把月历排成对齐的表格


下面这段代码把前面几层组合起来,输出一个对齐美观的单月表,并且支持指定「一周从哪天开始」。它只依赖标准库,Python 3.8 及以上可运行。


# 适用于 Python 3.8+
import calendar


def render_month(year, month, first_weekday=calendar.MONDAY):
"""把某个月的日历渲染成对齐的文本表格。"""
cal = calendar.TextCalendar(first_weekday)

# 表头:7 个宽度为 4 的列
header_days = [cal.formatweekday(i, 4) for i in cal.iterweekdays()]
sep = "+" + "+".join("-" * 6 for _ in range(7)) + "+"
lines = [sep, "|" + "|".join(f" {h} " for h in header_days) + "|", sep]

for week in cal.monthdayscalendar(year, month):
cells = [(" " if d == 0 else f"{d:2d}").center(4) for d in week]
lines.append("|" + "|".join(f" {c} " for c in cells) + "|")

lines.append(sep)
title = f"{calendar.month_name[month]} {year}"
lines.insert(0, title.center(len(sep)))
return "\n".join(lines)


def main():
for month in range(1, 13):
first_weekday, days = calendar.monthrange(2026, month)
print(f"{calendar.month_abbr[month]}: 1 号是周{first_weekday + 1} 共 {days} 天")
print()
print(render_month(2026, 10))


if __name__ == "__main__":
main()

几个需要逐行确认的点:


第一,cal.iterweekdays()按当前的firstweekday顺序产出星期编号,所以表头顺序会自动跟着走,不需要自己判断「今天是周几到周几」。formatweekday(i, width)取第i个星期名并指定列宽。


第二,cal.monthdayscalendar(year, month)里的 0 表示非本月日期,这里渲染成两个空格,所以矩阵的边框始终保持规整。如果直接打印0,表格会出现一排「0」而不是留白。


第三,calendar.month_name[month]用 1 到 12 直接索引,正好对上month_name长度 13 的设计;calendar.month_abbr[month]同理。


第四,f"{first_weekday + 1}"是把 0 到 6 的编号转成人话的「周一到周日」,这里只是演示;真实代码里更稳妥的写法是用calendar.day_name[first_weekday]直接取名字。


第五,str.center(width)在宽度小于字符串长度时不会截断,只是原样返回。上面的列宽都是 4,而数字最宽 2 位、表头缩写最多 3 位,不会溢出。


常见坑点


1. 把monthcalendar里的 0 当月号


❌ 遍历calendar.monthcalendar(2026, 10)的每个数字,直接拿 0 去拼日期字符串,得到一堆「0 号」。 ✅ 记住 0 是不属于本月的占位符,渲染前先判断if d == 0转成空白。


2.weekday的基准搞错


❌ 把calendar.weekday(2026, 10, 7)的结果当成 1 到 7,或者与datetime.date.isoweekday()混用。 ✅calendar.weekday和datetime.date.weekday()都是 0 到 6(周一为 0);isoweekday()是 1 到 7。


3.month_name用 0 索引


❌ 写calendar.month_name[0]想取一月,拿到的是空字符串。 ✅ 月份从 1 开始索引:calendar.month_name[1]是一月;星期用day_name[0]取星期一。


4. 忘记setfirstweekday是全局设置


❌ 在一个函数里setfirstweekday(SUNDAY)之后没改回来,导致后续所有输出都从周日开始。 ✅ 需要不同起始日就用各自的TextCalendar/Calendar实例,不要改全局状态。


5. 以为leapdays是闭区间


❌calendar.leapdays(2000, 2024)以为会把 2024 也算进去。 ✅ 区间是左闭右开,要包含终点就写leapdays(2000, 2025)。


6. 用calendar.month的返回值当数据源


❌ 想从calendar.month(2026, 10)返回的字符串里正则抠出日期,做二次加工。 ✅ 要数据就用结构层的monthcalendar或monthdatescalendar;month只负责排版展示。


7. 在机器可读输出里用 locale 相关的名字


❌ 把calendar.day_name的结果写进数据库字段或文件名,换 locale 后解析不了。 ✅ 机器可读一律存数字(星期编号、日号)或 ISO 格式;day_name/month_name只用于展示。


8. 沿用 Python 2 的写法


❌ 用print calendar.month(2026, 10)这种语句形式,或者依赖 Python 2 里calendar的旧行为。Python 2.7 已于2020 年 1 月 1 日停止维护。 ✅ 用print(calendar.month(2026, 10))。


总结




需求用哪个接口返回形式



这个月天数 / 1 号是周几calendar.monthrange(year, month)二元组

某天是周几calendar.weekday(year, month, day)0 到 6

是否闰年calendar.isleap(year)布尔值

数字矩阵(0 占位)calendar.monthcalendar(year, month)二维列表

带前后月补齐的日期Calendar.monthdatescalendar二维date列表

直接打印一个月calendar.month/prmonth字符串 / 屏幕输出

网页日历HTMLCalendar.formatmonthHTML 字符串

改一周起始日setfirstweekday或建实例时传参全局 / 局部



calendar模块的设计哲学是「输入永远是整数」。它不需要datetime对象,也不需要时间戳,只吃年和月。所以用它的时候,问题往往不在 API 难记,而在两个约定上:数字矩阵里的 0 表示本月之外,以及星期从 0 开始、月份从 1 开始。把这两条记牢,剩下的就是排版功夫了。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询