Python常用内建模块(内含实例)

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类支持daysweekshoursminutesseconds等等

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摘要并代替明文密码存到数据库中,这样可以防止用户密码随意暴露给运维人员。

但是这样也不一定安全。如果用户设置如123456password等简单的密码,黑客完全可以事先算出常用密码的摘要并构造出一个反推表即通过摘要推出简单的密码。

我们可以在程序设计上对简单密码进行加强保护,俗称加盐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

urllibrequest模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应。

urlopen()函数返回的对象类型是http.client.HTTPResponse。假设下面的resp就表示HTTPResponse对象。
resp.info()resp.getheaders() 返回HTTPResponseheader信息,只不过后者返回的是列表(如下面源码所示)。可以通过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正则中换行符的匹配

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章