
前言题面里的「检索输出月份日历表」重点在输出给定「年和月」把这个月的日历排成表。标准库的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.8import calendarprint(calendar.isleap(2024)) # Trueprint(calendar.isleap(2026)) # Falseprint(calendar.leapdays(2000, 2027)) # 2000 到 2026 之间的闰年数print(calendar.weekday(2026, 10, 7)) # 0 到 60 是星期一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.8import calendardef 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.8import calendarfor 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)迭代(日号, 星期)元组日号同样可为 0itermonthdays3(year, month)迭代(年, 月, 日)三元组日号为 0 时年月也是 0monthdatescalendar(year, month)二维列表元素是date对象补齐整周monthdays2calendar(year, month)二维列表元素是(日号, 星期)非本月日号为 0monthdayscalendar(year, month)二维列表元素是日号整数非本月为 0要「带前后月补齐的完整日历」用monthdatescalendar要「只关心本月的数字矩阵」用monthcalendar。两者的差别就在补齐策略上。# 适用于 Python 3.8import calendarcal calendar.Calendar()for week in cal.monthdays2calendar(2026, 10):print(week)文本层与标记层# 适用于 Python 3.8import calendarprint(calendar.month(2026, 10)) # 一个月的文本日历print(calendar.calendar(2026)) # 一整年的文本日历print(calendar.weekheader(3)) # 形如 Mon Tue ... 的表头calendar.month(theyear, themonth, w0, l0)返回字符串w是日期列宽l是每周的行数。calendar.calendar(theyear, w2, l1, c6, m3)返回整年字符串其中c是每月之间的横向间距、m是每行放几个月。要注意整年文本日历的排版假设半年跨度的月份顺序看输出前先在心里定好预期的列数。对应的打印版本是# 适用于 Python 3.8import calendarcalendar.prmonth(2026, 10) # 打印一个月等价于 print(calendar.month(2026, 10))calendar.prcal(2026) # 打印一整年TextCalendar类把这些拆得更细formatmonth、formatyear、formatweek、formatday、formatweekheader各自返回字符串prmonth、pryear负责打印。HTMLCalendar是同一个思路的网页版formatmonth(theyear, themonth, withyearTrue)返回 HTML 表格字符串formatyear返回整年formatyearpage连完整页面一起给。它还带一组 CSS 类名属性如cssclass_month、cssclass_noday、cssclass_year可以继承后覆盖改样式不需要碰生成的标记。三、一周从哪天开始firstweekday模块级的calendar.setfirstweekday(firstweekday)决定每周从星期几开始calendar.firstweekday()读取当前设置。参数取 0 到 6对应MONDAY到SUNDAY这几个常量。# 适用于 Python 3.8import calendarprint(calendar.MONDAY, calendar.SUNDAY) # 0 6print(calendar.firstweekday()) # 默认 0calendar.setfirstweekday(calendar.SUNDAY)print(calendar.firstweekday()) # 6calendar.setfirstweekday(calendar.MONDAY) # 记得改回来这个设置是全局的模块级函数month、monthcalendar、weekday的排版和默认构造的Calendar实例都会受影响。所以要写「一个用周日起始、另一个用周一起始」的两套输出要么用完立刻改回去要么各建自己的实例# 适用于 Python 3.8import calendarsunday_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月份缩写同上长度 13month_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.8import calendardef render_month(year, month, first_weekdaycalendar.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周一为 0isoweekday()是 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或monthdatescalendarmonth只负责排版展示。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 开始。把这两条记牢剩下的就是排版功夫了。