资讯动态

用Python实现八字排盘:从四柱计算到五行合冲分析

发布时间:2026/10/6 3:38:55 来源:尧图企业网站定制
去年有一次帮朋友排盘手工排到凌晨一点日柱还算错了。后来一对才发现问题出在日柱基准公式选错干支序号整整差了六十位。看上去只是换了个天干地支整个格局全变。从那天起我就决定把这种重复且容易出错的计算全部交给 Python自己只留判断和解读的部分。这篇文章聊的是怎么用 Python 把排八字这件事做得靠谱四柱怎么来、天干地支的合冲关系怎么建模、五行分值怎么算。不是丢一堆玄学术语而是把规则拆成数据结构、函数和可运行的流程。适合两类人一类是对八字感兴趣、想验证手排结果对不对的程序员另一类是打算做命理小工具的 Python 学习者。下面的代码我都实际跑过按顺序组装起来就是一个命令行工具。1. 排八字之前先把基础规则变成数据1.1 四柱由什么决定为什么手工排容易错八字排盘本质上是“历法计算 一套编号规则”。所谓四柱就是年柱、月柱、日柱、时柱每柱一个天干加一个地支一共八个字。难点不在“知识量”而在边界条件太多年柱不是按春节换的是按立春换的。2月4日立春之后出生哪怕还没到春节年柱已经进入下一年。月柱按二十四节气中的“节”划分不是按农历初一划分。立春后是寅月惊蛰后是卯月依此类推。日柱依赖精确的历法推算公历、农历、干支日三者之间没有简单的“年份取模”公式。时柱分十二个时辰23点到次日1点是子时这里还牵涉“晚子时是否算第二天”。这些条件叠在一起手工排盘确实容易错。我最初就是拿网上的公式硬套结果在“立春前”“惊蛰后”这类边界上反复翻车。所以我的做法是把排盘拆成两层。第一层是获取四柱的八个字尽量交给成熟的历法库第二层是基于这八个字做合冲检测、五行分值统计这层逻辑自己写因为每家算法侧重点不同需要能自由调整。1.2 天干地支和五行的映射关系任何五行分值计算第一步都是建立映射表。十个天干、十二个地支分别对应到金木水火土。这部分我在代码里写成两个字典后续所有统计都从这两个字典取值。TIAN_GAN [甲, 乙, 丙, 丁, 戊, 己, 庚, 辛, 壬, 癸] DI_ZHI [子, 丑, 寅, 卯, 辰, 巳, 午, 未, 申, 酉, 戌, 亥] GAN_WU_XING { 甲: 木, 乙: 木, 丙: 火, 丁: 火, 戊: 土, 己: 土, 庚: 金, 辛: 金, 壬: 水, 癸: 水 } ZHI_WU_XING { 子: 水, 丑: 土, 寅: 木, 卯: 木, 辰: 土, 巳: 火, 午: 火, 未: 土, 申: 金, 酉: 金, 戌: 土, 亥: 水 }这里有个容易忽略的地方天干的五行是单一的但地支的五行不能只看“本气”。比如寅属木但寅中实际藏着甲木、丙火、戊土三股气辰属土但辰中藏戊土、乙木、癸水。这一层我在第五节讲五行分值的时候会展开因为“地支藏干”才是分值计算的关键。1.3 合、冲、刑、害的本质是关系映射天干五合、地支六合、三合、六冲、相刑、相害听起来名目很多但对程序员来说这些本质就是“关系表”。每一组关系都可以用元组、集合或者字典表示检测逻辑则是两层循环在所有天干或地支之间两两匹配。我一开始也试图背口诀后来发现根本不用背。把口诀翻译成 Python 的映射表之后代码会自动完成匹配。关键是要把关系表建对尤其是六合和六冲容易记混。这是后文代码的基础先在这里把概念点透检测合冲不是“算”出来的是“查表匹配”出来的。这个思路一旦转换过来写代码就很顺了。2. 手写排盘算法先把日柱和时柱搞定2.1 日柱基于基准日的天数差推算是首选日柱是所有柱里最容易用代码算准的只要选一个干支已知的基准日然后用datetime计算天数差对60取余即可。六十甲子的循环周期是固定的不存在节气、时区这些干扰因素。我常用的基准日是 2024 年 2 月 10 日这一天是农历 2024 年正月初一日柱为甲辰干支序号是 40。基准日可以换成任何一个你确认过的日期关键是“干支序号”不能弄错。六十甲子从甲子开始序号 0乙丑是 1依此类推。from datetime import date TIAN_GAN [甲, 乙, 丙, 丁, 戊, 己, 庚, 辛, 壬, 癸] DI_ZHI [子, 丑, 寅, 卯, 辰, 巳, 午, 未, 申, 酉, 戌, 亥] # 生成六十甲子列表 GZ_LIST [TIAN_GAN[i % 10] DI_ZHI[i % 12] for i in range(60)] # 基准日2024-02-10 为甲辰日干支序号 40 BASE_DATE date(2024, 2, 10) BASE_GZ_INDEX 40 def get_day_ganzhi(d: date) - str: diff (d - BASE_DATE).days return GZ_LIST[(BASE_GZ_INDEX diff) % 60] # 示例 print(get_day_ganzhi(date(2024, 2, 10))) # 甲辰为什么我不用网上流传的“年尾二位数公式”因为那些公式往往是某一段年份内验证过的近似公式换一个世纪就可能出错。基准日法只需要一个可靠的起点加上标准库的日期运算逻辑清晰还方便写单元测试。2.2 时柱五鼠遁口诀转成几行公式时支的确定非常简单每两个小时一个时辰。23点到次日1点是子时1点到3点是丑时后面依此类推。def get_shi_zhi(hour: int, minute: int 0) - int: # 返回时支序号子0丑1寅2 ... # 小时边界23点算子时0点也算子时 if hour 23: return 0 return ((hour 1) // 2) % 12分钟的影响通常只在“半个时辰交界点”才会体现比如 2 点 59 分和 3 点 00 分。我更倾向于把分钟也纳入判断如果超过下一个时辰的起点分钟就归一化到下一时辰但这个处理在传统流派里也有争议后面第六节会聊。时干要靠五鼠遁口诀甲己还加甲乙庚丙作初丙辛从戊起丁壬庚子居戊癸何方发壬子是真途。意思是日干为甲或己时子时天干从甲开始日干为乙或庚时子时天干从丙开始。翻译成公式其实非常简洁子时天干序号等于(日干序号 % 5) * 2然后加上时支序号对10取余。def get_time_ganzhi(day_gan: str, shi_zhi_idx: int) - str: day_gan_idx TIAN_GAN.index(day_gan) zi_gan_idx (day_gan_idx % 5) * 2 # 甲己日从甲起乙庚日从丙起 ... time_gan_idx (zi_gan_idx shi_zhi_idx) % 10 return TIAN_GAN[time_gan_idx] DI_ZHI[shi_zhi_idx]验证一个例子日柱为甲辰午时。日干是甲序号0子时天干甲午时时支序号6甲 6 庚所以午时是庚午。这个案例我后面会在完整流程里用到。2.3 年柱和月柱难点全在节气上年柱比日柱麻烦因为年柱以立春为界。2024 年 2 月 4 日立春2 月 10 日春节时已经进入甲辰年。如果出生在 2 月 4 日之前即使农历还在癸卯年尾八字年柱也还是癸卯。月柱更麻烦十二个月以“节”为界而不是农历初一。立春到惊蛰之间是寅月惊蛰到清冥之间是卯月依此类推。下面是一个只精确到“日”的近似判断用来理解逻辑def get_month_zhi_approx(d: date) - int: # 返回月支序号立春开始为寅(2) # 节气日期为近似值只精确到日 terms [ (2, 4, 2), # 立春 - 寅 (3, 6, 3), # 惊蛰 - 卯 (4, 5, 4), # 清明 - 辰 (5, 6, 5), # 立夏 - 巳 (6, 6, 6), # 芒种 - 午 (7, 7, 7), # 小暑 - 未 (8, 7, 8), # 立秋 - 申 (9, 8, 9), # 白露 - 酉 (10, 8, 10), # 寒露 - 戌 (11, 7, 11), # 立冬 - 亥 (12, 7, 0), # 大雪 - 子 (1, 6, 1), # 小寒 - 丑 ] cur (d.month, d.day) # 立春前属于上一年的子月或丑月 if cur (2, 4): return 0 if cur (1, 6) else 1 for i, (m, day, branch) in enumerate(terms): if cur (m, day): return terms[i - 1][2] return terms[-1][2]这个函数能跑但我必须坦白讲它只精确到日遇到节气前后一天的边界就会出错。比如某年立春在 2 月 4 日 16 点如果你出生在当天早上 8 点按日期判断你已经算寅月但实际上还没交立春严格来说仍然是上一年的丑月。这种误差对排盘结果影响很大。月干部分可以用五虎遁口诀甲己之年丙作首乙庚之岁戊为头丙辛之年寻庚起丁壬壬寅顺水流戊癸何处觅甲寅。公式是寅月天干 (年干序号 % 5) * 2 2再加月支相对寅月的偏移量。def get_month_ganzhi(year_gan: str, month_zhi_idx: int) - str: year_gan_idx TIAN_GAN.index(year_gan) first_month_gan_idx (year_gan_idx % 5) * 2 2 if first_month_gan_idx 10: first_month_gan_idx - 10 offset (month_zhi_idx - 2) % 12 # 寅月偏移为0卯月为1 ... month_gan_idx (first_month_gan_idx offset) % 10 return TIAN_GAN[month_gan_idx] DI_ZHI[month_zhi_idx]2.4 校验直接把节气判断交给 lunar_python手写节气边界非常容易出错因为节气时刻是天文计算的结果每一年都不一样。我的建议是想理解原理就手写近似版想真正做工具就老老实实用专业历法库。Python 生态里我用得比较多的是lunar_python基于 lunar-java 移植封装了农历、公历、八字、节气等排四柱可以直接拿结果。pip install lunar_pythonfrom lunar_python import Solar solar Solar.fromYmdHms(2024, 2, 10, 12, 0, 0) lunar solar.getLunar() eight_char lunar.getEightChar() print(eight_char.getYear()) # 年柱 print(eight_char.getMonth()) # 月柱 print(eight_char.getDay()) # 日柱 print(eight_char.getTime()) # 时柱用这个库的好处是节气精确到秒立春、惊蛰等边界全部由天文历法计算不会因为我手写的“2月4日”近似表而出错。实际项目里我的排盘模块只用lunar_python拿八个字后面的合冲和五行分析全部自己写。到这里核心技术路线已经清楚了日柱、时柱、年柱、月柱的“为什么”理解了精确计算交给库接下来就可以把重心放到真正有价值的部分——合冲检测和五行分值计算。3. 五行分值从八个字到带权重的统计结果3.1 天干好算地支要看藏干很多初学项目算五行就是数“这个八字里木有几个、火有几个”然后得出结论。这种做法太粗了。因为一个地支并非只属于一个五行它内部还藏着其他天干的气专业术语叫“地支藏干”。比如寅这个地支本气是甲木中气是丙火余气是戊土。你把寅整个算成木就丢了火和土的贡献。如果命局里同时有午火、戌土寅中的丙火、戊土会影响全局的燥湿平衡。我在代码里用一个字典表达地支藏干和权重ZHI_CANG_GAN { 子: {癸: 1.0}, 丑: {己: 0.6, 癸: 0.3, 辛: 0.1}, 寅: {甲: 0.6, 丙: 0.3, 戊: 0.1}, 卯: {乙: 1.0}, 辰: {戊: 0.6, 乙: 0.3, 癸: 0.1}, 巳: {丙: 0.6, 戊: 0.3, 庚: 0.1}, 午: {丁: 0.7, 己: 0.3}, 未: {己: 0.6, 丁: 0.3, 乙: 0.1}, 申: {庚: 0.6, 壬: 0.3, 戊: 0.1}, 酉: {辛: 1.0}, 戌: {戊: 0.6, 辛: 0.3, 丁: 0.1}, 亥: {壬: 0.7, 甲: 0.3}, }这套权重不是标准答案不同流派可能略有差异但它比“单纯数五行个数”要合理得多。核心思想是一个地支的五行能量按比例分散到藏干上权重系数加起来等于1。3.2 月令为什么权重最高传统命理极度重视月令因为月令代表一个人出生时的季节气候。夏天火旺冬天水旺春木旺秋金旺。“得令”“失令”这两个词说的就是日主是否得到月令的支持。所以在分值模型里我给月支额外翻倍权重。比如月支是寅寅中的甲木权重0.6翻倍后变成1.2。这样寅月出生的人木的分数天然会高一些符合“春天木旺”的常识。月令权重完全可以调。我见过一些命理软件把月令权重设为1.5或2.5。关键是模型要可解释不是“随便调的”而是对应“季节对五行能量影响程度”的假设。3.3 分值计算代码核心函数接收四个柱的干支返回五行的原始分值和归一化百分比def calc_wuxing_score(pillars): pillars: list of (天干, 地支)顺序为年、月、日、时 scores {金: 0.0, 木: 0.0, 水: 0.0, 火: 0.0, 土: 0.0} for idx, (gan, zhi) in enumerate(pillars): # 天干权重固定为1 scores[GAN_WU_XING[gan]] 1.0 # 地支藏干月令权重翻倍 zhi_weight 2.0 if idx 1 else 1.0 for cang_gan, w in ZHI_CANG_GAN[zhi].items(): scores[GAN_WU_XING[cang_gan]] w * zhi_weight total sum(scores.values()) percent {k: round(v / total * 100, 1) for k, v in scores.items()} return scores, percent调用时需要先有四个柱。如果某个地支藏干权重表里漏了字函数会直接报 KeyError这能逼着我们把表补完整。我早期就因为把“辰”写错了导致土的分值明显偏低。4. 相合相冲检测把口诀变成字典和集合4.1 天干五合与地支六冲合冲检测是整个项目里最不需要“智力”的部分但却是最容易写错的部分因为关系多、容易漏。我的做法是先把所有关系建成模块级常量# 天干五合合化后的五行 TIAN_GAN_HE { (甲, 己): 土, (乙, 庚): 金, (丙, 辛): 水, (丁, 壬): 木, (戊, 癸): 火, } # 地支六冲 DI_ZHI_LIU_CHONG { (子, 午), (丑, 未), (寅, 申), (卯, 酉), (辰, 戌), (巳, 亥), }六冲我用无序集合表示因为冲是双向的不需要存储“子午”和“午子”两个方向。检测函数就是两层循环def check_chong(pillars): zhis [zhi for _, zhi in pillars] results [] for i in range(len(zhis)): for j in range(i 1, len(zhis)): pair (zhis[i], zhis[j]) if pair in DI_ZHI_LIU_CHONG or (pair[1], pair[0]) in DI_ZHI_LIU_CHONG: results.append((zhis[i], zhis[j], 六冲)) return results4.2 地支六合、三合与三会六合是两两一组三合则是三字成局。三合局的经典口诀是申子辰合水局亥卯未合木局寅午戌合火局巳酉丑合金局。三会用得少一些但做完整工具时也建议加上。# 地支六合 DI_ZHI_LIU_HE { (子, 丑): 土, (寅, 亥): 木, (卯, 戌): 火, (辰, 酉): 金, (巳, 申): 水, (午, 未): 土, } # 地支三合局 DI_ZHI_SAN_HE { (申, 子, 辰): 水, (亥, 卯, 未): 木, (寅, 午, 戌): 火, (巳, 酉, 丑): 金, } # 地支三会方 DI_ZHI_SAN_HUI { (寅, 卯, 辰): 木, (巳, 午, 未): 火, (申, 酉, 戌): 金, (亥, 子, 丑): 水, }三合和三会检测不能简单两两匹配得判断“三个地支是否都在四柱里出现过”。这个用集合最方便def check_san_he(pillars): zhi_set {zhi for _, zhi in pillars} results [] for trio, wx in DI_ZHI_SAN_HE.items(): if set(trio).issubset(zhi_set): results.append((.join(trio), 三合, wx)) for trio, wx in DI_ZHI_SAN_HUI.items(): if set(trio).issubset(zhi_set): results.append((.join(trio), 三会, wx)) return results这里要注意“局”和“方”区分三合局隔位也能成局三会方必须连续。但简化模型里我只看“三个字是否都在四柱中”。如果你做严格的命理工具可以再加距离权重。4.3 相刑和相害也顺便做进去如果只做合冲功能会显得单薄。相刑和相害也是常见关系而且实现方式完全一样DI_ZHI_XING { (子, 卯), (寅, 巳), (巳, 申), (寅, 申), # 寅巳申三刑 (丑, 戌), (戌, 未), (丑, 未), # 丑戌未三刑 } DI_ZHI_HAI { (子, 未), (丑, 午), (寅, 巳), (卯, 辰), (申, 亥), (酉, 戌), }检测相刑的细节是寅巳申三刑必须三个字都出现才算典型三刑但如果只出现两字如寅巳部分地区流派也认为构成“寅巳刑”。我建议默认两字不全算只输出“有潜在相刑组合”让使用者自己判断。4.4 合冲结果怎么和五行分值结合涉及解读我的原则是工具只输出客观关系不下吉凶结论。比如“子午冲”是水火相冲如果命局火旺冲的结果可能让火更不稳定如果水旺可能反而激发水势。单纯把“冲”标记为凶是不负责任的。但代码层面可以做一个联动检测到合冲之后把相关地支的五行属性也打印出来这样使用者能直观看到是“哪个五行和哪个五行发生了关系”。比如for gan, zhi in pillars: print(f{gan}{zhi} - 天干:{GAN_WU_XING[gan]}, 地支:{ZHI_WU_XING[zhi]})这样合冲检测和五行分值就串起来了不会各算各的。5. 一个完整案例从出生日期到分析报告5.1 主流程代码把前面所有模块拼起来就是一个简单的排盘工具。核心流程用lunar_python获取四柱。用四柱构建pillars列表。计算五行分值。检测天干五合、地支六合、六冲、三合、三会、相刑、相害。格式化输出。from lunar_python import Solar from datetime import datetime def pai_pan(dt: datetime): solar Solar.fromYmdHms(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) lunar solar.getLunar() ec lunar.getEightChar() pillars [ (ec.getYear()[:1], ec.getYear()[1:]), (ec.getMonth()[:1], ec.getMonth()[1:]), (ec.getDay()[:1], ec.getDay()[1:]), (ec.getTime()[:1], ec.getTime()[1:]), ] scores, percent calc_wuxing_score(pillars) print(出生时间, dt) print(八字, .join(gan zhi for gan, zhi in pillars)) print(五行分值, scores) print(五行比例, percent) print(天干五合, check_tian_gan_he(pillars)) print(地支六合, check_liu_he(pillars)) print(地支六冲, check_chong(pillars)) print(三合三会, check_san_he(pillars)) print(相刑, check_xing(pillars)) print(相害, check_hai(pillars))这里我假设check_tian_gan_he、check_liu_he、check_xing、check_hai和前面的check_chong、check_san_he一样都是两两遍历地支或天干。具体代码结构可以根据自己的风格合并我就不全部贴重复代码了。5.2 运行输出效果我拿 2024 年 2 月 10 日 12:00 这个时间跑了一遍输出大致如下出生时间 2024-02-10 12:00:00 八字 甲辰 丙寅 甲辰 庚午 五行分值 {金: 1.0, 木: 3.8, 水: 0.2, 火: 2.3, 土: 1.7} 五行比例 {金: 11.1%, 木: 42.2%, 水: 2.2%, 火: 25.6%, 土: 18.9%} 天干五合 无 地支六合 无 地支六冲 无 三合三会 无 相刑 无 相害 无这个命例出生在正月初一的中午甲辰年、丙寅月、甲辰日、庚午时。从五行比例看木最旺因为年干、日干都是甲木月支寅木又得月令翻倍春天木旺的季节特征很明显。合冲检测里没匹配到任何关系说明这个命例比较“干净”。为了验证合冲模块确实工作我又构造了一个四柱样例来测试test_pillars [ (甲, 子), (庚, 午), (丙, 申), (壬, 辰), ] print(check_chong(test_pillars)) # 应输出 子午冲 print(check_san_he(test_pillars)) # 应输出 申子辰三合水局输出[(子, 午, 六冲)] [(申子辰, 三合, 水)]这说明六冲、三合检测逻辑都是通的。5.3 工具化思路命令行和 Web API完成核心函数之后把它包装成命令行工具并不难。用argparse接收一个日期时间输出文本报告if __name__ __main__: import argparse parser argparse.ArgumentParser(descriptionPython 排八字小工具) parser.add_argument(--datetime, requiredTrue, help出生时间格式2024-02-10 12:00) args parser.parse_args() dt datetime.strptime(args.datetime, %Y-%m-%d %H:%M) pai_pan(dt)跑起来就是python bazi.py --datetime 2024-02-10 12:00如果你想做一个 Web API也可以用 FastAPI 包一层输入出生时间、经纬度输出 JSON 格式的命盘数据。经度参数留给“真太阳时”校正用这一点第六节马上讲到。6. 实际排盘容易忽略的三个细节6.1 子时换日早子时和晚子时的分歧23 点到 24 点是“晚子时”0 点到 1 点是“早子时”。传统流派里存在两种处理方式一种认为 23 点已经进入第二天日柱应该按第二天算。另一种认为 0 点才真正换日23 点到 23 点 59 分仍算当天日柱时柱干支则用当天的五鼠遁推算。这两种处理对时柱的影响尤其大。我的建议是工具里保留一个开关参数比如late_zi_as_next_day: bool False默认按“0点换日”处理同时让使用者知道这个参数存在自行选择流派。6.2 节气不是“当天”而是“精确到秒的时刻”我前面给出了节气的近似日期表也明确说了它只能用于学习。真正排盘时立春可能发生在 2 月 3 日 22 点也可能发生在 2 月 4 日 16 点。出生在当天上午和下午年柱可能完全不同。这个问题没有捷径要么调用lunar_python这样带有天文历法计算的库要么自己引入精确节气数据。自己维护一张“每年节气时刻表”非常痛苦而且容易因数据源错误导致全线崩盘。6.3 真太阳时北京时间还是当地时间中国使用统一的北京时间也就是东经 120 度的地方平太阳时。但一个人出生在成都东经约 104 度和北京时间大约差 16 度每度 4 分钟就是 64 分钟左右的时差。严格的传统排盘会先用出生经度把北京时间换算成真太阳时再用校正后的时间排时柱。这个校正对时柱影响较大因为时柱每两个小时一变差一个小时可能就从午时变成巳时。我的建议和子时问题一样把经纬度作为可选输入调用方自己决定要不要做真太阳时校正。不做也完全可以跑通只是结果在部分流派看来不够“正统”。这类问题没有绝对对错关键是工具要有明确的可配置性。我实际用下来的感受是排八字这件事真正花时间的不是写合冲检测和五行分值而是把边界条件处理干净。日柱基准、节气时刻、子时换日、真太阳时随便哪一个没考虑周全结果就可能全错。所以我的最终建议是所有边界条件都做成显式参数让每个使用者知道自己选的是什么规则。这样工具本身才站得住脚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑