
简介面向历史专业学生、档案工作者及对传统历法感兴趣的普通用户这份覆盖西汉至当代的中西历查询转化工具包可帮助读者快速完成公历与农历的日期对照与双向转换适用于古籍时间考证、族谱信息整理等场景。压缩包共10个文件包含1个可直接运行的exe主程序、6个gif动态演示图、2个sdb数据库文件及1个html说明页分别承担日期计算、操作示意、历法数据存储与使用引导功能整包仅3.81MB轻量便携。已有382人学习下载。内置SinoYear与SinoMonth数据库支持从西汉到当代的农历、公历双向查询gif图分步展示农历转公历、公历转农历等不同场景下的操作效果新手可对照演示快速上手省去自行摸索的步骤。html说明页还对程序使用与数据库结构作了简介离线可用适合需要随时核对历史日期的研究办公人群。1. 两千年中西历转化难点不在算法而在历法本身看到“两千年中西历转化”这个包名懂行的人第一反应是这又是一个查表工具。但把“两千年”放进标题事情的性质就变了。中西历转化公历与农历互转在 1900 至 2100 这个区间内查表法一个下午就能写完可一旦把时间轴拉长到两千年横跨太初历、授时历、时宪历和现行农历多个历法时代还要处理 1582 年格里历改革留下的日期空洞问题就从“写代码”变成了“定规则”。这篇文章从历法模型和数据边界讲起给出一套可复现的 Python 查表实现再用天文算法补全表外年份最后讲拿到 .rar 数据包后怎么验证完整性、怎么用锚点日期自检。适合做日历应用、历史数据分析和家谱时间换算的开发者也适合所有被“闰月差一天”坑过的同行。2. 中西历转化的历法模型三个常数、两次改革与数据选型2.1 核心参数回归年、朔望月与十九年七闰中西历互转本质是把两个历法体系下的“日序号”对齐。公历是纯太阳历一年 365闰年 366天只跟地球绕太阳的公转周期绑定农历是阴阳合历月份跟朔望月走年份又必须跟回归年对齐。三个数字决定了所有规则回归年 365.2422 日、朔望月 29.5306 日、格里历平均年 365.2425 日。12 个朔望月只有 354.367 日比回归年少 10.88 天。如果不加闰月春节会逐年提前约 17 年就从冬天挪到夏天历法就废了。于是有了十九年七闰19 个回归年约等于 235 个朔望月误差不到 0.09 天。这个结论可以用一段代码自己验证也是判断一个人是背过结论还是真懂历法的分水岭。# 验证十九年七闰找出让 19 个回归年与 N 个朔望月最接近的闰月数 solar_year 365.2422 synodic_month 29.5306 for leaps in range(0, 10): diff abs(19 * solar_year - (12 * 19 leaps) * synodic_month) print(leaps, round(diff, 2)) # 输出里 leaps7 时 diff 最小约 0.09 天代码里的12 * 19 leaps是 19 年的总月数乘上朔望月长度后与 19 个回归年比较差值最小的一组就是实际采用的置闰密度。注意这只是一个统计规律具体哪一年置闰、闰在哪个月现行农历不走固定周期而是按“无中气置闰”的规则逐月判定这一点在 2.2 节展开。这里把影响转换结果的几个参数先列出来后续所有代码和排错都围着它们转。参数数值对转换的影响回归年365.2422 日决定节气与公历日期的对应关系朔望月29.5306 日决定大小月30/29 天交替十九年七闰235 朔望月 ≈ 19 回归年决定闰月出现密度无中气置闰每 30° 一个中气决定闰月具体排在哪个月之后2.2 两千年内的历法分界平朔平气、定朔定气与格里历改革做两千年跨度最容易忽略的是同一个“农历”名称下规则改过不止一次。公元前 104 年实施的太初历把二十四节气系统编入历法但用的是平朔平气——朔和节气都按平均周期推算不追究真实天象。1281 年的授时历把回归年精度做到了 365.2425 日依然沿用平朔。真正的质变在 1645 年时宪历改用定朔定气朔的时刻按月球与太阳的真实黄经位置计算节气按太阳视黄经每 15° 取一次闰月规则也变成“无中气置闰”。这才是现代农历的直接前身。对转换程序来说这个分界带来两个必须处理的现实问题。其一1645 年之前按现代规则回推的“农历”跟当时实际行用的历法可能差一天甚至一个月严谨的数据包会在说明文件里声明自己的历法依据其二公历一侧也有类似的跳变1582 年格里历改革时儒略历的 10 月 4 日之后直接跳到 10 月 15 日中间 10 天在历史上不存在。如果数据范围覆盖 16 世纪程序里的日期序列必须显式处理这个空洞否则 1582 年 10 月的每一天都会平白多算或少算一天。常见做法是给转换器加一个“历法依据”参数1645 年之后按定朔定气规则1645 年之前按平朔平气回推并在输出结果里带上依据标记。不声明依据的转换工具在两千年尺度上等于没有结论。春节落在公历 1 月 21 日到 2 月 21 日之间这个浮动区间任何时代都成立但具体落在哪一天只有历法规则和权威历表说了算。2.3 数据来源选型官方历表、历谱回推与天文算法明确了规则下一个问题是数据从哪来。市面上的转换内核数据来源其实只有三类边界非常清楚。官方历表指紫金山天文台逐年发布的农历覆盖 1900 至 2100 年权威性最高是现代日期转换的默认选择。正史历谱是从历代正史和历志里整理出的编年表能覆盖古代表外年份但存在版本分歧同一个日期在不同学术版历谱里可能相差一天。天文算法回推是按定朔定气规则独立计算合朔与节气的时刻能覆盖任意年份但古早年份受 ΔT 误差影响见第 4 章。选型上我一般用“双轨制”1900 至 2100 用官方历表范围外用天文算法回推交界处用锚点日期对齐。判断一个数据包可不可用先看解压后有没有说明文件、里面有没有声明数据源和历法依据再看覆盖范围是不是连续的两千年。只丢一个二进制表、不给来源和版本号的数据风险在于某个闰月位错了会让一整年的日期整体偏移而这种错误用随机抽样很难发现。两千年中西历转化数据的可信度比算法的精巧度重要一个量级。3. 用查表法实现两千年中西历转化位编码与日偏移算法3.1 一年一条位编码闰月与大小月如何压进 4 字节查表法的核心是把每一年的农历信息压缩成一条整数编码。常见做法是每 4 字节存一年低 4 位存闰月序号0 表示无闰月中间 12 位存十二个月的大小1 表示大月 30 天0 表示小月 29 天第 17 位存闰月大小。两千年也就 8000 字节这正是这类数据包体积小到可以装进 .rar 的原因。以数据包常见的起始条目为例1900 年的编码是0x04BD8低 4 位是 8表示闰八月这也是正史里庚子年闰八月的由来。下面给出解码函数def decode(info: int) - tuple: 解析一条位编码返回 (闰月序号, 每月大小列表) leap info 0xF # 低 4 位闰月序号0 表示无闰月 sizes [(info (4 m - 1)) 1 for m in range(1, 13)] # 12 位每月大小 if leap: sizes.append((info 16) 1) # 闰月大小追加到列表末尾 return leap, sizesleap的取值是“闰月跟在第几个正常月之后”比如闰八月时leap8月份序列是正月到七月、八月、闰八月、九月到腊月。sizes列表的前 12 个元素对应正月到腊月有闰月时第 13 个元素是闰月大小。1900 至 1902 年的编码如下表可以用decode自行核对年份编码闰月备注19000x04BD8闰八月庚子年19010x04AE0无辛丑年19020x0A570无壬寅年3.2 公历转农历基准日偏移与农历年定位有了逐年编码转换就变成纯粹的日期算术。选一个锚点1900 年正月初一对应公历 1900 年 1 月 31 日所有日期先换算成相对这个锚点的天数差再在历年数据里逐月扣除。完整实现如下from datetime import date, timedelta # 数据准备年份 - 位编码实际使用时从 .rar 解出的 CSV 加载这里只列前两行 LUNAR_TABLE { 1900: 0x04BD8, 1901: 0x04AE0, } BASE date(1900, 1, 31) # 1900 年正月初一整个表的日偏移原点 def lunar_year_days(info: int) - int: 农历年的总天数12 个月 可能的闰月 days 29 * 12 bin((info 4) 0xFFF).count(1) leap info 0xF if leap: days 29 ((info 16) 1) return days def solar_to_lunar(y: int, m: int, d: int): 公历转农历返回 (农历年, 月, 日, 是否闰月) offset (date(y, m, d) - BASE).days year 1900 if offset 0: while offset lunar_year_days(LUNAR_TABLE[year]): offset - lunar_year_days(LUNAR_TABLE[year]) year 1 else: while offset 0: # 锚点之前的日期反向累加 year - 1 offset lunar_year_days(LUNAR_TABLE[year]) info LUNAR_TABLE[year] leap info 0xF sizes [(info (4 i)) 1 for i in range(12)] for mon in range(1, 13): dim 29 sizes[mon - 1] # 正常月天数29 或 30 if offset dim: return year, mon, offset 1, False offset - dim if leap mon: leap_dim 29 ((info 16) 1) if offset leap_dim: return year, mon, offset 1, True # 闰月沿用所随月份号 offset - leap_dim raise ValueError(date out of range)逻辑分三层第一层算天数差offset是目标日期相对 1900 年正月初一的天数第二层定位农历年正数往后累减负数往前累加每次减去一整年的天数第三层在年内逐月扣除遇到闰月时先过正常月再过闰月。关键参数有两个BASE必须选一个确认无误的正月初一LUNAR_TABLE必须覆盖目标年份两侧各一年否则反向定位会越界。注意返回值里闰月用跟随的正常月号例如闰五月初一返回(2009, 5, 1, True)这个约定要和 3.3 节的入参严格一致。3.3 农历转公历逐月累加、闰月插入与 2033 年问题反向转换要处理的是“闰月插在中间”的顺序问题。给定农历年、月、日和一个闰月标志先从正月初一出发按正月、二月、……的顺序累加天数遇到闰月就多累加一个闰月天数最后加上日偏移def lunar_to_solar(ly: int, lm: int, ld: int, is_leap: bool False): 农历转公历。lm 取值 1-12闰月号使用其跟随的正常月号如闰八月 lm8 info LUNAR_TABLE[ly] leap info 0xF leap_dim 29 ((info 16) 1) days ld - 1 # 初一是当天所以日偏移减一 for mon in range(1, lm): days 29 ((info (4 mon - 1)) 1) if mon leap: # 闰月紧跟其跟随月之后 days leap_dim if is_leap: days 29 ((info (4 lm - 1)) 1) return BASE timedelta(daysdays)这个函数的入参设计很容易踩坑闰八月必须表达为(ly, 8, ld, True)而不是把月份传成 13。循环里mon leap判断的是“这个月后面有没有闰月”有就累加闰月天数末尾的is_leap分支处理目标本身就是闰月的情况此时需要把其跟随正常月的天数也加上。两个方向的函数共用同一套编码约定是查表法自洽的前提。注意2033 年附近存在著名的“2033 年问题”该年多个月份缺中气按不同置闰口径可能得出闰七月或闰十一月权威历表取的是闰十一月。如果你的代码自己写置闰判定而不是查表2033 年前后的结果就可能与官方数据不一致。处理这类争议年份以数据包内逐年编码表为准不要临时改规则。4. 撑满两千年中西历转化跨度表外年份用天文算法.rar 包先验后解4.1 表外年份的定朔定气算法与 ΔT 修正官方历表只到 2100 年更早和更晚要靠天文算法回推。常见做法是基于 Jean Meeus 在《Astronomical Algorithms》里的方法计算两个序列每个朔望月的合朔时刻和太阳视黄经 15° 整数倍的节气时刻。合朔定初一节气定中气归属再用“无中气置闰”确定闰月。这一步里最容易被低估的参数是 ΔT即力学时 TT 与世界时 UT 的差值所有天文时刻都要先修正它才能换到北京时间def delta_t(year: float) - float: Espenak-Meeus 简化式单位秒。两千年跨度够用 u (year - 1820) / 100 return -20 32 * u * uΔT 在两千年尺度上不是常数公元前后能差到两三个小时20 世纪只有几十秒。对“日”级转换来说只要合朔时刻不落在北京时间 23 点到次日 1 点之间ΔT 误差一般不影响日期结论但恰好在临界时刻就会出现“官方历表说今天是初一、算法推出来是明天”的边界问题。严谨的数据包会在 .rar 里附带一张分段的 ΔT 表而不是只给一个公式。另外再次强调1645 年前的历法本身不是定朔定气算法回推结果与正史历谱可能差一日数据包如果声明“按现行农历规则回推”这个口径要接受并写进文档。4.2 拿到 .rar 包先 list 再 test最后校验历史数据最怕改动一个字节一个闰月位错位一整年的日期全错而且很难发现。处理 .rar 包的正确顺序是先列目录、再测完整性、然后解压、最后做哈希校验unrar l 两千年中西历转化.rar # 1. 列目录先看包内结构和说明文件 unrar t 两千年中西历转化.rar # 2. 测试压缩包完整性不写盘 unrar x 两千年中西历转化.rar ./lunar/ # 3. 保留包内目录结构解压到 ./lunar sha256sum ./lunar/* # 4. Linux/macOS 校验 certutil -hashfile 两千年中西历转化.rar SHA256 # 5. Windows 校验l只列出文件名和大小先看有没有 readme 和校验文件t是测试不解压能发现压缩包是否损坏x按包内路径解压e则是平铺解压遇到没有根目录的包会把一堆 CSV 散到当前目录。校验时要拿 readme 里公布的哈希值逐文件比对没有哈希值就至少核对文件数和首尾年份。unrar 子命令速查如下子命令作用常用参数l列出包内文件无t测试完整性不写盘无x按原路径解压目标目录e平铺解压到当前目录慎用易散落文件p输出文件内容到标准输出小文件预览提示网上搜“rar 密码移除”得到的工具对真正加密的 rar 基本无效。RAR 对数据本体加密没有恢复记录时密码是唯一入口。历法数据是公开资料正常发布方不会加密遇到解压要密码先看压缩包注释和文件名常见做法是把密码写在 readme 或文件名里。若包是同事自己加密后忘了密码直接找原始文件重新打包不要在破解工具上浪费时间。4.3 把数据接进转换函数CSV 加载与十六进制抽查解压后最常见的数据形态是两种CSV 文本表或 4 字节一条的二进制表。CSV 便于人工核对二进制适合程序直接读取。先给 CSV 加载函数这也是第 3 章LUNAR_TABLE的正式来源def load_infos(path: str) - dict[int, int]: infos {} with open(path, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue # 跳过空行和注释 y, info, *_ line.split(,) infos[int(y)] int(info, 16) return infos infos load_infos(./lunar/lunar_table.csv) # 常见做法加载时直接断言覆盖范围和闰月序号合法性 assert min(infos) 1900 and max(infos) 2100 assert all((v 0xF) 12 for v in infos.values())年份用 int 作键可以直接支持负数年份公元前int(info, 16)把0x04BD8还原成整数断言闰月序号不大于 12能在加载阶段就拦住位错位的数据。如果是二进制表用 struct 按 4 字节无符号整数读出来再打开一个十六进制编辑器看文件头合法的第一年数据 0x04BD8 按小端序存储应为D8 4B 04 00对不上说明字节序或表头偏移搞错了。这种抽查十秒完成能挡住最大的低级错误。5. 锚点日期与往返自校验两千年中西历转化结果的验证技巧5.1 锚点日期春节、闰月与基准日的组合验证代码写完先别急着接业务用一组经过反复验证的锚点日期打一遍。这些日期要么是官方历表里的春节要么是闰月日期任何一个对不上都说明数据或代码有问题公历农历验证点1900-01-31庚子年正月初一基准锚点错一整天后面全错1949-10-01己丑年八月初十近现代日期对照2000-01-01己卯年十一月廿五跨世纪边界2009-06-23己丑年闰五月初一闰月位置验证2024-06-10甲辰年五月初五端午节验证2025-01-29乙巳年正月初一近年春节其中 2009 年还有一个额外的天文锚点当年 7 月 22 日日全食当天是农历六月初一这天被多次天文记录交叉验证可以用来复核闰五月之后的大小月配置。这些锚点日期不需要背收藏一份在项目文档里每次拿到新的数据包都跑一遍。5.2 往返一致性与批量自检锚点之外还要做批量自检。随机生成一万个公历日期先转农历再转回公历结果必须严格相等同时验证每年正月初一都落在公历 1 月 21 日到 2 月 21 日之间这是农历历法本身的上下界from datetime import date from random import randint def batch_check(): for ly in range(1900, 2101): cny lunar_to_solar(ly, 1, 1) # 每年春节 assert date(ly, 1, 21) cny date(ly, 2, 21), ly for _ in range(10000): y, m, d randint(1900, 2100), randint(1, 12), randint(1, 28) r solar_to_lunar(y, m, d) assert lunar_to_solar(*r) date(y, m, d) # 往返一致性 batch_check()往返一致是查表法自洽的必要条件但不是充分条件——如果基准日和编码表整体偏移一天往返测试依然能通过这就是锚点日期不可省略的原因。自觉满足这三层验证锚点定基准、往返查自洽、春节区间查边界若数据覆盖 1582 年再单独检查 10 月 4 日到 10 月 15 日的跳变若覆盖 1645 年前确认结果带“按现行农历规则回推”的声明。全过了这两千年的历法表才敢真正接进业务代码。本文还有配套的精品资源点击获取