一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

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

 宇哥工作室 2020-09-24

背景

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

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

項目使用說明

先上項目吧,想直接使用的同學(xué),拿來就能用了 ZhDate GitHub主頁,對開發(fā)過程有興趣的請繼續(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年正月初一 的日期對象
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)歷日期對象

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)歷對象
new_zhdate2 = ZhDate(2019, 1, 1) - ZhDate(2018, 1, 1) #兩個zhdate對象相減得到兩個農(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)歷對象

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

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

核心算法

重要的事情說三遍

農(nóng)歷不是算出來的,是天文臺觀測出來的
農(nóng)歷不是算出來的,是天文臺觀測出來的
農(nóng)歷不是算出來的,是天文臺觀測出來的

所以也想做農(nóng)歷功能的同學(xué)就不要費心去學(xué)什么農(nóng)歷算法了,浪費了我三天時間也沒看懂到底是怎么計算的。
目前通用的也是比較準(zhǔn)確的,可下載的農(nóng)歷陽歷對照數(shù)據(jù)是 香港天文臺農(nóng)歷對照表(文字版), 可下載txt格式的農(nóng)歷對照數(shù)據(jù)。寫了一個簡單的爬蟲,將所有txt文件下載下來。注意獲得到的txt是Big5的,并且需要跳過頭部的三行,頭部三行是每個文件的年份基礎(chǔ)信息??梢杂靡韵麓a來讀取,這里還用到了如何跳過文件頭部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年的每天對照數(shù)據(jù)。經(jīng)過編碼轉(zhuǎn)換后,重新存一個json或者pickle文件就可以直接拿來用了,速度也不慢。但是這個包含了所有日期數(shù)據(jù)的文件,json格式的話,有6M多,字典pickle格式也有2M多,顯然不利于傳播和重復(fù)使用。參考了網(wǎng)上一篇Java的農(nóng)歷轉(zhuǎn)換源碼,雖然使用的基礎(chǔ)數(shù)據(jù)存在錯誤,但是算法非常精辟,所以就 拿來主義 了。

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

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

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對這些數(shù)據(jù)進行編碼。編碼過程略。

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

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

 0001 1000 1000 1000 1000
  • 第一部分,最左邊的前4位,只有0或1,0表示當(dāng)年閏月為小月(即29天),1表示當(dāng)年閏月為大月(即30天),這個需要和最右側(cè)的最后4位結(jié)合使用。
  • 第二部分,中間的12位,表示當(dāng)年農(nóng)歷年每月的大小月,0表示小月,1表示大月,忽略閏月,從左起第一位表示1月。
  • 第三部分,最右側(cè)的最后4位,轉(zhuǎn)換成10進制表示當(dāng)年的閏月月份,如果閏月不存在那就為 0。

舉例說明

2019年的年度編碼 43312

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

0000 1010 1001 0011 0000

位數(shù)不足左側(cè)補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)換成二進制為

0000 0111 1001 0101 0100

位數(shù)不足左側(cè)補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進制,等于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個月,含閏月的天數(shù)。
  • 解析完成

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

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

年度編碼解析代碼

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

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

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

天干地支算法

天干地支是中國特有的一種歷法,看起來很復(fù)雜,實際上用簡單的代碼就用打印出來

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進制,所以每一個序數(shù)對10取余數(shù),得到天干,每個序數(shù)對12取余數(shù)得到地支,相互組合就是該序數(shù)對應(yīng)的天干地支數(shù)。所以不用查表,用的時候直接打印一份就行了。

年度的天干地支最容易算,需要注意的是必須使用農(nóng)歷年份,不能用公歷年份。查下百度得知 1900年為 庚子年,序號 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é)

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

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

  • Python中二進制的使用 (撰寫中)
  • Python自定義類中的函數(shù)重載,如何自定義打印字符串,自定義比較,以及加減運算符(未開始)
  • 如何將自己的代碼讓 pip 能夠 install (未開始)
  • 其他想到的

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    亚洲国产av精品一区二区| 厕所偷拍一区二区三区视频| 欧美人妻盗摄日韩偷拍| 日本本亚洲三级在线播放| 国产精品国产亚洲看不卡| 人妻精品一区二区三区视频免精| 91欧美日韩精品在线| 国产日产欧美精品大秀| 日韩精品在线观看一区| 欧美日不卡无在线一区| 欧美大黄片在线免费观看| 最近日韩在线免费黄片| 亚洲国产成人一区二区在线观看| 99福利一区二区视频| 欧美精品在线播放一区二区| 精品日韩视频在线观看| 91欧美日韩精品在线| 九九热在线免费在线观看| 人妻熟女中文字幕在线| 国产一区二区三区色噜噜| 日本高清不卡在线一区| 欧美精品在线播放一区二区| 国产精品久久精品国产| 99热在线播放免费观看| 女生更色还是男生更色| 久久夜色精品国产高清不卡| 精品久久久一区二区三| 成人午夜在线视频观看| 日本东京热视频一区二区三区| 国产成人精品国产成人亚洲| 欧美日韩精品人妻二区三区| 国产丝袜极品黑色高跟鞋| 日韩精品一区二区三区av在线 | 成人精品日韩专区在线观看 | 办公室丝袜高跟秘书国产 | 欧美丝袜诱惑一区二区| 国产精品成人一区二区三区夜夜夜| 欧美日韩精品综合在线| 免费久久一级欧美特大黄孕妇| 亚洲国产成人精品一区刚刚 | 国产欧美一区二区三区精品视|