首页 > 编程语言 >Python 农历公历相互转换

Python 农历公历相互转换

时间:2023-08-13 11:44:06浏览次数:40  
标签:公历 info Python 30 29 农历 lunar day

Python 农历公历相互转换

stackOverflowshIP属地: 江苏 0.2252019.02.19 18:23:48字数 2,054阅读 6,862

背景

日常用python处理各种数据分析工作,最近需要对历年春节期间的数据做一些对比工作,本来只是用了一个简单的日期数组来进行,但后来发现一些数据在农历日期进行对比的时候,会有一些有趣的规律,进而产生了公历农历进行互转的需求。

本来以为网上有现成的库或者是文章,结果发现要不是请求网络Api,要么就是数据有错误,语言不是Python的等等。由于基于是10万量级的数据,网络请求转换明显是不可能的,所以自己写了一个本地转换的库,研究过程中又发现了一些比较有趣的在平时开发中用的不多的算法和Python基础,就都添加了上去,并成为我第一个发布的pypi包。这篇文章主要介绍基础算法和使用方法,后续会把那些Python基础知识也补充进去。

项目使用说明

先上项目吧,想直接使用的同学,拿来就能用了 ZhDate GitHub主页,对开发过程有兴趣的请继续往下看。

安装方法

通过 pip 直接安装

pip install zhdate

或从git拉取

git clone https://github.com/CutePandaSh/zhdate.git
cd zhdate
python setup.py install

更新

pip install zhdate --upgrade

使用方法

见如下代码案例:

from zhdate import ZhDate

date1 = ZhDate(2010, 1, 1) # 新建农历 2010年正月初一 的日期对象
print(date1)  # 直接返回农历日期字符串
dt_date1 = date1.to_datetime() # 农历转换成阳历日期 datetime 类型

dt_date2 = datetime(2010, 2, 6)
date2 = ZhDate.from_datetime(dt_date2) # 从阳历日期转换成农历日期对象

date3 = ZhDate(2020, 4, 30, leap_month=True) # 新建农历 2020年闰4月30日
print(date3.to_datetime())

# 支持比较
if ZhDate(2019, 1, 1) == ZhDate.from_datetime(datetime(2019, 2, 5)):
    pass

# 减法支持
new_zhdate = ZhDate(2019, 1, 1) - 30  #减整数,得到差额天数的新农历对象
new_zhdate2 = ZhDate(2019, 1, 1) - ZhDate(2018, 1, 1) #两个zhdate对象相减得到两个农历日期的差额
new_zhdate3 = ZhDate(2019, 1, 1) - datetime(2019, 1, 1) # 减去阳历日期,得到农历日期和阳历日期之间的天数差额

# 加法支持
new_zhdate4 = ZhDate(2019, 1, 1) + 30 # 加整数返回相隔天数以后的新农历对象

# 中文输出
new_zhdate5 = ZhDate(2019, 1, 1)
print(new_zhdate5.chinese())

# 当天的农历日期
ZhDate.today()

核心算法

重要的事情说三遍

农历不是算出来的,是天文台观测出来的
农历不是算出来的,是天文台观测出来的
农历不是算出来的,是天文台观测出来的

所以也想做农历功能的同学就不要费心去学什么农历算法了,浪费了我三天时间也没看懂到底是怎么计算的。
目前通用的也是比较准确的,可下载的农历阳历对照数据是 香港天文台农历对照表(文字版), 可下载txt格式的农历对照数据。写了一个简单的爬虫,将所有txt文件下载下来。注意获得到的txt是Big5的,并且需要跳过头部的三行,头部三行是每个文件的年份基础信息。可以用以下代码来读取,这里还用到了如何跳过文件头部n行,以及打开非utf8编码格式文件的小技巧。

with open('./{年份}.txt', encoding='big5') as file:
     for n_line, line in enumerate(file.readline()):
        if n_line < 3:
            continue
       else:
            dosomething()

下载到的数据是从 公历 1901年1月1日,农历 1900年11月11日起,至 2100年12月31日,农历 2100年12月1日之间的200年的每天对照数据。经过编码转换后,重新存一个json或者pickle文件就可以直接拿来用了,速度也不慢。但是这个包含了所有日期数据的文件,json格式的话,有6M多,字典pickle格式也有2M多,显然不利于传播和重复使用。参考了网上一篇Java的农历转换源码,虽然使用的基础数据存在错误,但是算法非常精辟,所以就 拿来主义 了。

香港天文台原始数据处理

从原始数据处理转换成可用于统计和进一步处理的完整代码如下:

from datetime import datetime

CHINESENUMBERS = {
    '一': 1,
    '二': 2,
    '三': 3,
    '四': 4,
    '五': 5,
    '六': 6,
    '七': 7,
    '八': 8,
    '九': 9,
    '十': 10,
    '正': 1
}

def read_single_file(file_name, coding="big5"):
    result = list()
    with open(file_name, encoding=coding) as file:
        for idx, l in enumerate(file.readlines()):
            if idx < 3:
                continue
            else:
                result.append(list(filter(lambda x: x != "" and x != "\n", l.split(" "))))
    return result

def day_data_process(day_data, c_year, c_month, c_leap=False):
    day_info = dict()
    date = datetime.strptime(day_data[0], '%Y年%m月%d日')
    day_info['year'] = date.year
    day_info['month'] = date.month
    day_info['day'] = date.day

    chinese_day = day_data[1]
    if chinese_day == '正月':
        day_info['lunar_year'] = c_year + 1
    else:
        day_info['lunar_year'] = c_year
    
    if chinese_day[-1] == '月':
        if chinese_day[0] == '閏':
            day_info['lunar_leap'] = True
            if len(chinese_day) == 4:
                day_info['lunar_month'] = 10 + CHINESENUMBERS[chinese_day[2]]
            else:
                day_info['lunar_month'] = CHINESENUMBERS[chinese_day[1]]
        else:
            day_info['lunar_leap'] = False
            if len(chinese_day) == 3:
                day_info['lunar_month'] = 10 + CHINESENUMBERS[chinese_day[1]]
            else:
                day_info['lunar_month'] = CHINESENUMBERS[chinese_day[0]]
        day_info['lunar_day'] = 1
    else:
        day_info['lunar_month'] = c_month
        day_info['lunar_leap'] = c_leap

        if chinese_day[0] == '初':
            day_info['lunar_day'] = CHINESENUMBERS[chinese_day[1]]
        elif chinese_day[0] == '十':
            day_info['lunar_day'] = 10 + CHINESENUMBERS[chinese_day[1]]
        elif chinese_day[0] == '廿':
            day_info['lunar_day'] = 20 + CHINESENUMBERS[chinese_day[1]]
        elif chinese_day == '二十':
            day_info['lunar_day'] = 20
        elif chinese_day == '三十':
            day_info['lunar_day'] = 30
    
    return day_info

def lunar_data():
    data_list = list()
    for i in range(1901, 2101):
        data_list = data_list + read_single_file(f"./rawdata/{i}.txt")
    lunar_calendar_data = list()
    for day in data_list:
        try:
            datetime.strptime(day[0], '%Y年%m月%d日')
        except:
            continue
        if len(lunar_calendar_data) != 0:
            lunar_calendar_data.append(
                day_data_process(day, lunar_calendar_data[-1]['lunar_year'], lunar_calendar_data[-1]['lunar_month'], lunar_calendar_data[-1]['lunar_leap'])
            )
        else:
            lunar_calendar_data.append(day_data_process(day, 1900, 11))
    
    return lunar_calendar_data

上述代码可返回一个每天日期信息字典的List,可再使用pandas对这些数据进行编码。编码过程略。

年度数据编码

每一整年的数据可用 20位的二进制数表示

 0001 1000 1000 1000 1000
  • 第一部分,最左边的前4位,只有0或1,0表示当年闰月为小月(即29天),1表示当年闰月为大月(即30天),这个需要和最右侧的最后4位结合使用。
  • 第二部分,中间的12位,表示当年农历年每月的大小月,0表示小月,1表示大月,忽略闰月,从左起第一位表示1月。
  • 第三部分,最右侧的最后4位,转换成10进制表示当年的闰月月份,如果闰月不存在那就为 0。

举例说明

2019年的年度编码 43312

转换成二进制为

0000 1010 1001 0011 0000

位数不足左侧补0, 解析如下:

  • 先考虑中间12位表示月份,形成月份天数数组 [30, 29, 30, 29, 30, 29, 29, 30, 29, 29, 30, 30],此为农历1-12月的月份天数。
  • 再看最后4位,等于0,表示当年无闰月
  • 解析完成

2020年的年度编码 31060

转换成二进制为

0000 0111 1001 0101 0100

位数不足左侧补0, 解析如下:

  • 先考虑中间12位表示月份,形成月份天数数组 [29, 30, 30, 30, 30, 29, 29, 30, 29, 30, 29, 30],此为农历1-12月的月份天数。
  • 再看最后4位,转换10进制,等于4,表示当年存在 闰4月
  • 查看最左侧,前4位,等于0,表示当年闰4月为小月,只有29天
  • 在初始月份数组的 4月后插入 29,形成新的月份天数List [29, 30, 30, 30, 29, 30, 29, 29, 30, 29, 30, 29, 30],这里包含13个月,含闰月的天数。
  • 解析完成

坑爹的网上农历说明

有些网站上提到每年的闰月应该和实际月天数相同,比如上述的例子,按照说明那么 2020年的农历4月和农历闰4月的天数是相同的,实际上是不同的,所以按照天文台的数据进行处理吧。

年度编码解析代码

def decode(year_code):
    """解析年度农历代码函数
    
    Arguments:
        year_code {int} -- 从年度代码数组中获取的代码整数
    
    Returns:
        [int] -- 当前年度代码解析以后形成的每月天数数组,已将闰月嵌入对应位置,即有闰月的年份返回长度为13,否则为12
    """
    month_days = list()
    for i in range(5, 17):
        if (year_code >> (i - 1)) & 1:
            month_days.insert(0, 30)
        else:
            month_days.insert(0, 29)
    if year_code & 0xf:
        if year_code >> 16:
            month_days.insert((year_code & 0xf), 30)
        else:
            month_days.insert((year_code & 0xf), 29)
    return month_days

香港天文台能下载到的只有1901年-2100年的数据,作为一个强迫症患者,看到这个1901总是不爽,在百度上查了一下,正好它支持1900年2050年的数据,所以手动添加了1900的部分,形成了这个项目中的1900 - 2100年的完整农历数据。

为了加快运算除了年度代码,还存储了每年的农历正月初一的公历日期,这样就用了20K就保存了200年的农历数据。

天干地支算法

天干地支是中国特有的一种历法,看起来很复杂,实际上用简单的代码就用打印出来

tian = '甲乙丙丁戊己庚辛壬癸'
di = '子丑寅卯辰巳午未申酉戌亥'
for i in range(0, 60):
    print(f"{i:} {tian[i % 10]}{di[i % 12]}")

----------------
0 甲子
1 乙丑
2 丙寅
3 丁卯
4 戊辰
5 己巳
6 庚午
...(略)
51 乙卯
52 丙辰
53 丁巳
54 戊午
55 己未
56 庚申
57 辛酉
58 壬戌
59 癸亥

对的,就是这么简单,天干是10进制,地支是12进制,所以每一个序数对10取余数,得到天干,每个序数对12取余数得到地支,相互组合就是该序数对应的天干地支数。所以不用查表,用的时候直接打印一份就行了。

年度的天干地支最容易算,需要注意的是必须使用农历年份,不能用公历年份。查下百度得知 1900年为 庚子年,序号 36,所以用以下代码可获得当前农历年的天干地支

def year_tiandi(year):
    td_num = year - 1900 + 36
    tian = '甲乙丙丁戊己庚辛壬癸'
    di = '子丑寅卯辰巳午未申酉戌亥'
    return f"{tian[td_num % 10]}{di[td_num % 12]}年"

总结

以上就是整个项目中最核心的部分,本质上来说,这个项目并不涉及复杂算法,最核心的是使用二进制来压缩存储年度数据,相关的在Python中如何二进制的基本用法,以及应用案例我会另开文章来写。至于涉及到的其他,我觉得需要整理的基础知识点也会陆续补充上来,作为分享以及自己的学习笔记。

计划中逐步完成的相关文章清单:

  • Python中二进制的使用 (撰写中)
  • Python自定义类中的函数重载,如何自定义打印字符串,自定义比较,以及加减运算符(未开始)
  • 如何将自己的代码让 pip 能够 install (未开始)
  • 其他想到的

标签:公历,info,Python,30,29,农历,lunar,day
From: https://www.cnblogs.com/tomcat2022/p/17626340.html

相关文章

  • Python代码分享-获取B站粉丝数量
    分享一下一段Python代码。#获取B站粉丝数量#冰河之刃#2020-08-02importrequestsimportjsonurl="https://api.bilibili.com/x/relation/stat?vmid=490458635"payload={}#发起请求response=requests.request("GET",url,data=payload)#print(response.......
  • 学习笔记-流畅的Python 1st
    P31和*都遵循不修改原有的操作对象,而是创建一个新的序列>>>a=[1,2,3]>>>c=a*2>>>a[0]=3>>>c[1,2,3,1,2,3]如果在a*n这个语句中,序列a里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my......
  • 4.0 Python 变量与作用域
    在python中,变量的作用域决定了变量在哪些位置可以被访问。一个程序中的变量并不是所有的地方都可以访问的,其访问权限决定于变量的赋值位置。python中有两种最基本的变量作用域:局部作用域和全局作用域。局部变量是在函数内部定义的变量,只能在其被声明的函数内部访问。而全局变量则......
  • 5.0 Python 定义并使用函数
    函数是python程序中的基本模块化单位,它是一段可重用的代码,可以被多次调用执行。函数接受一些输入参数,并且在执行时可能会产生一些输出结果。函数定义了一个功能的封装,使得代码能够模块化和组织结构化,更容易理解和维护。在python中,函数可以返回一个值或者不返回任何值,而且函数的参......
  • n、Appium_Python_Api
    一、Appium_Python_Api方法参考博客:https://blog.csdn.net/ezreal_tao/article/details/80911950https://cloud.tencent.com/developer/article/1569596contextscontexts(self):Returnsthecontextswithinthecurrentsession.返回当前会话中的上下文,使用后可以识别H5......
  • python简介
    python简介1.python的产生与应用python于1989年圣诞节期间由吉多·范罗苏姆(GuidovanRossum)(中文名字:龟叔)为打发时间开发的一个新脚本解释程序,作为ABC语言的一种继承。(龟叔:2005年加入谷歌至2012年,2013年加入Dropbox直到现在,依然掌握着Python发展的核心方向,被称为仁慈的独裁者)。......
  • python基础
    python基础一、python基础初识1.运行python代码。在d盘下创建一个t1.py文件内容是:print('helloworld')打开windows命令行输入cmd,确定后写入代码pythond:t1.py您已经运行了第一个python程序,即:终端---->cmd----->python文件路径。回车搞定~2.解释器。上一步中执......
  • 某公司笔试题 - 坐标移动(附python代码)
    #开发一个坐标计算工具,A表示向左移动,D表示向右移动,W表示向上移动,S表示向下移动。从(0,0)点开始移动,从输入字符串里面读取坐标,并将最终输入结果输出文件里面。#输入坐标为A(或D或W或S)+数字(两位以内)。坐标之间以;分隔#数据范围:1<=n<=10000每组输入的字符串长度坐标保证满足-......
  • python案例
    这猜单词游戏。具体步骤如下:导入random模块,用于随机选择单词。设置初始生命次数为3。创建一个单词列表words,其中包含了一些单词。使用random.choices()函数从单词列表中随机选择一个单词作为秘密单词secret_word。创建一个clue列表,用于表示未猜中的字母的占位符。初始时,将cl......
  • Python微信公众号文章批量转pdf
    文章来源:https://www.cnblogs.com/MrFlySand/p/17216072.html操作步骤下载离线html网页文件1、登录微信公众号后台,打开“文章发表记录”。按Ctrl+S保存离线html网页文件。3、记住html文件保存路径,设置html文件名称。4、点击第2页的文章发表记录,重复步骤2、步骤3的操......