1. 程式人生 > >整理總結 python 中時間日期類資料處理與型別轉換(含 pandas)

整理總結 python 中時間日期類資料處理與型別轉換(含 pandas)

我自學 python 程式設計並付諸實戰,迄今三個月。 pandas可能是我最高頻使用的庫,基於它的易學、實用,我也非常建議朋友們去嘗試它。——尤其當你本身不是程式設計師,但多少跟表格或資料打點交道時,pandasexcelVBA 簡單優雅多了。

pandas 善於處理表格類資料,而我日常接觸的資料天然帶有時間日期屬性,比如使用者行為日誌、爬蟲爬取到的內容文字等。於是,使用 pandas 也就意味著相當頻繁地與時間日期資料打交道。這篇筆記將從我的實戰經驗出發,整理我常用的時間日期類資料處理、型別轉換的方法。

與此相關的三個庫如下。


import time
import datetime
import pandas as pd

其中,timedatetime都是 python 自帶的,pandas則是一個第三方庫。換言之,前兩者無需額外安裝,第三方庫則需要通過pip install pandas命令列自行安裝。如何檢查自己是否安裝了某個庫,如何安裝它,又如何檢視和更新版本,對新手來說是一個比較大的話題,也是基礎技能,值得另外整理一篇筆記,就不在這裡佔篇幅了。當然,如果你不想自己本地折騰,也可電腦瀏覽器訪問https://xue.cn 這樣的網站,網頁上直接寫程式碼並執行它們。

一、time模組

time模組,我最常用到的功能就三個:

  • 指定程式休眠;
  • 獲取當前時間戳;
  • 時間戳與本地時間的互相轉換

time.sleep(s)
指定程式休眠 s 秒

指定程式休眠時間,通常是在長時間執行的迴圈任務中進行。比如爬蟲任務,控制讀取網頁的時間間隔;自迴圈任務的時間間隔,呼叫瀏覽器開啟網頁的時間間隔等等。

先用兩個列印語句,輔助觀察和理解time.sleep()的效果:

print(datetime.datetime.now())
time.sleep(5)
print(datetime.datetime.now())

至於長時間執行的迴圈任務,我通常是把核心業務邏輯封裝好,利用jupyter lab自帶的多程序特定,建一個 notebook 放入下面這個函式去持續執行。


def repeat_myself(how_many_times = 10):
    print('--------',how_many_times,'----------')
    # 被封裝的核心程式碼
    your_main_def() 

    # 自迴圈減 1 ;如果剩餘次數是0,則終止自迴圈
    how_many_times += -1
    if how_many_times == 0:
        print(datetime.datetime.now(),'stop it.')
        return

    # 每次呼叫設定一個時間間隔
    print(datetime.datetime.now(),'have a rest')
    how_long = random.randint(30,120)
    time.sleep(how_long)
    return repeat_myself(how_many_times)

repeat_myself(12)

time.time()獲取當前時間戳

最初我認為無需急於掌握時間戳這個技能點,但實戰中,1) 我的爬蟲有時爬取到時間戳型別的資料,為了易讀,要把它轉換為正常人能看懂的方式;2) 使用 mysql 時我關心儲存所佔用的空間以及讀寫效率,並獲知一個時間資料存成 char 不如時間戳更節省空間。好吧,實戰需要,那麼趕緊掌握起這個小技能吧。

先了解下如何生成時間戳。通過time.time()得到的時間戳,是一個有著10位整數位 + 6位小數位的浮點數,可根據需要簡單運算轉換為需要的 10、13、16 位整數時間戳。

# 獲取當前時間戳

# 值是 1569642653.1041737 ,float
a = time.time()
# 1569642653,得到 10位時間戳,int
b = int(a)
# 1569642653104,得到 13位時間戳,int
c = int(a * 1000)
# 1569642653104173,得到 16位時間戳,int
d = int(a * 1000000)

接下來,瞭解一下時間戳和人類易讀的時間之間的轉換。

時間戳與人類易讀的時間互相轉換

如上面所示,時間戳是一個floatint型別的數值,至少有 10 位整數。把時間戳轉換為人類易讀的時間,用到的是localtime(),與其相反的是mktime()能把人類易讀的時間轉換為時間戳。


# 時間戳轉換為人類易讀的時間
# 結果是:time.struct_time(tm_year=2019, tm_mon=9, tm_mday=28, tm_hour=12, tm_min=12, tm_sec=1, tm_wday=5, tm_yday=271, tm_isdst=0)
# 資料型別是 time.struct_time
e = time.localtime(a)
f = time.localtime(b)
g = time.localtime(c//1000)
h = time.localtime(d//1000000)

# 人類易讀的時間轉換為時間戳
# 結果是:1569643921.0,float
i = time.mktime(e)
j = time.mktime(f)
k = time.mktime(g)
l = time.mktime(h)

type()檢查,localtime() 得到的結果,是 time.struct_time 型別,直觀可見這個型別對人類依然不是最友好的。最友好的表達將用到 strftimestrptime 這兩個方法,處理 time.struct_timestring字串 兩個型別的互換。


# 把 struct_time 轉換為指定格式的字串
# '2019-09-28 12:12:01 Saturday'
good = time.strftime("%Y-%m-%d %H:%M:%S %A", e)

# 把字串轉換為 struct_time
# 結果是:time.struct_time(tm_year=2019, tm_mon=9, tm_mday=28, tm_hour=12, tm_min=12, tm_sec=1, tm_wday=5, tm_yday=271, tm_isdst=-1)
nice = time.strptime(good,"%Y-%m-%d %H:%M:%S %A")

在我的筆記中,僅整理總結自己常用的方法,至於我自己從未用到或很少用到的方法,並不羅列其中。如有小夥伴希望系統完整地瞭解,可直接搜:time site:python.org 或點選訪問官方文件 能檢視完整說明。

二、datetime 模組

datetime獲取到的時間資料是非常易讀的,在和人互動時,比 time 更好用一些。我通常把 datetime 用於以下 2 個場景。

場景A:log時間戳,列印資訊監控程式碼執行情況

新手寫程式碼,變相就是寫bug,以我自己來說,使用不熟模組或寫新業務時,寫程式碼和除錯修復錯誤,佔用時間常常各半。採用 jupter lab的 notebook,讓寫程式碼和除錯方便許多,但依然需要 print() 列印資訊方便監控程式碼執行情況。比如下方這個程式碼片段:

# 顯示效果:2019-09-28 12:44:36.574576 df_rlt ...
print(datetime.datetime.now(),'df_rlt ...')
for one in df_rlt.values:
    print(datetime.datetime.now(),one,'for circle ...')
    try:
        sql_insert = 'INSERT INTO questions(q_id,q_title,q_description,q_keywords,q_people,q_pageview,time) VALUES( "'\
            + str(quesition_id) + '", "' + str(one[0])+ '", "' + str(one[1]) + '", "' + str(one[2]) + '", "' \
            + str(one[3]) + '", "' + str(one[4]) + '", "' + str(datetime.datetime.now()) + '");' 
        sql_update = 'update topic_monitor SET is_title="1" where question_id = "' + str(quesition_id) + '";'
        cursor.execute(sql_insert)
        cursor.execute(sql_update)
        conn.commit()
        print(datetime.datetime.now(),'sql_insert ...')
    except:
        print(datetime.datetime.now(),'sql_insert error...')
        continue

場景B:檔名時間戳,檔名中增加當前日期

檔名中增加當前日期作為引數,既避免檔案相互覆蓋(比如資料每天更新,每天匯出一次),也方便直觀地檢視檔案版本。當然啦,如果處理的是超級頻繁匯出的檔案,精確到天並不滿足需求,可自行精確到時分秒,或直接用int(time.time())時間戳作為檔名中的引數。

# 效果:'d:/out_put/xuecn_comments_statistics_2019-09-28.xlsx'
comms_file = output_path + 'xuecn_comments_statistics_' + str(datetime.datetime.now())[:10] + '.xlsx'

直接搜:datetime site:python.org 或者點選訪問 python 官方文件檢視超多方法說明。

與官方文件對比,我已經用到的知識點真是九牛一毛。不過也沒關係,從需要和興趣出發就好,沒必要硬著頭皮把自己打造成移動字典,很多方法呢都是用多了自然記住了,無需反覆死記硬背。

三、pandas 中的時間處理

我寫這篇筆記,本就是奔著精進 pandas 來的,前面花了很大篇幅先整理了timedatetime這些基礎功,現在進入重頭戲,即 pandas 中與時間相關的時間處理。

前面兩個部分舉例,處理的均是單個值,而在處理 pandasdataframe 資料型別時,事情會複雜一點,但不會複雜太多。我在實戰中遇到的情況,總結起來無非兩類:

  • 資料型別的互換
  • 索引與列的互換

需要留意的是,資料型別應該靠程式判斷,而非我們人肉判斷。python pandas 判斷資料型別,常用type()df.info() 這兩個方法。

首先,我們構造一個簡單的資料示例 df

構造這個例項,只是為了方便後面的展開。構造一個 dataframe 的方法有非常多。這裡就不展開了。

import random
df = pd.DataFrame({
    'some_data' : [random.randint(100,999) for i in range(1,10)],
    'a_col' : '2019-07-12',
    'b_col' : datetime.datetime.now().date(),
    'c_col' : time.time()},
    index=range(1,10))

然後,我們逐項檢視它的資料型別

剛學著用pandas經常會因為想當然地認為某個物件是某個資料型別,從而程式碼執行報錯。後來學乖,特別留心資料型別。

某個資料是什麼型別,如何檢視,某個方法對資料型別有什麼要求,如何轉換資料型別,這些都是實戰中特別關心的。

# pandas.core.frame.DataFrame
type(df)
# pandas.core.series.Series
type(df['some_data'])
# numpy.ndarray
type(df['some_data'].values)
# numpy.int64
type(df['some_data'].values[0])
# str
type(df['a_col'].values[0])
# datetime.date
type(df['b_col'].values[0])
# numpy.float64
type(df['c_col'].values[0])

df.info()
"""
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9 entries, 1 to 9
Data columns (total 4 columns):
some_data    9 non-null int64
a_col        9 non-null object
b_col        9 non-null object
c_col        9 non-null float64
dtypes: float64(1), int64(1), object(2)
memory usage: 420.0+ bytes
"""

為什麼要轉換資料型別,有什麼用途

為什麼要把時間日期之類的資料轉換為 pandas 自帶的 datetime64 型別呢?這當然不是強迫症整潔癖,而且即便不做轉換也不會帶來任何報錯。

最重要的原因是,資料分析將會高頻用到基於時間的統計,比如:每天有多少使用者註冊、登入、付費、留言……產品運營通常按日統計,把dt.date改成dt.weekdt.monthdt.hour就能輸出周統計、月統計、分時統計……當然官方文件介紹的方法還有更多,我提到的僅是自己高頻使用的方法。


df.groupby(df['c_col'].dt.date).some_data.agg('sum')

次要的原因是,輸出資料到 excel 表格中發給其它同事時,咱們還是得考慮檔案的易讀、簡潔吖。比如,時間戳得轉換為人能看懂的文字,比如僅顯示日期,無需把後面時分秒之類的冗餘資料也顯示出來等等。

通過不同方式拿到的資料型別,通常相互之間並不一致,而我們想要使用某些方法提高生產力,必須遵循該方法所要求的資料型別。於是資料型別轉換就成了剛需。

如何轉換為 pandas 自帶的 datetime 型別

在上方示例中,肉眼可見 a_colb_col 這兩列都是日期,但 a_col 的值其實是string 字串型別,b_col的值是datatime.date型別。想要用pandas 的按時間屬性分組的方法,前提是轉換為 pandas 自己的 datetime型別。

轉換方法是一致的:

# 字串型別轉換為 datetime64[ns] 型別
df['a_col'] = pd.to_datetime(df['a_col'])
# datetime.date 型別轉換為 datetime64[ns] 型別
df['b_col'] = pd.to_datetime(df['b_col'])
# 時間戳(float) 型別轉換為 datetime64[ns] 型別
df['c_col'] = pd.to_datetime(df['c_col'].apply(lambda x:time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(x))))

# 檢視轉換後的屬性
df.info()
"""
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9 entries, 1 to 9
Data columns (total 4 columns):
some_data    9 non-null int64
a_col        9 non-null datetime64[ns]
b_col        9 non-null datetime64[ns]
c_col        9 non-null datetime64[ns]
dtypes: datetime64[ns](3), int64(1)
memory usage: 420.0 bytes
"""

其中,難點是 c_col 這列。其實不難,只是幾個巢狀,顯得有點複雜而已:

  1. y = time.localtime(x),把 x 從時間戳(10個整數位+6個小數位的那串數字)型別轉換為struct_time
  2. z = time.strftime('%Y-%m-%d %H:%M:%S',y) 把上一步得到的 struct_time 轉換為 字串
  3. lambda x:z 匿名函式,輸入一個值x,得到字串z
  4. df['c_col'].apply() 對整列每個值做上述匿名函式所定義的運算,完成後整列值都是字串型別
  5. pd.to_datetime() 把整列字串轉換為 pandas 的 datetime 型別,再重新賦值給該列(相當於更新該列)

我其實非常希望有個過來人告訴我,這個知識點用的頻繁嗎,在什麼時期是否應該掌握?於是我自己寫的筆記,通常都會留意分享自己實戰過來的這個判斷。當然啦,每個人實戰的方向不太一樣,大家可作參考,無需完全照搬。具體說來:

  • 第 1、2 步是第一部分 time 模組總結到基礎技能。
  • 第 3 步的匿名函式 lambda 是相當風騷的知識點,xue.cn 《自學是門手藝》有一節專門講到它,建議掌握。
  • 第 4 步結合匿名函式lambda,是對 dataframe 整列進行統一操作的重要技能點,多用幾次就熟練了。
  • 第 5 步 無需死記硬背。為啥我總說 pandas 易學好用呢?因為它的很多方法,都能直接見文生義,幾乎沒有記憶負擔。

關於時間日期處理的pandas 官方文件篇幅也挺長的,沒中文版,大家想要系統瞭解,直接點開查閱吧~

關於索引與列的互換

不管何種原因導致,通常使用 pandas 時會經常對索引與列進行互換。比如把某列時間資料設為索引,把時間索引設為一列……這些操作並沒有額外的特別之處,都統一在pandas 如何進行索引與列的互換 這個技能點之下。限於篇幅,我這裡就不展開啦。不過索引與列的轉換是高頻操作,值得另寫一篇筆記。

有一點反覆強調都不過為,即,我的筆記僅記錄自己實戰中頻繁遇到的知識技能,並非該模組全貌。如需系統掌握或遇到筆記之外的疑問,請善用搜索技能喲:你的關鍵詞們 site:python.org

如果我的整理帶給你幫助,請點個贊鼓勵我繼續分享。如需勘誤請留言,或挪步到我的 github 提issues