Python爬蟲技術--基礎篇--內建模組datetime和collections
1.datetime
datetime是Python處理日期和時間的標準庫。
獲取當前日期和時間
我們先看如何獲取當前日期和時間:
>>> from datetime import datetime
>>> now = datetime.now() # 獲取當前datetime
>>> print(now)
2015-05-18 16:28:07.198690
>>> print(type(now))
<class 'datetime.datetime'>
注意到datetime
是模組,datetime
模組還包含一個datetime
from datetime import datetime
匯入的才是datetime
這個類。
如果僅匯入import datetime
,則必須引用全名datetime.datetime
。
datetime.now()
返回當前日期和時間,其型別是datetime
。
獲取指定日期和時間
要指定某個日期和時間,我們直接用引數構造一個datetime
:
>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時間建立datetime
>>> print(dt)
2015-04-19 12:20:00
datetime轉換為timestamp
在計算機中,時間實際上是用數字表示的。我們把1970年1月1日 00:00:00 UTC+00:00時區的時刻稱為epoch time,記為0
(1970年以前的時間timestamp為負數),當前時間就是相對於epoch time的秒數,稱為timestamp。
你可以認為:
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00
對應的北京時間是:
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00
可見timestamp的值與時區毫無關係,因為timestamp一旦確定,其UTC時間就確定了,轉換到任意時區的時間也是完全確定的,這就是為什麼計算機儲存的當前時間是以timestamp表示的,因為全球各地的計算機在任意時刻的timestamp都是完全相同的(假定時間已校準)。
把一個datetime
型別轉換為timestamp只需要簡單呼叫timestamp()
方法:
>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時間建立datetime
>>> dt.timestamp() # 把datetime轉換為timestamp
1429417200.0
注意Python的timestamp是一個浮點數,整數位表示秒。
某些程式語言(如Java和JavaScript)的timestamp使用整數表示毫秒數,這種情況下只需要把timestamp除以1000就得到Python的浮點表示方法。
timestamp轉換為datetime
要把timestamp轉換為datetime
,使用datetime
提供的fromtimestamp()
方法:
>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00
注意到timestamp是一個浮點數,它沒有時區的概念,而datetime是有時區的。上述轉換是在timestamp和本地時間做轉換。
本地時間是指當前作業系統設定的時區。例如北京時區是東8區,則本地時間:
2015-04-19 12:20:00
實際上就是UTC+8:00時區的時間:
2015-04-19 12:20:00 UTC+8:00
而此刻的格林威治標準時間與北京時間差了8小時,也就是UTC+0:00時區的時間應該是:
2015-04-19 04:20:00 UTC+0:00
timestamp也可以直接被轉換到UTC標準時區的時間:
>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t)) # 本地時間
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t)) # UTC時間
2015-04-19 04:20:00
str轉換為datetime
很多時候,使用者輸入的日期和時間是字串,要處理日期和時間,首先必須把str轉換為datetime。轉換方法是通過datetime.strptime()
實現,需要一個日期和時間的格式化字串:
>>> from datetime import datetime
>>> cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')
>>> print(cday)
2015-06-01 18:19:59
字串'%Y-%m-%d %H:%M:%S'
規定了日期和時間部分的格式。詳細的說明請參考Python文件。
注意轉換後的datetime是沒有時區資訊的。
datetime轉換為str
如果已經有了datetime物件,要把它格式化為字串顯示給使用者,就需要轉換為str,轉換方法是通過strftime()
實現的,同樣需要一個日期和時間的格式化字串:
>>> from datetime import datetime
>>> now = datetime.now()
>>> print(now.strftime('%a, %b %d %H:%M'))
Mon, May 05 16:28
datetime加減
對日期和時間進行加減實際上就是把datetime往後或往前計算,得到新的datetime。加減可以直接用+
和-
運算子,不過需要匯入timedelta
這個類:
>>> from datetime import datetime, timedelta
>>> now = datetime.now()
>>> now
datetime.datetime(2015, 5, 18, 16, 57, 3, 540997)
>>> now + timedelta(hours=10)
datetime.datetime(2015, 5, 19, 2, 57, 3, 540997)
>>> now - timedelta(days=1)
datetime.datetime(2015, 5, 17, 16, 57, 3, 540997)
>>> now + timedelta(days=2, hours=12)
datetime.datetime(2015, 5, 21, 4, 57, 3, 540997)
可見,使用timedelta
你可以很容易地算出前幾天和後幾天的時刻。
本地時間轉換為UTC時間
本地時間是指系統設定時區的時間,例如北京時間是UTC+8:00時區的時間,而UTC時間指UTC+0:00時區的時間。
一個datetime
型別有一個時區屬性tzinfo
,但是預設為None
,所以無法區分這個datetime
到底是哪個時區,除非強行給datetime
設定一個時區:
>>> from datetime import datetime, timedelta, timezone
>>> tz_utc_8 = timezone(timedelta(hours=8)) # 建立時區UTC+8:00
>>> now = datetime.now()
>>> now
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012)
>>> dt = now.replace(tzinfo=tz_utc_8) # 強制設定為UTC+8:00
>>> dt
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))
如果系統時區恰好是UTC+8:00,那麼上述程式碼就是正確的,否則,不能強制設定為UTC+8:00時區。
時區轉換
我們可以先通過utcnow()
拿到當前的UTC時間,再轉換為任意時區的時間:
# 拿到UTC時間,並強制設定時區為UTC+0:00:
>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
>>> print(utc_dt)
2015-05-18 09:05:12.377316+00:00
# astimezone()將轉換時區為北京時間:
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2015-05-18 17:05:12.377316+08:00
# astimezone()將轉換時區為東京時間:
>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2015-05-18 18:05:12.377316+09:00
# astimezone()將bj_dt轉換時區為東京時間:
>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2015-05-18 18:05:12.377316+09:00
時區轉換的關鍵在於,拿到一個datetime
時,要獲知其正確的時區,然後強制設定時區,作為基準時間。
利用帶時區的datetime
,通過astimezone()
方法,可以轉換到任意時區。
注:不是必須從UTC+0:00時區轉換到其他時區,任何帶時區的datetime
都可以正確轉換,例如上述bj_dt
到tokyo_dt
的轉換。
小結
datetime
表示的時間需要時區資訊才能確定一個特定的時間,否則只能視為本地時間。
如果要儲存datetime
,最佳方法是將其轉換為timestamp再儲存,因為timestamp的值與時區完全無關。
2.collections
collections是Python內建的一個集合模組,提供了許多有用的集合類。
namedtuple
我們知道tuple
可以表示不變集合,例如,一個點的二維座標就可以表示成:
>>> p = (1, 2)
但是,看到(1, 2)
,很難看出這個tuple
是用來表示一個座標的。
定義一個class又小題大做了,這時,namedtuple
就派上了用場:
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1
>>> p.y
2
namedtuple
是一個函式,它用來建立一個自定義的tuple
物件,並且規定了tuple
元素的個數,並可以用屬性而不是索引來引用tuple
的某個元素。
這樣一來,我們用namedtuple
可以很方便地定義一種資料型別,它具備tuple的不變性,又可以根據屬性來引用,使用十分方便。
可以驗證建立的Point
物件是tuple
的一種子類:
>>> isinstance(p, Point)
True
>>> isinstance(p, tuple)
True
類似的,如果要用座標和半徑表示一個圓,也可以用namedtuple
定義:
# namedtuple('名稱', [屬性list]):
Circle = namedtuple('Circle', ['x', 'y', 'r'])
deque
使用list
儲存資料時,按索引訪問元素很快,但是插入和刪除元素就很慢了,因為list
是線性儲存,資料量大的時候,插入和刪除效率很低。
deque是為了高效實現插入和刪除操作的雙向列表,適合用於佇列和棧:
>>> from collections import deque
>>> q = deque(['a', 'b', 'c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q
deque(['y', 'a', 'b', 'c', 'x'])
deque
除了實現list的append()
和pop()
外,還支援appendleft()
和popleft()
,這樣就可以非常高效地往頭部新增或刪除元素。
defaultdict
使用dict
時,如果引用的Key不存在,就會丟擲KeyError
。如果希望key不存在時,返回一個預設值,就可以用defaultdict
:
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A')
>>> dd['key1'] = 'abc'
>>> dd['key1'] # key1存在
'abc'
>>> dd['key2'] # key2不存在,返回預設值
'N/A'
注意預設值是呼叫函式返回的,而函式在建立defaultdict
物件時傳入。
除了在Key不存在時返回預設值,defaultdict
的其他行為跟dict
是完全一樣的。
OrderedDict
使用dict
時,Key是無序的。在對dict
做迭代時,我們無法確定Key的順序。
如果要保持Key的順序,可以用OrderedDict
:
>>> from collections import OrderedDict
>>> d = dict([('a', 1), ('b', 2), ('c', 3)])
>>> d # dict的Key是無序的
{'a': 1, 'c': 3, 'b': 2}
>>> od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> od # OrderedDict的Key是有序的
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
注意,OrderedDict
的Key會按照插入的順序排列,不是Key本身排序:
>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys()) # 按照插入的Key的順序返回
['z', 'y', 'x']
OrderedDict
可以實現一個FIFO(先進先出)的dict,當容量超出限制時,先刪除最早新增的Key:
from collections import OrderedDict
class LastUpdatedOrderedDict(OrderedDict):
def __init__(self, capacity):
super(LastUpdatedOrderedDict, self).__init__()
self._capacity = capacity
def __setitem__(self, key, value):
containsKey = 1 if key in self else 0
if len(self) - containsKey >= self._capacity:
last = self.popitem(last=False)
print('remove:', last)
if containsKey:
del self[key]
print('set:', (key, value))
else:
print('add:', (key, value))
OrderedDict.__setitem__(self, key, value)
ChainMap
ChainMap
可以把一組dict
串起來並組成一個邏輯上的dict
。ChainMap
本身也是一個dict,但是查詢的時候,會按照順序在內部的dict依次查詢。
什麼時候使用ChainMap
最合適?舉個例子:應用程式往往都需要傳入引數,引數可以通過命令列傳入,可以通過環境變數傳入,還可以有預設引數。我們可以用ChainMap
實現引數的優先順序查詢,即先查命令列引數,如果沒有傳入,再查環境變數,如果沒有,就使用預設引數。
下面的程式碼演示瞭如何查詢user
和color
這兩個引數:
from collections import ChainMap
import os, argparse
# 構造預設引數:
defaults = {
'color': 'red',
'user': 'guest'
}
# 構造命令列引數:
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = { k: v for k, v in vars(namespace).items() if v }
# 組合成ChainMap:
combined = ChainMap(command_line_args, os.environ, defaults)
# 列印引數:
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])
沒有任何引數時,打印出預設引數:
$ python3 use_chainmap.py
color=red
user=guest
當傳入命令列引數時,優先使用命令列引數:
$ python3 use_chainmap.py -u bob
color=red
user=bob
同時傳入命令列引數和環境變數,命令列引數的優先順序較高:
$ user=admin color=green python3 use_chainmap.py -u bob
color=green
user=bob
Counter
Counter
是一個簡單的計數器,例如,統計字元出現的個數:
>>> from collections import Counter
>>> c = Counter()
>>> for ch in 'programming':
... c[ch] = c[ch] + 1
...
>>> c
Counter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1})
>>> c.update('hello') # 也可以一次性update
>>> c
Counter({'r': 2, 'o': 2, 'g': 2, 'm': 2, 'l': 2, 'p': 1, 'a': 1, 'i': 1, 'n': 1, 'h': 1, 'e': 1})
Counter
實際上也是dict
的一個子類,上面的結果可以看出每個字元出現的次數。
小結
collections
模組提供了一些有用的集合類,可以根據需要選用。