背景
日常用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 (未開始)
- 其他想到的