背景
日常用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 直接安裝
或從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ù)表示
- 第一部分,最左邊的前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)換成二進制為
位數(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)換成二進制為
位數(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 (未開始)
- 其他想到的
|