datetime
datetime
是Python處理日期和時間的標準庫。
獲取當前日期和時間
注意下面第一個datetime
是包名,第二個datetime
是類名
# 獲取當前日期和時間
from datetime import datetime
now = datetime.now()
print(now)
print(type(now))
獲取指定日期和時間
#獲取指定日期和時間
from datetime import datetime
# 注意datetime的初始化方法裏 年月日時分秒 都是int類型
dt = datetime(2020, 2, 10, 22, 15, 30)
print(dt)
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表示的。
from datetime import datetime
dt = datetime(2020, 2, 10, 22, 15, 30)
print(dt)
# 獲取datetime對應的timestamp
print(dt.timestamp())
注意Python的timestamp是一個浮點數。如果有小數位,小數位表示毫秒數。
某些編程語言(如Java和JavaScript)的timestamp使用整數表示毫秒數,這種情況下只需要把timestamp除以1000就得到Python的浮點表示方法。
timestamp轉化爲datetime
我們知道timestamp是和時區沒有關係的,但是datetime是和時區有關的。
# timestamp轉化爲datetime
ts = 1581344130.0
# 轉化成本地時間,即北京時間,UTC+8
local_dt = datetime.fromtimestamp(ts)
# 轉化成utc時間,即UTC+0
utc_dt = datetime.utcfromtimestamp(ts)
print(local_dt)
print(utc_dt)
我們可以從打印結果也能看出datetime是和時區有關的,北京時間就比UTC時間+8。
str轉化爲datetime
用datetime.strptime(cls, date_string, format)
,注意是datetime
類的類方法。
我們常用的日期格式就是下面的格式,更加的詳細可以參考Python官網日期格式
# str轉化爲datetime
dt = datetime.strptime("2020-05-20 20:05:10", "%Y-%m-%d %H:%M:%S")
print(dt)
print(type(dt))
datetime轉化爲str
採用datetime.strftime(self, fmt)
格式化datetime
# datetime轉化爲str
s = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(s)
datetime加減
對於日期和時間的加減需要用到datetime
包裏的timedelta
類。
其中timedelta
類支持days
、weeks
、hours
、minutes
、seconds
等等
def __init__(self, days: float = ..., seconds: float = ..., microseconds: float = ...,
milliseconds: float = ..., minutes: float = ..., hours: float = ...,
weeks: float = ...)
# datetime加減
from datetime import datetime,timedelta
now = datetime.now()
print("當前時間是: ",now)
print("後退5天的時間是: ",now + timedelta(days=5))
print("前進5天的時間是: ",now - timedelta(days=5))
但是時間加減上並沒有前幾年或者前幾個月,這就需要另一個arrow
模塊去實現,不過這不是內建模塊,是第三方模塊,具體使用可查看Arrow官網。
# 從timestamp獲取arrow
a = arrow.get(1581344130.0)
# 注意這裏的format方法和strftime方法的參數格式是不同的
print(a.format("YYYY-MM-DD HH:mm:ss"))
# 從str獲取arrow
a = arrow.get("2020-02-10 22:15:30", "YYYY-MM-DD HH:mm:ss")
print(a.format("YYYY-MM-DD HH:mm:ss"))
# 獲取前2年 後3月 的時間
print("前2年 後3月 的時間: ",a.shift(years=-2, months=3).format("YYYY-MM-DD HH:mm:ss"))
collections
collections
是Python內建的一個集合模塊,提供了許多有用的集合類。
namedtuple
我們知道tuple
裏的元素是不可變的,但是訪問其中元素時是和list
一樣通過索引下標
訪問。
例如,我們定義一個平面系座標的點(x,y),就可以用namedtuple
來定義,然後通過名字來訪問。如下所示
# namedtuple 通過名字訪問tuple元素
from collections import namedtuple
import math
Point = namedtuple("Point",["x", "y"])
p = Point(3,4)
print("點p的橫座標爲{}, 縱座標爲{}, 其到原點長度是{}".format(p.x, p.y, math.sqrt(math.pow(p.x,2)+math.pow(p.y,2))))
# 此處Point已經是一個類型
print("p是Point? ", isinstance(p, Point))
print("p是tuple? ",isinstance(p, tuple))
deque
使用list
存儲數據時,按索引訪問元素很快,但是插入和刪除元素就很慢了,因爲list是線性存儲,數據量大的時候,插入和刪除效率很低
deque是爲了高效實現插入和刪除操作的雙向列表
,適合用於隊列和棧
。
from collections import deque
q = deque(['a', 'b', 'c'])
q.append('d')
q.appendleft('1')
print(q)
print("彈出右邊的元素: ", q.pop())
print("彈出左邊的元素: ", q.popleft())
print(q)
defaultdict
使用dict
時,如果引用的Key
不存在,就會拋出KeyError
。如果希望key不存在時,返回一個默認值
,就可以用defaultdict
注意defaultdict
裏面的參數是一個函數,該函數無參,但是有返回值即默認值。
# defaultdict 當key不存在時返回默認值
from collections import defaultdict
d = defaultdict(lambda : 0)
d['patrick']=100
print('patrick的分數是: ', d['patrick'])
print('marry的分數是(默認值): ', d['marry'])
ChainMap
ChainMap
可以把一組dict
串起來並組成一個邏輯上的dict
。ChainMap本身也是一個dict,但是查找的時候,會按照順序在內部的dict依次查找
。
這個在嚮應用程序傳入參數時非常適用。嚮應用程序傳入既可以通過命令行傳入,也可以通過環境變量,程序也有默認參數。但是我們可以使用ChainMap
來按序查找參數的值,即可以先從命令行查找,然後從環境變量查找,最後查默認值。
這個可以結合Hadoop的配置來理解,Hadoop優先支持命令行,然後支持配置文件,再支持默認值。
# ChainMap
from collections import ChainMap
import os
import argparse
# 構造缺省參數
defaults = {"dfs_replication": 3, "mapreduce_job_reduces": 0}
# 構造命令行參數
parser = argparse.ArgumentParser()
parser.add_argument("-dr", "--dfs_replication")
parser.add_argument("-mrn", "--mapreduce_job_reduces")
namespace = parser.parse_args()
# 獲取v不爲空的鍵值對
command_line = {k:v for k,v in vars(namespace).items() if v}
combined_maps = ChainMap(command_line, os.environ, defaults)
print("dfs_replication=",combined_maps["dfs_replication"])
print("mapreduce_job_reduces=",combined_maps["mapreduce_job_reduces"])
下面分別展示了三種情況下的參數的值。
注意:這裏之所以把.
換成了_
是因爲環境變量不支持.
而且需要使用export
將變量使得後面的子進程可見,否則os.environ
會獲取不到設置的環境變量值。
這裏由於使用了 argparse
模塊,故可以直接輸入-h
來查看其幫助信息。當然這個幫助信息是自動生成的。關於 argparse
模塊後面會有專門的章節去講,這也是Python內建模塊,詳細可以直接參考官網argparse模塊。
Counter
Counter
是一個簡單的計數器。
能非常方便的計數和統計前N的字符及其出現次數。
from collections import Counter
d = Counter()
for c in "programming":
d[c] = d[c]+1
print(d)
print("統計前2的字符是", d.most_common(2))
# 可以傳入iterable
print(Counter("programming"))
# 可以通過k=v傳入
print(Counter(a=3, b=2))
# 可以通過dict傳入
print(Counter({"a":3, "b":2}))
base64
Base64
是一種用64個字符來表示任意二進制數據的方法。
由於二進制文件如圖片、exe文件等包含很多無法顯示和打印的字符,所以,如果要讓記事本這樣的文本處理軟件能處理二進制數據,就需要一個二進制到字符串的轉換方法。Base64是一種最常見的二進制編碼方法。
Base64的原理很簡單,首先,準備一個包含64個字符的數組:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
然後,對二進制數據進行處理,每3個字節一組,一共是3x8=24bit,劃爲4組,每組正好6個bit:
這樣我們得到4個數字作爲索引,然後查表,獲得相應的4個字符,就是編碼後的字符串。
所以,Base64編碼會把3字節的二進制數據編碼爲4字節的文本數據,長度增加33%,好處是編碼後的文本數據可以在郵件正文、網頁等直接顯示。
如果要編碼的二進制數據不是3的倍數,最後會剩下1個或2個字節怎麼辦?Base64用\x00字節在末尾補足後,再在編碼的末尾加上1個或2個=號,表示補了多少字節,解碼的時候,會自動去掉。
所以Base64編碼後的字符串一定是4的倍數,如果不是4的倍數就需要再後面補相應個數的=號。
這裏需要說明下=
號只會出現在最後面作爲補足位數,前提是表裏本身就不包含=
。
如下所示,需要注意的是 b64encode 和 b64decode 方法的輸入參數和返回參數都是bytes類型的字符串
# -*- coding:UTF-8 -*-
import base64
# b64encode 和 b64decode 方法的輸入參數和返回參數都是bytes類型的字符串
# 編碼後的是bytes類型字符串
e_str = base64.b64encode("大數據平臺yarn".encode())
print(e_str)
# <class 'bytes'>
print(type(e_str))
s = base64.b64decode(e_str)
print(s.decode())
# 該方法是用於處理那些尾部已經去掉等號的bytes類型字符串
def safe_base64_decode(s):
s = s.decode()
left = len(s) % 4
list_str = []
list_str.append(s)
for i in range(left):
list_str.append("=")
return base64.b64decode("".join(list_str).encode()).decode()
assert '大數據平臺yarn' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+weWFybg=='), "帶等號解碼失效"
assert '大數據平臺yarn' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+weWFybg'), "不帶等號解碼失效"
assert '大數據平臺' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+w'), "帶等號解碼失效"
print('ok')
Base64是一種通過查表的編碼方法,不能用於加密,即使使用自定義的編碼表也不行(一般也不需要自定義表)。
Base64適用於小段內容的編碼,比如數字證書籤名、Cookie的內容等
hashlib
摘要算法簡介
摘要算法
又稱爲哈希算法
、散列算法
。它通過一個函數把任意長度的數據轉換爲一個固定的數據串(如16位固定長度)。
摘要算法的目的是爲了發現原始數據是否被人篡改過,如Apache下的包有的也會附帶着摘要,來判斷下載的包是否是完整的未經篡改的包,只是平時我們不去校驗摘要而已。
摘要算法之所以能夠指出原數據是否被人篡改過,主要在於摘要函數是一個單向函數,計算摘要很容易,但是通過摘要反推出原數據就很困難,任意一個bit的修改都會導致計算出摘要完全不同。
Python的hashlib
提供了常見的摘要算法,如MD5,SHA1等等。
MD5摘要算法使用
下面是常用的MD5
摘要算法的使用方法。
update()
:傳入原始數據(注意必須是bytes
類型),可以多次調用
digest()
:計算原始數據的摘要,返回的類型是bytes
hexdigest()
:計算原始數據的摘要,返回的類型是32位固定長度的字符串
import hashlib
# 獲取md5算法
md5 = hashlib.md5()
# 將Bytes類型的字符串傳入update方法裏, 可以多次傳入
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
md5.update('just use update & hexdigest'.encode('utf-8'))
# 計算到目前位置通過update傳入到該md5裏所有數據的摘要, 返回bytes類型的字符串
print(md5.digest())
# 返回32位固定長度的16進制字符串, 其類型是str
print(md5.hexdigest())
python的hashlib
模塊還支持如下算法,但是使用方法都是和md5
類似。
越安全的摘要算法長度越長,耗時越高。
摘要算法的應用
最常見的就是在數據庫中對用戶的密碼取md5摘要並代替明文密碼存到數據庫中,這樣可以防止用戶密碼隨意暴露給運維人員。
但是這樣也不一定安全。如果用戶設置如123456
、password
等簡單的密碼,黑客完全可以事先算出常用密碼的摘要並構造出一個反推表即通過摘要推出簡單的密碼。
我們可以在程序設計上對簡單密碼進行加強保護,俗稱加鹽
即Salt
。即對原始密碼添加一個複雜的字符串然後再進行摘要計算。只要Salt
沒有暴露,那麼很難通過摘要計算出明文密碼。
如果有兩個用戶使用同樣的密碼,那麼保存在數據庫中的摘要是一樣的,如何讓相同口令的用戶存儲不同的摘要呢?可以把用戶名作爲Salt
的一部分,從而實現相同口令的用戶存儲不同的摘要。
hmac
爲了防止黑客通過彩虹表根據哈希值反推出明文口令,根據上面內容可以採用加鹽
的方式使得相同的輸入得到不同的哈希值,大大增加黑客的破解難度。
其實加鹽
這種方式就是Hmac
算法:Keyed-Hashing for Message Authentication
。它通過一個標準算法,在計算哈希的過程中,把key混入計算過程中。採用Hmac替代我們自己的salt算法,可以使程序算法更標準化,也更安全。
Python自帶的hmac
模塊實現了標準的Hmac算法。
下面是Hmac算法的例子,和上面的MD5使用方法一樣。
import hmac
key = b'secret key'
message = "how to use md5 in python hashlib?".encode()
h = hmac.new(key, message, 'md5')
# 可以通過upadte方法傳入數據
h.update('just use update & hexdigest'.encode())
print(h.hexdigest())
itertools
itertools
模塊提供了很多用來創建和使用迭代對象的函數。
下面圖展示了itertools
模塊的源碼簡介
count
count(start=0, step=1) --> start, start+step, start+2*step, ...
會創建一個無限迭代器。
如下所示,打印100以內的自然數,由於是無限迭代器,所以測試代碼裏設置了退出條件。
import itertools
# 打印100以內的自然數
num = itertools.count(1)
for n in num:
if n <= 100:
print(n)
else:
break
cycle
cycle(p) --> p0, p1, ... plast, p0, p1, ...
會創建一個無限迭代器。
下面代碼展示了cycle
的使用方法,字符串是可迭代的。爲了使測試代碼能退出故使用了enumerate
計算迭代次數。
iter_c = itertools.cycle("hadoop")
for i,c in enumerate(iter_c):
if i< 10:
# 此處爲了讓打印看的更清除,就將sep和end設置成空字符串
print(c, sep="", end="")
else:
break
repeat
repeat(elem [,n]) --> elem, elem, elem, ... endlessly or up to n times
負責將一個元素無限重複下去,也可以指定參數來指定重複次數。
如下所示,將字符串hadoop
重複3次。
iter_s = itertools.repeat("hadoop", 3)
for s in iter_s:
print(s)
accumulate
accumulate(p[, func]) --> p0, p0+p1, p0+p1+p2
會創建一個不斷累積的無限迭代器,其中累積函數默認是求和。
如下面代碼就展示了求自然數前N項和的函數。
def sum_n(n):
iter_num = itertools.count(1)
iter_sum = itertools.accumulate(iter_num)
for i, s in enumerate(iter_sum):
if i == 10:
break
print("自然數前{}項和爲{}".format((i+1), s))
if __name__ == "__main__":
sum_n(10)
pass
chain
chain(p, q, ...) --> p0, p1, ... plast, q0, q1, ...
可以將已有的迭代器串起來形成更大的迭代器。
如下面所示,將兩個字符串迭代器串起來形成一個大的迭代器。
iter_chain = itertools.chain("hadoop", "spark")
for c in iter_chain:
# 此處爲了讓打印看的更清除,就將sep和end設置成空字符串
print(c, sep="", end="")
dropwhile
dropwhile(pred, seq) --> seq[n], seq[n+1], starting when pred fails
也會創建一個子迭代器。
如下面例子展示了從自然數中的11開始前十個自然數。
def print_ten(iter_element):
for i, element in enumerate(iter_element):
if i == 10:
break
print(element)
iter_n = itertools.dropwhile(lambda x:x<11, itertools.count(1))
print_ten(iter_n)
takewhile
takewhile(pred, seq) --> seq[0], seq[1], until pred fails
會創建一個迭代器。
如下面代碼展示了使用takewhile
取奇數項前十個元素
n=10
odd = itertools.count(1, step=2)
ten_odd = itertools.takewhile(lambda v: (v+1)/2 <= n, odd)
print(list(ten_odd))
groupby
groupby()
把迭代器中相鄰的重複元素挑出來放在一起。可以類比MapReduce任務的Reduce階段,不過又不同於Reduce,因爲這個迭代器中相同的元素並不是全部都是相鄰的。
所以請特別注意是相鄰的重複元素。
下面展示了groupby()
使用的例子及其結果
iter_groups = itertools.groupby('AAAABBCCCCDDD')
for k,sub_iter in iter_groups:
print("*******************************")
print("k={}".format(k))
print(list(sub_iter))
實際上挑選規則是通過函數完成的,只要作用於函數的兩個元素返回的值相等,這兩個元素就被認爲是在一組的,而函數返回值作爲組的key。如果我們要忽略大小寫分組,就可以讓元素’A’和’a’都返回相同的key。
下面的例子就是讓相鄰的大小寫字母歸爲同一組。
iter_groups = itertools.groupby('AAaaABBCccCDdd', lambda v: v.upper())
for k,sub_iter in iter_groups:
print("*******************************")
print("k={}".format(k))
print(list(sub_iter))
計算圓周率的小例子
根據提供的四個步驟去計算圓周率,如果打印出ok則證明算法是正確的。
def pi(n):
""" 計算pi的值
step 1: 創建一個奇數序列: 1, 3, 5, 7, 9, ...
step 2: 取該序列的前N項: 1, 3, 5, 7, 9, ..., 2*N-1.
step 3: 添加正負符號並用4除: 4/1, -4/3, 4/5, -4/7, 4/9, ...
step 4: 求和:
"""
# step 1
odd = itertools.count(1, step=2)
# step 2
n_odd = itertools.takewhile(lambda v: (v + 1) / 2 <= n, odd)
# step 3 注意這裏採用生成式的方式生成迭代對象
iter_n = ( -4/i if (i+1)/2%2 == 0 else 4/i for i in n_odd)
# step 4
from functools import reduce
return reduce(lambda x,y:x+y, iter_n)
def check_pi():
print(pi(10))
print(pi(100))
print(pi(1000))
print(pi(10000))
assert 3.04 < pi(10) < 3.05
assert 3.13 < pi(100) < 3.14
assert 3.140 < pi(1000) < 3.141
assert 3.1414 < pi(10000) < 3.1415
print('ok')
if __name__ == "__main__":
check_pi()
pass
contextlib
在Python裏我們是通過with
語句來自動關閉文件資源的,不需要寫try ... finally ...
這種繁瑣的語句。
並不是只有open()
函數返回的文件描述符對象才能使用with
語句。實際上,任何對象,只要正確實現了上下文管理,就可以使用with
語句關閉相應資源。
實現上下文管理是通過__enter__
和__exit__
這兩個方法實現的。
下面的代碼就展示瞭如何讓自定義類可以使用with
語句自動關閉資源。
class Query(object):
def __init__(self, name):
self.name=name
def __enter__(self):
print("Begin")
# 注意必須返回當前對象實例 否則使用with語句會報錯
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
print("Error")
else:
print("End")
def query(self):
print("Query info about {}".format(self.name))
with Query("patrick") as q:
q.query()
實際上每個類都要這麼去實現兩個方法也挺繁瑣的。
我們可以使用contextlib
模塊提供的contextmanager
去簡化代碼。
如下面代碼所示。@contextmanager
這個裝飾器接受一個generator
,用yield
語句把with ... as var
把變量輸出出去,然後,with
語句就可以正常地工作了。
class Query(object):
def __init__(self, name):
self.name=name
def query(self):
print("Query info about {}".format(self.name))
from contextlib import contextmanager
@contextmanager
def create_query(name):
print("Begin")
q = Query(name)
yield q
print("End")
with create_query("patrick") as q:
q.query()
很多時候我們希望在某些代碼塊前後自動執行特定代碼,可以使用@contextmanager
來實現。
如當向數據庫執行一條查詢語句時,可以按照下面的方式去計算查詢時間並打印出來。
from contextlib import contextmanager
import time
@contextmanager
def count_time(action):
print("Begin")
start = time.perf_counter()
yield
end = time.perf_counter()
print("{}共耗時{:.0f}s".format(action, (end-start)))
print("End")
with count_time("select"):
import random
# 向數據庫執行一條查詢語句
time.sleep(random.randint(3,7))
closing
contextlib
中還包含一個closing
對象,這個對象就是一個上下文管理器,它的__exit__
函數僅僅調用傳入參數的close
。其源碼如下。
所以closeing
上下文管理器僅使用於具有close()
方法的資源對象。如我們通過urllib.urlopen
打開一個網頁,urlopen
返回的對象有close
方法,所以我們就可以使用closing
上下文管理器。
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen("https://www.baidu.com/")) as resp:
print(type(resp))
for line in resp:
print(line)
urllib
urllib
模塊提供了一系列操作url的功能。
Get
urllib
的request
模塊可以非常方便地抓取URL內容,也就是發送一個GET請求到指定的頁面,然後返回HTTP的響應。
urlopen()
函數返回的對象類型是http.client.HTTPResponse
。假設下面的resp
就表示HTTPResponse
對象。
resp.info()
和 resp.getheaders()
返回HTTPResponse
的header
信息,只不過後者返回的是列表(如下面源碼所示)。可以通過resp.info().get('Content-Type')
來獲取具體的header
值,後者的話只能遍歷獲取了效率沒有前者高。
resp.geturl()
獲取頁面真實的url, 通過和原有的url進行比對可發現是否產生了重定向。
resp.getcode()
獲取響應的返回碼, 其實就是返回resp.status
。
resp.read()
獲取響應的返回內容。
下面代碼展示了通過urlopen()
函數來抓取三個網頁內容。
其中在抓取https://www.qq.com/
時發現該網頁的響應header
裏有Content-Encoding: gzip
,表示該網頁是通過gzip壓縮然後返回給客戶端的,客戶端需要進行解壓縮,所以下面的代碼就用了zlib
模塊去解壓縮。
由於resp.read()
返回的類型是bytes
,所以轉換成中文需要解碼。一方面可以參考響應header
裏的Content-Type
看其具體是什麼編碼,一方面可以參考第三方庫chardet
來檢查返回的內容具體是什麼編碼。
在發現網頁編碼類型是gb2312
時,直接用gbk
去解碼即可,如果用gb2312
反而會報錯,具體可看下面代碼。
from urllib.request import urlopen
from contextlib import closing
# url = "https://lol.qq.com/" # gb2312編碼
url = "https://www.qq.com/" # gb2312編碼 並採用了Content-Encoding: gzip 通過壓縮網頁內容來減少網絡傳輸數據量, 當然客戶端就需要解壓縮
# url = "https://www.csdn.net/" # utf-8編碼
with closing(urlopen(url)) as resp:
# 屬於http.client.HTTPResponse
print(type(resp))
# 獲取返回的header信息, 可以通過resp.info() 也可以通過resp.getheaders()
print(resp.info())
print(resp.getheaders())
# 獲取頁面真實的url, 通過和原有的url進行比對可發現是否產生了重定向
# 其實就是返回resp.url
print(resp.geturl())
# 獲取響應的返回碼 其實就是返回resp.status
print(resp.getcode())
# 獲取響應返回的內容
data = resp.read()
# 獲取其'Content-Type'
print(resp.info().get('Content-Type'))
gzip_val = resp.info().get("Content-Encoding")
if gzip_val:
print("Content-Encoding: ", gzip_val)
# 解壓縮
import zlib
data = zlib.decompress(data, 16+zlib.MAX_WBITS)
# 引用第三方包檢查其類型 只作爲參考, 也可以使用上面提到的'Content-Type'
import chardet
detect_res = chardet.detect(data)
print(detect_res)
if detect_res.get("encoding").lower().find("utf") != -1:
print(data.decode())
else:
# 例如 https://lol.qq.com/ 網頁的編碼就是gb2312 但是解碼的時候直接用gbk解碼就好
print(data.decode("gbk"))
模擬瀏覽器去訪問
如果我們要想模擬瀏覽器發送GET請求,就需要使用Request
對象,通過往Request
對象添加相應的header
信息,我們就可以把請求僞裝成瀏覽器。例如,模擬iPhone 6去請求豆瓣首頁
首先通過request.Request(url)
獲取Request
對象,然後通過req.add_header()
添加相應的header
信息,再使用urlopen(req)
請求網頁,注意此時urlopen()
函數的參數是Request
對象。
下面只展示了部分代碼,其餘內容和上面的代碼保持一致。
其中user-agent
的值可以通過在瀏覽器上按F12
查看(如下圖所示)。
from urllib.request import urlopen
from urllib import request
from contextlib import closing
# url = "https://lol.qq.com/" # gb2312編碼
# url = "https://www.qq.com/" # gb2312編碼 並採用了Content-Encoding: gzip 通過壓縮網頁內容來減少網絡傳輸數據量, 當然客戶端就需要解壓縮
# url = "https://www.csdn.net/" # utf-8編碼
url = "https://www.douban.com/"
req = request.Request(url)
# req.add_header("user-agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36")
# 模擬手機發送請求
req.add_header("user-agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36")
with closing(urlopen(req)) as resp:
# 獲取返回的header信息, 可以通過resp.info() 也可以通過resp.getheaders()
print(resp.info())
print(resp.getheaders())
Post
可以採用post的方式去提交請求。譬如登錄。
下面的請求中查看所有用戶信息和添加用戶信息之前都需要先登錄。那麼這就涉及到一個保存cookie的問題。
下面的請求是用SpringBoot
寫的一個簡單的web應用。傳遞參數都是用的json
,所以在發出請求時都需要在請求的header
信息裏添加content-type: application/json
url | 說明 |
---|---|
/login | 登錄 |
/user/list | 查看所有用戶信息 |
/user/add | 添加用戶信息 |
單獨的用戶登錄
由於傳遞的是json信息,需要使用json.dumps(login_d).encode("utf8")
將json信息轉化成字符串並編碼成bytes
類型,這樣才能傳遞給urlopen()
函數的data參數。
from urllib.request import urlopen
from urllib import request
from contextlib import closing
import json
url = "http://localhost:8080/"
req = request.Request(url+"login")
# 登錄信息
login_d = {"username": 'admin', "password": 'admin'}
# 將個人信息轉化成字符串後並編碼轉化成bytes類型
post_data = json.dumps(login_d).encode("utf8")
# 由於後臺服務支持的是 application/json 而不是application/x-www-form-urlencoded 所以請求對象這裏必須顯示設置
req.add_header("content-type", "application/json")
with closing(urlopen(req, data=post_data)) as resp:
# 獲取響應返回的內容
data = resp.read()
# 後臺返回的是json字符串 通過json.loads轉化成json對象
res = json.loads(data)
print(res)
print(res.get("msg"))
print(res.get("code"))
添加和查看用戶信息
添加用戶前,需要登錄,如上面所示需要考慮cookie的保存問題。
需要使用下面的代碼保存cookie
信息,並且不能需要使用opener
對象去請求,而不是像以前一樣用urlopen()
函數。
from urllib import request
from http import cookiejar
from contextlib import closing
# 利用cookie保存登錄信息
cookie = cookiejar.CookieJar()
handler = request.HTTPCookieProcessor(cookie)
# 後面用opener去請求而不是用urlopen() 這樣就能訪問cookie裏的登錄信息
opener = request.build_opener(handler)
下面是完整的代碼
# -*- coding:UTF-8 -*-
from urllib import request
from http import cookiejar
from contextlib import closing
import json
server_url = "http://localhost:8080/"
# 利用cookie保存登錄信息
cookie = cookiejar.CookieJar()
handler = request.HTTPCookieProcessor(cookie)
# 後面用opener去請求而不是用urlopen() 這樣就能訪問cookie裏的登錄信息
opener = request.build_opener(handler)
def login():
req = request.Request(server_url + "login")
# 登錄信息
login_d = {"username": 'admin', "password": 'admin'}
# 將個人信息轉化成字符串後並編碼轉化成bytes類型
post_data = json.dumps(login_d).encode("utf8")
# 由於後臺服務支持的是 application/json 而不是application/x-www-form-urlencoded 所以請求對象這裏必須顯示設置
req.add_header("content-type", "application/json")
# with closing(urlopen(req, data=post_data)) as resp:
with closing(opener.open(req, data=post_data)) as resp:
# 獲取響應返回的內容
data = resp.read()
# 後臺返回的是json字符串 通過json.loads轉化成json對象
res = json.loads(data)
print(res)
def user_list():
req = request.Request(server_url + "user/list")
with closing(opener.open(req)) as resp:
# 獲取響應返回的內容
data = resp.read()
# 後臺返回的是json字符串 通過json.loads轉化成json對象
res = json.loads(data)
# 採用pprint模塊打印的更加美化
import pprint
pprint.pprint(res)
def user_add(user):
req = request.Request(server_url + "user/add")
req.add_header("content-type", "application/json")
post_data = json.dumps(user).encode()
with closing(opener.open(req, data=post_data)) as resp:
# 獲取響應返回的內容
data = resp.read()
# 後臺返回的是json字符串 通過json.loads轉化成json對象
res = json.loads(data)
print(res)
if __name__ == "__main__":
login()
user_data = {"username": "鹿丸", "password": "admin", "age": 20, "sex": "男", "money": 500.25, "school": "北京大學"}
user_add(user_data)
user_data = {"username": "丁次", "password": "admin", "age": 20, "sex": "男", "money": 200.25, "school": "火影大學"}
user_add(user_data)
user_data = {"username": "井野", "password": "admin", "age": 20, "sex": "女", "money": 100.25, "school": "中忍大學"}
user_add(user_data)
user_list()
pass
XML
python有三種方式去解析xml文件。個人傾向於第三種方式。
- 使用
dom
去解析。缺點是需要將整個xml文件都讀入內存,內存佔用高,比較慢。 - 使用
sax
去解析。sax
是採用事件驅動模型,邊讀入內存邊解析,優點是佔用內存小,解析快,缺點是需要自己寫對應事件的回調函數。 - 使用
ElementTree
去解析。ElementTree 相對於 DOM 來說擁有更好的性能,與 SAX 性能差不多,API 使用也很方便。
準備xml文件如下。
<?xml version="1.0" encoding="utf-8"?>
<list>
<student id="stu1" name="stu1_name">
<id>1001</id>
<name>張三</name>
<age>22</age>
<gender>男</gender>
</student>
<student id="stu2" name="stu2_name">
<id>1002</id>
<name>李四</name>
<age>21</age>
<gender>女</gender>
</student>
</list>
dom
from xml.dom.minidom import parse
xml_path = "d:/test.xml"
def func_dom():
"""
使用dom解析xml文件, 由於dom會將整個xml文件讀入內存,佔用內存高,解析會比較慢
"""
# 將文件讀取成一個dom對象
dom = parse(xml_path)
# 獲取文檔元素對象 這裏獲取的是<list>
root = dom.documentElement
# 類型是 xml.dom.minidom.Element
print(type(root))
# 根據tag=student獲取所有的student
stus = root.getElementsByTagName("student")
for stu in stus:
# 獲取屬性值
attr_id = stu.getAttribute("id")
attr_name = stu.getAttribute("name")
# 獲取節點值
id = stu.getElementsByTagName("id")[0].childNodes[0].data
name = stu.getElementsByTagName("name")[0].childNodes[0].data
age = stu.getElementsByTagName("age")[0].childNodes[0].data
gender = stu.getElementsByTagName("gender")[0].childNodes[0].data
print("attr_id={}\tattr_name={}\tid={}\tname={}\tage={}\tgender={}".format(
attr_id, attr_name, id, name, age, gender
))
sax
使用sax
最麻煩的就是需要自己寫回調函數。需要了解xml.sax.handler.ContentHandler
類中幾個事件的調用時機。
-
characters(content)
方法
從行開始,遇到標籤之前,存在字符,content 的值爲這些字符串。
從一個標籤,遇到下一個標籤之前, 存在字符,content 的值爲這些字符串。
從一個標籤,遇到行結束符之前,存在字符,content 的值爲這些字符串。
標籤可以是開始標籤,也可以是結束標籤。 -
startDocument() 方法
文檔啓動的時候調用。 -
endDocument() 方法
解析器到達文檔結尾時調用。 -
startElement(name, attrs)
方法
遇到XML開始標籤時調用,name是標籤的名字,attrs是標籤的屬性值字典。 -
endElement(name)
方法
遇到XML結束標籤時調用。
from xml.sax import ContentHandler
import xml.sax as sax
xml_path = "d:/test.xml"
class StudentHandler(ContentHandler):
def __init__(self):
# 存放所有的學生
self.stus = []
def startElement(self, name, attrs):
# 記錄當前的element
self.CurrentData = name
if name == "student":
stu = {"attr_id": attrs["id"], "attr_name": attrs["name"]}
self.stus.append(stu)
# 記錄當前學生
self.CurrentStu = stu
def endElement(self, name):
if name in ("id", "name", "age", "gender"):
# 清空CurrentData 這是由於characters的調用時機會有三次
# 所以當遇到介紹元素時就可以清空CurrentData保證屬性值不會被覆蓋
self.CurrentData = ""
pass
def characters(self, content):
if self.CurrentData in ("id", "name", "age", "gender"):
self.CurrentStu[self.CurrentData]=content
pass
def func_sax():
"""
SAX 用事件驅動模型,通過在解析XML的過程中觸發一個個的事件並調用用戶定義的回調函數來處理XML文件。
邊讀邊解析,優點是佔用內存小,缺點是需要自己寫回調函數
"""
# 創建一個SAX parser
sax_parser = sax.make_parser()
# 關閉命名空間
sax_parser.setFeature(sax.handler.feature_namespaces, 0)
# 重寫Handler
stu_handler = StudentHandler()
sax_parser.setContentHandler(stu_handler)
# 解析xml文件
sax_parser.parse(xml_path)
import pprint
pprint.pprint(stu_handler.stus)
ElementTree
個人比較推薦這種方式去解析xml,主要是api比較友好。
def func_element_tree():
"""
使用ElementTree解析xml
"""
# 解析xml文件爲ElementTree對象
tree = ET.parse(xml_path)
# 獲取根元素
root = tree.getroot()
# xml.etree.ElementTree.Element
# print(type(root))
for stu in root:
attrs = stu.attrib
stu_d = {"attr_id": attrs["id"], "attr_name": attrs["name"]}
stu_d["id"]=stu.findtext("id")
stu_d["name"]=stu.findtext("name")
stu_d["age"]=stu.findtext("age")
stu_d["gender"]=stu.findtext("gender")
print(stu_d)
三種方式的代碼運行截圖
將上面三種方式的代碼合併在一起後,運行結果截圖如下。
HTMLParser
python提供了html.parser.HTMLParser
類去解析HTML。需要注意的是該類也是事件驅動型,和用SAX
去解析xml文件類似。
下面是該類常用的方法:
- HTMLParser.
feed
(data):接收一個字符串類型的HTML內容,並進行解析。 - HTMLParser.
handle_starttag
(tag, attrs):對開始標籤的處理方法。例如<div id="main">
,參數tag指的是div,attrs指的是一個(name,Value)的列表,即列表裏面裝的數據是元組。 - HTMLParser.
handle_endtag
(tag):對結束標籤的處理方法。例如</div>
,參數tag指的是div。 - HTMLParser.
handle_startendtag
(tag, attrs):識別沒有結束標籤的HTML標籤,例如<img />
等。 - HTMLParser.
handle_data
(data):對標籤之間的數據的處理方法。<tag>test</tag>
,data指的是“test”。
實戰例子
我們獲取csdn博客首頁右下角的企業博客信息, 包括企業博客的 名字、原創數、粉絲數、獲贊數。如下圖。
思路:首先通過上面的urlopen()
函數去抓取該網頁內容,然後通過html.parser.HTMLParser
類去解析HTML獲取網頁上列出的企業博客信息。個人覺得最重要的是需要仔細觀看網頁內容,然後根據需求定位到所需要的元素,即需要一定的HTML知識。因爲html.parser.HTMLParser
類就是在掃描一個個標籤的時候觸發的事件(也就是你寫的回調函數)。
下面貼出信息所在的html部分內容。
下面的代碼我在上面的思路基礎上又寫了一個稍微沒那麼複雜的代碼,即通過re
模塊去匹配到所需要的html內容,這樣就會大大地較少解析的內容,同時代碼上看起來就會簡潔一些。
此處就不細講代碼具體的實現邏輯了。代碼關鍵處有註釋。
# -*- coding:UTF-8 -*-
from contextlib import closing
from urllib.request import urlopen
from html.parser import HTMLParser
"""
獲取csdn博客首頁右下角的企業博客信息, 包括企業博客的 名字、原創數、粉絲數、獲贊數。
"""
def get_page(url):
data = ""
with closing(urlopen(url)) as resp:
# 獲取響應返回的內容
data = resp.read()
return data.decode()
class CsdnHtmlParser(HTMLParser):
def __init__(self):
# 必須對父類進行初始化 要不然運行會報錯
HTMLParser.__init__(self)
# 記錄一些信息以便後面事件驅動時使用
self.enterprises = []
self.is_tick = False
self.is_tick_data = False
self.CurrentEnterprise = None
self.name_ok = False
def handle_starttag(self, tag, attrs):
if tag == "div" and (("class", "enterprise_r") in attrs):
enterprise = {"msg": "ok"}
self.CurrentEnterprise = enterprise
self.enterprises.append(enterprise)
elif tag == "a" and (("target", "_blank") in attrs):
if self.CurrentEnterprise:
href = list(filter(lambda x: x[0] == "href", attrs))[0][1]
self.CurrentEnterprise["地址"] = href
self.name_ok = True
elif tag == "span" and (("class", "name") in attrs):
if self.CurrentEnterprise:
self.is_tick = True
self.attr_name = None
elif tag == "span" and (("class", "number") in attrs):
if self.CurrentEnterprise and self.attr_name:
self.is_tick_data = True
pass
def handle_startendtag(self, tag, attrs):
# print("tag: ", tag)
pass
def handle_endtag(self, tag):
if tag == "div" and self.CurrentEnterprise:
self.CurrentEnterprise = None
pass
def handle_data(self, data):
if self.CurrentEnterprise and self.name_ok:
self.CurrentEnterprise["名字"] = data
self.name_ok = False
if self.is_tick:
old_attr_name = self.attr_name
self.attr_name = data
if self.is_tick_data:
self.CurrentEnterprise[old_attr_name] = data
self.is_tick = False
self.is_tick_data = False
pass
pass
def parse_page(htmldata):
"""
獲取企業博客的 名字、原創數、粉絲數、獲贊數
"""
csdn_parser = CsdnHtmlParser()
csdn_parser.feed(htmldata)
import pprint
pprint.pprint(csdn_parser.enterprises)
def cut_html(htmldata):
import re
# 記住.*?這樣就不是貪婪匹配了
pattern = r'.*?<div class="enterprise_r">(.*?)</p>\n\s+</div>.*?'
# 這裏使用re.DOTALL讓.可以表示任意字符(包括回車換行符) 不加的話.不能表示回車換行符的
# 使用findall找出所有匹配到的內容
matchs = re.findall(pattern, htmldata, re.DOTALL)
for text in matchs:
# print(text)
cut_parser = CutCsdnHtmlParser()
cut_parser.feed(text)
cut_parser.blog_info()
class CutCsdnHtmlParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
# 記錄一些信息以便後面事件驅動時使用
self.href=""
self.name=""
self.blog = {}
self.name_ok = False
self.is_tick = False
self.attr_name = None
self.is_tick_data = None
def handle_starttag(self, tag, attrs):
if tag == "a" and (("target", "_blank") in attrs):
self.href=list(filter(lambda x: x[0] == "href", attrs))[0][1]
self.name_ok=True
elif tag == "span" and (("class", "name") in attrs):
self.is_tick = True
elif tag == "span" and (("class", "number") in attrs):
self.is_tick_data = True
pass
def handle_startendtag(self, tag, attrs):
pass
def handle_endtag(self, tag):
pass
def handle_data(self, data):
if self.name_ok:
self.name=data
# 避免下次進來替換掉正確的值
self.name_ok=False
elif self.is_tick:
self.attr_name=data
self.is_tick=False
elif self.is_tick_data:
self.blog[self.attr_name]=data
self.is_tick_data=False
pass
def blog_info(self):
self.blog["href"]=self.href
self.blog["name"]=self.name
import pprint
pprint.pprint(self.blog)
pass
if __name__ == "__main__":
url = "https://www.csdn.net/"
# 第一種方式是直接整個HTML文件 由於HTMLParser是事件驅動類型 代碼寫的會比較凌亂
htmldata = get_page(url)
parse_page(htmldata)
# 第二種方式是首先用re模塊去匹配出所需要的html內容,然後再通過HTMLParser去解析
# 由於已經通過re模塊找出我們所需要的內容,所以代碼上相較於第一種方式會簡單一點
htmldata = get_page(url)
cut_html(htmldata)
參考網址
廖雪峯老師Python教程之常用內建模塊
argparse簡述
python菜鳥教程之xml解析
python解析xml
python正則中換行符的匹配