從 Python 2.4 版開始,cx_Oracle 自身可以處理 DATE 和 TIMESTAMP 數據類型,將這些列的值映射到 Python 的 datetime 模塊的 datetime 對象中。因為 datetime 對象支持原位的運算操作,這可以帶來某些優勢。內置的時區支持和若干專用模塊使 Python 成為一台實時機器。由於有了 cx_Oracle 的映射機制,Python 和 Oracle 間的日期/時間數據類型轉換對開發人員是完全透明的。
Python 開發人員可能一開始會覺得 Oracle 的日期運算有點奇怪,但只需幾點提示,該算法就會變得清楚且合理。本系列的這部分內容將幫助您從 Oracle 和 Python 兩個角度來深入理解日期運算。二者均對日期/時間數據類型的處理提供豐富的支持,因此選擇哪個由編程人員決定。如果您傾向於將應用程序邏輯放在數據庫中,或者喜歡將日期/時間操作封裝在應用程序自身內部,Oracle 和 Python 的無縫集成會為您帶來最大的靈活性,同時編程工作量卻很少。
Oracle
Oracle 的特色在於為時區和日期運算提供頂級的支持。用於處理時間和日期的基本的 Oracle 數據類型包括:
- DATE — 日期和時間信息,包括世紀、年、月、日、小時、分和秒。這種類型的列支持的值范圍在公元前 4712 年 1 月 1 日到公元 9999 年 12 月 31 日之間。
- TIMESTAMP — DATE 數據類型的粒度精確到秒。TIMESTAMP 字段包含 DATE 中的全部信息,另外還包括指定精度的秒的小數(最多為 9 位)。默認精度為 6 位。
- TIMESTAMP WITH TIME ZONE — 除 TIMESTAMP 列中包含的信息外,此變體還包括時區偏移量,它是當地時間和 UTC(全球統一時間)之間的差值。精度屬性與上面相同。
- TIMESTAMP WITH LOCAL TIME ZONE — 與 TIMESTAMP WITH TIME ZONE 相對,此類型的值中不包含時區偏移量,而是由用戶的當地會話時區確定該值。
日期時間由許多字段組成,其數量由數據類型的粒度和變體決定。可以使用 EXTRACT 語句通過 SQL 查詢將這些字段提取出來。要了解有關數據類型中的可用字段和時間間隔的詳細信息,請參考 Oracle 數據庫 SQL 語言參考 的數據類型部分。我們來了解一下工作原理:
SQL> SELECT EXTRACT(YEAR FROM hire_date) FROM employees ORDER BY 1;
EXTRACT(YEARFROMHIRE_DATE)
----------------------------------------------------
1987
1987
?
2000
107 rows selected.
利用此方法和 Oracle 的日期運算,您還可以獲得兩個日期之間的時間間隔:
SQL> SELECT hire_date, SYSDATE, EXTRACT(YEAR FROM (SYSDATE-hire_date) YEAR TO MONTH) "Years" 2 FROM employees WHERE ROWNUM <= 5; HIRE_DATE SYSDATE Years ------------------ ------------------ ---------- 17-JUN-87 23-FEB-07 19 21-SEP-89 23-FEB-07 17 13-JAN-93 23-FEB-07 14 03-JAN-90 23-FEB-07 17 21-MAY-91 23-FEB-07 15 5 rows selected.
日期操作涉及的另一數據類型為 INTERVAL,它表示一段時間。在編寫本文時,Python 不支持將 INTERVAL 數據類型作為查詢的一部分返回。唯一的方法是使用 EXTRACT 從時間間隔中提取出所需的信息。盡管如此,包含返回 TIMESTAMP 類型的時間間隔的查詢仍然運轉良好。
INTERVAL 類型有兩個變體:
- INTERVAL YEAR TO MONTH — 存儲年和月的數量信息。年的精度可以手動指定。默認值是 (INTERVAL YEAR(2) TO MONTH)。
- Error — 這里提到的除 Warning 外的所有異常的基類。
- INTERVAL DAY TO SECOND — 在要求更高的精度時,此類型將天、小時、分和秒的信息存儲為一段時間。天和秒的精度都可以顯式指定,范圍為 0 到 9。默認值是 INTERVAL DAY(2) TO SECOND(6)。
SYSDATE+1 等於明天
現在看一下 Oracle 如何解決日期運算。在處理 datetime 列時,Oracle 認為 1 是指 1 天。這一方法確實非常直觀。如果您想使用更小的單位,需要使用除法:1 分鍾是 1/1440,這是因為 1 天有 60*24 分鍾;1 小時是 1/24;1 秒鍾是 1/86400,依此類推。
要查詢從現在起的 15 分鍾,使用 SELECT SYSDATE+15/1440 FROM dual。
格式化日期
Oracle 自身將日期顯示為字符串,如上面的示例所示。格式化取決於從環境中繼承的或顯式設置的參數。要查看您數據庫中的格式化參數,使用此查詢:
SQL> SELECT * FROM v$nls_parameters WHERE REGEXP_LIKE(parameter, 'NLS_(DATE|TIME).*');
PARAMETER VALUE
------------------------------ -------------------------------------
NLS_DATE_FORMAT RR/MM/DD
NLS_DATE_LANGUAGE POLISH
NLS_TIME_FORMAT HH24:MI:SSXFF
NLS_TIMESTAMP_FORMAT RR/MM/DD HH24:MI:SSXFF
NLS_TIME_TZ_FORMAT HH24:MI:SSXFF TZR
NLS_TIMESTAMP_TZ_FORMAT RR/MM/DD HH24:MI:SSXFF TZR
6 rows selected.
開發人員可以使用一組 Oracle 函數(TO_DATE、TO_TIMESTAMP、TO_TIMESTAMP_TZ、TO_YMINTERVAL、TO_DSINTERVAL)將字符值轉換為日期時間。TO_CHAR 函數用於反方向的轉換。注意這些轉換對於 Oracle 和 Python 間的轉換通常不是必要的,這是因為我們處理的類型在兩個方向都是可轉換的。盡管如此,Oracle 自身在格式化日期時間上仍然允許極大的靈活性:
SQL> SELECT TO_CHAR(TO_DATE('04-2007-07', 'DD-YYYY-MM'), 'DD/MM/YYYY') FROM dual;
TO_CHAR(TO_DATE('04-2007-07','DD-YYYY-MM'),'DD/MM/YYYY')
----------
04/07/2007
獲取當前的時間和時區信息同樣容易。引入了兩個新的格式化模型 TZH 和 TZM:
SQL> SELECT TO_CHAR(SYSTIMESTAMP, 'HH24:MI TZH:TZM') FROM dual; TO_CHAR(SYSTIMESTAMP,'HH24:MI TZH:TZM') ------------ 16:24 +01:00
要獲得可用格式化模型的完整列表,請參考 Oracle 數據庫 SQL 語言參考 的格式模型部分,該部分還有大量的用法示例。在很多情況下,您可能會覺得為當前會話設置永久的格式模型非常有用。可使用 ALTER SESSION 語句進行此設置:
Connected to: Oracle Database 10g Express Edition Release 10.2.0.1.0 - Production SQL> SELECT SYSDATE FROM dual; SYSDATE -------------- 23-FEB-07 SQL> ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'; Session altered. SQL> SELECT SYSDATE FROM dual; SYSDATE ------------------- 2007-02-23 17:50:15
Oracle 數據庫全球化支持指南 中詳細介紹了國家語言支持 (NLS) 參數的設置過程。Oracle 數據庫 SQL 語言參考 包含與此主題相關的更多信息。尤其要查閱日期時間/時間間隔運算和日期時間函數這兩部分,以了解日期運算和內置的日期函數的更多信息。
Python
Python 允許您在處理時間和日期時在低級和高級接口間進行自由選擇。為了充分利用 Python 的標准庫,我們將重點介紹 datetime 模塊,它同時也是日期/時間運算的基礎。該模塊有 5 個核心類型:date、time、datetime、timedelta 和 tzinfo。
>>> import datetime
>>> d = datetime.datetime.now()
>>> print d
2007-03-03 16:48:27.734000
>>> print type(d)
<type 'datetime.datetime'>
>>> print d.hour, d.minute, d.second
(16, 48, 27)
如上所述,datetime 對象精確到微秒,所公開的一組屬性與天、小時、秒等相對應。在 Python 中,了解某對象所擁有的屬性和方法的最快途徑是使用內置的 dir() 函數。關於 Python 標准庫的介紹也非常豐富,因此您可以隨時使用 help() 函數來了解該對象的簡要說明 — 大多數情況下這足以讓您立即入門。
>>> dir(datetime.datetime)
['__add__', '__class__', '__delattr__', '__doc__', '__eq__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__le__', '__lt__', '__ne__', '__new__', '__radd__', '__reduce__',
'__reduce_ex__', '__repr__', '__rsub__', '__setattr__', '__str__',
'__sub__', 'astimezone', 'combine', 'ctime', 'date', 'day', 'dst',
'fromordinal', 'fromtimestamp', 'hour', 'isocalendar', 'isoformat',
'isoweekday', 'max', 'microsecond', 'min', 'minute', 'month', 'now',
'replace', 'resolution', 'second', 'strftime', 'strptime', 'time',
'timetuple', 'timetz', 'today', 'toordinal', 'tzinfo', 'tzname',
'utcfromtimestamp', 'utcnow', 'utcoffset', 'utctimetuple',
'weekday', 'year']
>>> help(datetime.datetime.weekday)
Help on method_descriptor:
weekday(...)
Return the day of the week represented by the date.
Monday == 0 ... Sunday == 6
當只需要 datetime 的一個組件(date 或 time)時,您可以使用 datetime.datetime 對象的 date() 或 time() 方法來分別返回 datetime.date 或 datetime.time 對象。
Python 的日期運算
不需要手動計算日期之間的差值,因為 datetime 模塊已經通過 timedelta 對象支持這樣的運算。Timedelta 表示持續時間,它在內部存儲天、秒和微秒的數量。timedelta 屬性還提供毫秒、分、小時和周等信息。此內容是根據內部表示計算出的。支持的取值范圍是:-999999999 <= 天數 <= 999999999、0 <= 秒數 < 86400、0 <= 微秒數 < 1000000。注意 timedelta 對象由所需數量的字段進行表示。因此 timedelta(hours=1) 與 timedelta(0, 3600) 的輸出相同,即 0 天和 3600 秒。此處無毫秒,因為表示 1 小時不需要使用毫秒。
下面給出 Python 中最常用和支持的 datetime/timedelta 對象運算操作。
| 對象類型 |
運算 |
示例和相應結果 |
| datetime.timedelta |
td2 + td3 |
timedelta(minutes=10) + timedelta(hours=2) == timedelta(0, 7800) |
| td2 - td3 |
timedelta(weeks=3) - timedelta(hours=72) == timedelta(18) |
|
| td2 * n |
timedelta(minutes=5) * 5 == timedelta(0, 1500) |
|
| td2 / n |
timedelta(weeks=1) / 7 == timedelta(1) |
|
| datetime.date |
d2 + td |
date(2007, 12, 31) + timedelta(days=1) == date(2008, 1, 1) |
| d2 - td |
date(2007, 12, 31) - timedelta(weeks=52) == date(2007, 1, 1) |
|
| d1 - d2 |
date(2007, 1, 1) - date(2006, 1, 1) == timedelta(365) # 2004 年為閏年,有 366 天,Python 知道這一點: date(2005, 1, 1) - date(2004, 1, 1) == timedelta(366) |
|
| datetime.datetime |
d2 + td |
datetime(2007, 12, 31, 23, 59) + timedelta(minutes=1) == datetime(2008, 1, 1, 0, 0) |
| d2 - td |
datetime(2007, 12, 31) - timedelta(weeks=52, seconds=1) == datetime(2006, 12, 31, 23, 59, 59) |
|
| d1 - d2 |
datetime(2007, 1, 1) - datetime(2008, 1, 1) == timedelta(-365) |
不要企圖用獲取 2007 年 2 月 29 日來愚弄 Python,因為此日期不存在 — 解釋器將引發 ValueError 異常,並顯示“day is out of range for month”消息。您也可以隨時使用 datetime.datetime.today() 為當前的日期和時間獲取一個 datetime 對象。
如果您需要將現有的字符串分析為 date(time) 對象,可以使用 datetime 對象的 strptime() 方法。
>>> from datetime import datetime
>>> datetime.strptime("2007-12-31 23:59:59", "%Y-%m-%d %H:%M:%S")
datetime.datetime(2007, 12, 31, 23, 59, 59)
strptime 函數的格式字符串的完整列表包含在 Python 庫參考的時間模塊文檔中。
時區
Python 通過 datetime 模塊提供了對時區的支持,但它還沒有馬上進入自己的黃金階段,它只是為您的實現提供了一個框架。您需要創建您自己的、從 datetime.tzinfo 繼承的類,同時把需要的邏輯放在里面。Python 庫參考對此主題進行了大量介紹。
TO_DATE(Python)
利用 cx_Oracle 可以使 Oracle 和 Python 間的數據類型轉換變得完全不可見,這一點都不奇怪。包含 DATE 或 TIMESTAMP 列的查詢返回 datetime 對象。
我們來看一下 Oracle 的 INTERVAL 運算和 Python 的 timedelta 計算是否等同。
>>> import cx_Oracle
>>> db = cx_Oracle.connect('hr/hrpwd@localhost:1521/XE')
>>> cursor = db.cursor()
>>> r = cursor.execute("SELECT end_date-start_date diff, end_date,
start_date FROM job_history")
>>> for diff, end_date, start_date in cursor:
... print diff, '\t', (end_date-start_date).days
...
2018 2018
1497 1497
1644 1644
很好!它們的確匹配。
下面的示例說明,您可以自由決定將日期/時間邏輯放在數據庫端還是 Python 端。這一靈活性使得適合任意情形成為可能。我們來查一下 1998 年第 4 季度招聘的所有員工:
>>> Q4 = (datetime.date(1998, 10, 1), datetime.date(1998, 12, 31))
>>> r = cursor.execute("""
SELECT last_name||' '||first_name name, TO_CHAR(hire_date, 'YYYY-MM-DD')
FROM employees WHERE hire_date BETWEEN :1 AND :2 ORDER BY hire_date ASC """, Q4)
>>> for row in cursor:
... print row
...
('Sewall Sarath', '1998-11-03')
('Himuro Guy', '1998-11-15')
('Cambrault Nanette', '1998-12-09')
可以放心地使用 Python 的 datetime.date、datetime.time 和 datetime.datetime 對象作為綁定變量來查詢日期。您可以選擇所要求的粒度等級,但要記住,在處理秒的小數時,您需要指導 cx_Oracle 讓其明白傳遞了一個小數部分。普通查詢返回帶有秒的小數部分的完全有效的時間戳,這僅與使用綁定變量有關。當然,您可以結合使用 Python 的 strptime() 函數和 Oracle 的 TO_DATE(),但說實話,干嘛要自找麻煩呢?
我們來創建一個簡單表,結構如下:
CREATE TABLE python_tstamps (
ts TIMESTAMP(6)
);
下面的示例說明了這一問題。ts 有一個小數部分,它在 ts = datetime.datetime.now() 插入過程中被截斷了:
>>> ts = datetime.datetime.now()
>>> print ts
2007-03-10 20:01:24.046000
>>> cursor.execute("INSERT INTO python_tstamps VALUES(:t)", {'t':ts})
>>> db.commit()
SQL> SELECT ts FROM python_tstamps;
TS
-----------------------------------------------------------------------
10-MAR-07 08.01.56.000000 PM
10-MAR-07 08.12.02.109000 PM
解決方法是在准備和執行階段之間使用 setinputsizes() 方法。它指導 cx_Oracle 如何處理特定的綁定變量。它在內存中預先定義了一些區域以存儲這些對象。也可用它來為特定長度的字符串預先分配內存區域 — 它們應當以表示其長度的整數值給出。
現在我們來重新編寫插入操作:
>>> ts = datetime.datetime.now()
>>> print ts
2007-03-10 20:12:02.109000
>>> cursor.prepare("INSERT INTO python_tstamps VALUES(:t_val)")
>>> cursor.setinputsizes(t_val=cx_Oracle.TIMESTAMP)
cursor.setinputsizes(t_val=cx_Oracle.TIMESTAMP)
>>> cursor.execute(None, {'t_val':ts})
>>> db.commit()
SQL> SELECT ts FROM python_tstamps;
TS
----------------------------------------------------------------------------
10-MAR-07 08.01.56.000000 PM
10-MAR-07 08.12.02.109000 PM
總結
日期時間上下文中應當記住的有關 cx_Oracle 4.3 的重要內容:
- 不支持 INTERVAL 和 TIMESTAMP WITH (LOCAL) TIME ZONE
- 除非在 prepare() 和 execute() 間使用了 setinputsizes() 方法,作為綁定變量傳遞的日期/時間值的秒的小數部分將被截取。
- 對於時區支持,或者選擇使用標准庫的 datetime.tzinfo 類來編寫您自己的實現,或者選擇 SourceForge 中可用的 pytz 模塊。不過要做好充分准備,因為 WITH TIME ZONE 列類型和 Python datetime 對象間沒有平滑的轉換。
Python 標准庫提供用於日期/時間任務的其他工具,包括:
- 一個日歷模塊,用於以文本和 HTML 格式來顯示日歷,同時用於編寫您自己的導出實現
- 一個 timeit 模塊,用於對 Python 代碼進行概要描述和基准評測
- 一個 sched 模塊,它的功能與 Linux/Unix 下的 cron 實用程序相同
完成本教程后,您應當已經熟悉了負責 Oracle 和 Python 日期處理的概念。熟悉了無縫集成的 datetime 數據類型后,您現在可以將它們注入到您的可感知日歷的應用程序中,把 Python 放到您的開發工具箱中。
馬上就需要日歷嗎?導入日歷;輸出 calendar.calendar(2007)。
