Python 農(nóng)歷公歷相互轉(zhuǎn)換

背景

日常用python處理各種數(shù)據(jù)分析工作改橘,最近需要對(duì)歷年春節(jié)期間的數(shù)據(jù)做一些對(duì)比工作私蕾,本來只是用了一個(gè)簡(jiǎn)單的日期數(shù)組來進(jìn)行扛施,但后來發(fā)現(xiàn)一些數(shù)據(jù)在農(nóng)歷日期進(jìn)行對(duì)比的時(shí)候,會(huì)有一些有趣的規(guī)律民假,進(jìn)而產(chǎn)生了公歷農(nóng)歷進(jìn)行互轉(zhuǎn)的需求拔疚。

本來以為網(wǎng)上有現(xiàn)成的庫或者是文章荞彼,結(jié)果發(fā)現(xiàn)要不是請(qǐng)求網(wǎng)絡(luò)Api觉既,要么就是數(shù)據(jù)有錯(cuò)誤,語言不是Python的等等阀参。由于基于是10萬量級(jí)的數(shù)據(jù)肝集,網(wǎng)絡(luò)請(qǐng)求轉(zhuǎn)換明顯是不可能的,所以自己寫了一個(gè)本地轉(zhuǎn)換的庫蛛壳,研究過程中又發(fā)現(xiàn)了一些比較有趣的在平時(shí)開發(fā)中用的不多的算法和Python基礎(chǔ)包晰,就都添加了上去,并成為我第一個(gè)發(fā)布的pypi包炕吸。這篇文章主要介紹基礎(chǔ)算法和使用方法,后續(xù)會(huì)把那些Python基礎(chǔ)知識(shí)也補(bǔ)充進(jìn)去勉痴。

項(xiàng)目使用說明

先上項(xiàng)目吧赫模,想直接使用的同學(xué),拿來就能用了 ZhDate GitHub主頁蒸矛,對(duì)開發(fā)過程有興趣的請(qǐng)繼續(xù)往下看瀑罗。

安裝方法

通過 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) # 新建農(nóng)歷 2010年正月初一 的日期對(duì)象
print(date1)  # 直接返回農(nóng)歷日期字符串
dt_date1 = date1.to_datetime() # 農(nóng)歷轉(zhuǎn)換成陽歷日期 datetime 類型

dt_date2 = datetime(2010, 2, 6)
date2 = ZhDate.from_datetime(dt_date2) # 從陽歷日期轉(zhuǎn)換成農(nóng)歷日期對(duì)象

date3 = ZhDate(2020, 4, 30, leap_month=True) # 新建農(nóng)歷 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  #減整數(shù)胸嘴,得到差額天數(shù)的新農(nóng)歷對(duì)象
new_zhdate2 = ZhDate(2019, 1, 1) - ZhDate(2018, 1, 1) #兩個(gè)zhdate對(duì)象相減得到兩個(gè)農(nóng)歷日期的差額
new_zhdate3 = ZhDate(2019, 1, 1) - datetime(2019, 1, 1) # 減去陽歷日期,得到農(nóng)歷日期和陽歷日期之間的天數(shù)差額

# 加法支持
new_zhdate4 = ZhDate(2019, 1, 1) + 30 # 加整數(shù)返回相隔天數(shù)以后的新農(nóng)歷對(duì)象

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

# 當(dāng)天的農(nóng)歷日期
ZhDate.today()

核心算法

重要的事情說三遍

農(nóng)歷不是算出來的斩祭,是天文臺(tái)觀測(cè)出來的
農(nóng)歷不是算出來的劣像,是天文臺(tái)觀測(cè)出來的
農(nóng)歷不是算出來的,是天文臺(tái)觀測(cè)出來的

所以也想做農(nóng)歷功能的同學(xué)就不要費(fèi)心去學(xué)什么農(nóng)歷算法了摧玫,浪費(fèi)了我三天時(shí)間也沒看懂到底是怎么計(jì)算的耳奕。
目前通用的也是比較準(zhǔn)確的,可下載的農(nóng)歷陽歷對(duì)照數(shù)據(jù)是 香港天文臺(tái)農(nóng)歷對(duì)照表(文字版), 可下載txt格式的農(nóng)歷對(duì)照數(shù)據(jù)诬像。寫了一個(gè)簡(jiǎn)單的爬蟲屋群,將所有txt文件下載下來。注意獲得到的txt是Big5的坏挠,并且需要跳過頭部的三行芍躏,頭部三行是每個(gè)文件的年份基礎(chǔ)信息〗岛荩可以用以下代碼來讀取对竣,這里還用到了如何跳過文件頭部n行,以及打開非utf8編碼格式文件的小技巧榜配。

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

下載到的數(shù)據(jù)是從 公歷 1901年1月1日否纬,農(nóng)歷 1900年11月11日起,至 2100年12月31日芥牌,農(nóng)歷 2100年12月1日之間的200年的每天對(duì)照數(shù)據(jù)烦味。經(jīng)過編碼轉(zhuǎn)換后,重新存一個(gè)json或者pickle文件就可以直接拿來用了壁拉,速度也不慢谬俄。但是這個(gè)包含了所有日期數(shù)據(jù)的文件,json格式的話弃理,有6M多溃论,字典pickle格式也有2M多,顯然不利于傳播和重復(fù)使用痘昌。參考了網(wǎng)上一篇Java的農(nóng)歷轉(zhuǎn)換源碼钥勋,雖然使用的基礎(chǔ)數(shù)據(jù)存在錯(cuò)誤,但是算法非常精辟辆苔,所以就 拿來主義 了算灸。

香港天文臺(tái)原始數(shù)據(jù)處理

從原始數(shù)據(jù)處理轉(zhuǎn)換成可用于統(tǒng)計(jì)和進(jìn)一步處理的完整代碼如下:

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

上述代碼可返回一個(gè)每天日期信息字典的List,可再使用pandas對(duì)這些數(shù)據(jù)進(jìn)行編碼驻啤。編碼過程略菲驴。

年度數(shù)據(jù)編碼

每一整年的數(shù)據(jù)可用 20位的二進(jìn)制數(shù)表示

 0001 1000 1000 1000 1000
  • 第一部分,最左邊的前4位骑冗,只有0或1赊瞬,0表示當(dāng)年閏月為小月(即29天)先煎,1表示當(dāng)年閏月為大月(即30天),這個(gè)需要和最右側(cè)的最后4位結(jié)合使用巧涧。
  • 第二部分薯蝎,中間的12位,表示當(dāng)年農(nóng)歷年每月的大小月谤绳,0表示小月占锯,1表示大月,忽略閏月闷供,從左起第一位表示1月烟央。
  • 第三部分,最右側(cè)的最后4位歪脏,轉(zhuǎn)換成10進(jìn)制表示當(dāng)年的閏月月份疑俭,如果閏月不存在那就為 0。

舉例說明

2019年的年度編碼 43312

轉(zhuǎn)換成二進(jìn)制為

0000 1010 1001 0011 0000

位數(shù)不足左側(cè)補(bǔ)0婿失, 解析如下:

  • 先考慮中間12位表示月份钞艇,形成月份天數(shù)數(shù)組 [30, 29, 30, 29, 30, 29, 29, 30, 29, 29, 30, 30],此為農(nóng)歷1-12月的月份天數(shù)豪硅。
  • 再看最后4位哩照,等于0,表示當(dāng)年無閏月
  • 解析完成

2020年的年度編碼 31060

轉(zhuǎn)換成二進(jìn)制為

0000 0111 1001 0101 0100

位數(shù)不足左側(cè)補(bǔ)0懒浮, 解析如下:

  • 先考慮中間12位表示月份飘弧,形成月份天數(shù)數(shù)組 [29, 30, 30, 30, 30, 29, 29, 30, 29, 30, 29, 30],此為農(nóng)歷1-12月的月份天數(shù)砚著。
  • 再看最后4位次伶,轉(zhuǎn)換10進(jìn)制,等于4稽穆,表示當(dāng)年存在 閏4月
  • 查看最左側(cè)冠王,前4位,等于0舌镶,表示當(dāng)年閏4月為小月柱彻,只有29天
  • 在初始月份數(shù)組的 4月后插入 29,形成新的月份天數(shù)List [29, 30, 30, 30, 29, 30, 29, 29, 30, 29, 30, 29, 30]餐胀,這里包含13個(gè)月哟楷,含閏月的天數(shù)。
  • 解析完成

坑爹的網(wǎng)上農(nóng)歷說明

有些網(wǎng)站上提到每年的閏月應(yīng)該和實(shí)際月天數(shù)相同否灾,比如上述的例子卖擅,按照說明那么 2020年的農(nóng)歷4月和農(nóng)歷閏4月的天數(shù)是相同的,實(shí)際上是不同的,所以按照天文臺(tái)的數(shù)據(jù)進(jìn)行處理吧磨镶。

年度編碼解析代碼

def decode(year_code):
    """解析年度農(nóng)歷代碼函數(shù)
    
    Arguments:
        year_code {int} -- 從年度代碼數(shù)組中獲取的代碼整數(shù)
    
    Returns:
        [int] -- 當(dāng)前年度代碼解析以后形成的每月天數(shù)數(shù)組,已將閏月嵌入對(duì)應(yīng)位置健提,即有閏月的年份返回長(zhǎng)度為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

香港天文臺(tái)能下載到的只有1901年-2100年的數(shù)據(jù),作為一個(gè)強(qiáng)迫癥患者私痹,看到這個(gè)1901總是不爽脐嫂,在百度上查了一下,正好它支持1900年2050年的數(shù)據(jù)紊遵,所以手動(dòng)添加了1900的部分账千,形成了這個(gè)項(xiàng)目中的1900 - 2100年的完整農(nóng)歷數(shù)據(jù)。

為了加快運(yùn)算除了年度代碼暗膜,還存儲(chǔ)了每年的農(nóng)歷正月初一的公歷日期匀奏,這樣就用了20K就保存了200年的農(nóng)歷數(shù)據(jù)。

天干地支算法

天干地支是中國(guó)特有的一種歷法学搜,看起來很復(fù)雜娃善,實(shí)際上用簡(jiǎn)單的代碼就用打印出來

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 癸亥

對(duì)的,就是這么簡(jiǎn)單瑞佩,天干是10進(jìn)制聚磺,地支是12進(jìn)制,所以每一個(gè)序數(shù)對(duì)10取余數(shù)炬丸,得到天干瘫寝,每個(gè)序數(shù)對(duì)12取余數(shù)得到地支,相互組合就是該序數(shù)對(duì)應(yīng)的天干地支數(shù)稠炬。所以不用查表焕阿,用的時(shí)候直接打印一份就行了。

年度的天干地支最容易算酸纲,需要注意的是必須使用農(nóng)歷年份捣鲸,不能用公歷年份。查下百度得知 1900年為 庚子年闽坡,序號(hào) 36栽惶,所以用以下代碼可獲得當(dāng)前農(nóng)歷年的天干地支

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

總結(jié)

以上就是整個(gè)項(xiàng)目中最核心的部分,本質(zhì)上來說疾嗅,這個(gè)項(xiàng)目并不涉及復(fù)雜算法外厂,最核心的是使用二進(jìn)制來壓縮存儲(chǔ)年度數(shù)據(jù),相關(guān)的在Python中如何二進(jìn)制的基本用法代承,以及應(yīng)用案例我會(huì)另開文章來寫汁蝶。至于涉及到的其他,我覺得需要整理的基礎(chǔ)知識(shí)點(diǎn)也會(huì)陸續(xù)補(bǔ)充上來,作為分享以及自己的學(xué)習(xí)筆記掖棉。

計(jì)劃中逐步完成的相關(guān)文章清單:

  • Python中二進(jìn)制的使用 (撰寫中)
  • Python自定義類中的函數(shù)重載墓律,如何自定義打印字符串,自定義比較幔亥,以及加減運(yùn)算符(未開始)
  • 如何將自己的代碼讓 pip 能夠 install (未開始)
  • 其他想到的
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耻讽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子帕棉,更是在濱河造成了極大的恐慌针肥,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件香伴,死亡現(xiàn)場(chǎng)離奇詭異慰枕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)即纲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門具帮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人崇裁,你說我怎么就攤上這事匕坯。” “怎么了拔稳?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵葛峻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我巴比,道長(zhǎng)术奖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任轻绞,我火速辦了婚禮采记,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘政勃。我一直安慰自己唧龄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布奸远。 她就那樣靜靜地躺著既棺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪懒叛。 梳的紋絲不亂的頭發(fā)上丸冕,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音薛窥,去河邊找鬼胖烛。 笑死眼姐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的佩番。 我是一名探鬼主播众旗,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼趟畏!你這毒婦竟也來了逝钥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤拱镐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后持际,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沃琅,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蜘欲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了益眉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姥份,死狀恐怖郭脂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澈歉,我是刑警寧澤展鸡,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站埃难,受9級(jí)特大地震影響莹弊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涡尘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一忍弛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧考抄,春花似錦细疚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挑势,卻和暖如春镇防,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背潮饱。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工来氧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓啦扬,卻偏偏與公主長(zhǎng)得像中狂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扑毡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 如果不注意胃榕,大概很多人認(rèn)為“閏月”與“閏年”是一個(gè)意思,其實(shí)不是瞄摊,雖說只是一字之差勋又,所包含的意思卻相差很遠(yuǎn)。 “閏...
    雨落未驚風(fēng)閱讀 8,148評(píng)論 1 1
  • 在古代,算命一直都是一種精英文化行為惯驼。隨便舉出古代研究易學(xué)命理的人蹲嚣,都是些牛人,不是大文豪祟牲,就是大哲學(xué)家隙畜。比如...
    DUU_e50f閱讀 13,490評(píng)論 8 26
  • 1/30/18 Elaine 1. 情懷 格局與氣度。 “被滿足”和“能承擔(dān)”说贝。 ...
    Elaine旅晴閱讀 566評(píng)論 0 1
  • 琳琳在家炒大白菜燉脂渣议惰,又做米飯,炒豆腐干乡恕,下班到家一頭扎進(jìn)在廚房忙活今晚的晚飯换淆。媽媽從這么遠(yuǎn)的地方坐車給我們...
    文心怡筱雅閱讀 329評(píng)論 0 0
  • 在路上 文/高小蔚 《上》 在路上,朋友送我的CD的名字几颜。匯集了很多人的歌倍试,大都沒有聽過,卻是因了這三個(gè)字蛋哭,對(duì)它莫...
    高小蔚閱讀 280評(píng)論 6 7