1. 程式人生 > 實用技巧 >Python爬蟲技術--基礎篇--內建模組datetime和collections

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_dttokyo_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串起來並組成一個邏輯上的dictChainMap本身也是一個dict,但是查詢的時候,會按照順序在內部的dict依次查詢

什麼時候使用ChainMap最合適?舉個例子:應用程式往往都需要傳入引數,引數可以通過命令列傳入,可以通過環境變數傳入,還可以有預設引數。我們可以用ChainMap實現引數的優先順序查詢,即先查命令列引數,如果沒有傳入,再查環境變數,如果沒有,就使用預設引數

下面的程式碼演示瞭如何查詢usercolor這兩個引數:

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模組提供了一些有用的集合類,可以根據需要選用。