一、hashlib 文件一致性校验

为何要进行文件一致性校验?

为了确保你得到的文件是正确的版本,而没有被注入病毒和木马程序。例如我们经常在网上下载软件,而这些软件已经被注入了一些广告和病毒等,如果不进行文件与原始发布商的一致性校验的话,可能会给我们带来一定的损失。

文件一致性校验原理

要进行文件的一致性校验,我们不可能像文本文件比较那样,将两个文件放到一起对比,因为很多的时候文件很大。目前最理想的办法就是,是通过加密算法,对文件生成对应的值,通过生成的值与发布商提供的值比较来确认两个文件是否一致。

MD5 和 SHA1 就是目前使用最为广泛的良种加密算法。

举例:

先手动创建 2 个文件,file1 和 file2,内容为 123

使用 md5 计算 file1 的加密值

  1. import hashlib
  2. md5obj = hashlib.md5() # 创建md5对象
  3. with open('file1','rb') as f:
  4. content = f.read() # 读取文件所有内容
  5. md5obj.update(content)
  6. print(md5obj.hexdigest())

执行输出:

202cb962ac59075b964b07152d234b70

再计算 fiel2 的加密值,再把上面的代码复制一遍?太 low 了,如果有多个文件怎么办?

定义一个方法

  1. import hashlib<br>def check(filename):
  2. md5obj = hashlib.md5() # 创建md5对象
  3. with open(filename,'rb') as f:
  4. content = f.read() # 读取文件所有内容
  5. md5obj.update(content)
  6. return md5obj.hexdigest()
  7. ret1 = check('file1')
  8. ret2 = check('file2')
  9. print(ret1)
  10. print(ret2)

执行输出:

202cb962ac59075b964b07152d234b70

202cb962ac59075b964b07152d234b70

从结果上,可以看出是一致的。

修改 file2 的内容,改成 123456

再次校验,执行输出

202cb962ac59075b964b07152d234b70

e10adc3949ba59abbe56e057f20f883e

就可以知道,文件内容不一致了。

但是上面的方法,有一个缺陷,当文件达到 GB 级别的时候,内存会爆炸

那怎么办呢?先来讲一个小例子

  1. import hashlib
  2. md5obj = hashlib.md5() # 创建md5对象
  3. md5obj.update(b'alex sb') # b'string' 表示bytes类型,不能有中文符号
  4. print(md5obj.hexdigest())

执行输出:

3eae6f4760a98a52891a109face75648

拆分字符串

  1. import hashlib
  2. md5obj = hashlib.md5() # 创建md5对象
  3. md5obj.update(b'alex ') # 拆分字符串
  4. md5obj.update(b'sb') #
  5. print(md5obj.hexdigest())

执行输出:

3eae6f4760a98a52891a109face75648

可以看出 2 个结果是一样的

结论

一段字符串直接进行摘要和分成几段摘要的结果是相同的

那么就可以把大文件,分段进行 md5 加密,就可以了

下载一部电影《海上钢琴师》,文件有 1.58GB

本片讲述了一个钢琴天才传奇的一生。 豆瓣评分 9.2

计算电影的 md5 值

  1. import hashlib
  2. def check(filename):
  3. md5obj = hashlib.md5()
  4. with open(filename,'rb') as f:
  5. while True:
  6. content = f.read(1048576) # 每次读取1048576字节,也就是1MB
  7. if content:
  8. md5obj.update(content)
  9. else:
  10. break # 当内容为空时,终止循环
  11. return md5obj.hexdigest()
  12. ret1 = check('E:\迅雷下载\[迅雷下载www.2tu.cc]海上钢琴师.BD1280高清中英双字.rmvb')
  13. print(ret1)

花费了 9 秒,执行输出:

30c7f078203d761d3f13bec6f8fd3088

一次性校验,对算法要求没有那么高,用 md5 就足够了。

总结:

序列化 把数据类型变成字符串

为什么要有序列化 因为在网络上和文件中能存在的只有字节

json 在所有语言中通用 只对有限的数据类型进行序列化 字典 列表 字符串 数字 元组

在多次写入 dump 数据进入文件的时候,不能通过 load 来取。

pickle 只能在 python 中使用 对绝大多数数据类型都可以进行序列化

在 load 的时候,必须拥有被 load 数据类型对应的类在内存里

dumps 序列化

loads 反序列化

dump 直接向文件中序列化

load 直接对文件反序列化

shelve

f = open() 打开文件

json 和 pickle 必须熟练掌握

二、configparser

该模块适用于配置文件的格式与 windows ini 文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。

创建文件

来看一个好多软件的常见文档格式如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597799263011-64db4ffe-ebfe-4a48-82c4-00a451df03b3.png)

section 称之为节点,节点里面的赋值对,称之为项

如果想用 python 生成一个这样的文档怎么做呢?

  1. import configparser
  2. config = configparser.ConfigParser() #创建一个ConfigParser对象
  3. config["DEFAULT"] = {'ServerAliveInterval': '45', #默认参数
  4. 'Compression': 'yes',
  5. 'CompressionLevel': '9',
  6. 'ForwardX11':'yes'
  7. }
  8. config['bitbucket.org'] = {'User':'hg'} #添加一个节点bitbucket.org
  9. config['topsecret.server.com'] = {'Host Port':'50022','ForwardX11':'no'}
  10. with open('example.ini', 'w') as configfile: #写入配置文件example.ini
  11. config.write(configfile)

执行程序,查看 example.ini 的内容

  1. [DEFAULT]
  2. serveraliveinterval = 45
  3. forwardx11 = yes
  4. compression = yes
  5. compressionlevel = 9
  6. [bitbucket.org]
  7. user = hg
  8. [topsecret.server.com]
  9. forwardx11 = no
  10. host port = 50022

可以看出节点的项,都变成小写了。

这是因为它在写入的时候,将所有字符串使用了 lower()方法,转换为小写了。

查找文件

  1. import configparser
  2. config = configparser.ConfigParser()
  3. config.read('example.ini')<br>###上面内容为固定部分###
  4. print(config.sections()) # 查看所有的节点,但默认不显示DEFAULT,返回列表

执行输出:

[‘bitbucket.org’, ‘topsecret.server.com’]

下面的代码,固定部分我就不贴了

  1. print('bitbucket.org' in config) # 验证某个节点是否在文件中

执行输出:True

  1. print(config['bitbucket.org']['user']) # 查看某节点下面的某个项的值

执行输出:hg

  1. print(config['bitbucket.org']) # 输出一个可迭代对象

执行输出:

  1. #使用for循环一个可迭代对象
  2. for key in config['bitbucket.org']: # 注意,有default时,会默认输出它的键
  3. print(key)

执行输出:

user

serveraliveinterval

forwardx11

compression

compressionlevel

  1. print(config.items('bitbucket.org')) # 找到'bitbucket.org'下所有的键值对

执行输出:

[(‘serveraliveinterval’, ‘45’), (‘forwardx11’, ‘yes’), (‘compression’, ‘yes’), (‘compressionlevel’, ‘9’), (‘user’, ‘hg’)]

  1. print(config.get('bitbucket.org','compression')) # get方法section下的key对应的value

执行输出:yes

增删改操作

增加一个节点

  1. print(config.add_section('yuan')) # 增加一个节点

注意,它不会立即写入!必须执行下面的代码

  1. config.write(open('example.ini', "w")) # 写入文件

open(‘example.ini’,w) 表示清空文件

config.write 表示写入内容

再次查看文件内容:

  1. [DEFAULT]
  2. serveraliveinterval = 45
  3. forwardx11 = yes
  4. compression = yes
  5. compressionlevel = 9
  6. [bitbucket.org]
  7. user = hg
  8. [topsecret.server.com]
  9. forwardx11 = no
  10. host port = 50022
  11. [yuan]

删除一个节点

  1. config.remove_section('bitbucket.org')
  2. config.write(open('example.ini', "w")) # 写入文件

修改节点

  1. config.set('yuan','k2','222') # yuan节点增加项k2 = 222
  2. config.write(open('example.ini', "w")) # 写入文件

总结:

section 可以直接操作它的对象来获取所有的节信息

option 可以通过找到的节来查看多有的项

三、logging

为了保护数据安全

所有的增加,修改,删除操作,都要记录日志

比如 log 日志,管理员操作日志,消费记录…

日志给我们在内部操作的时候提供很多遍历

日志给用户提供更多的信息

在程序使用的过程中自己调试需要看的信息

帮助程序员排查程序的问题

logging 模块 不会自动帮你添加日志的内容

你自己想打印什么 你就写什么

  1. import logging
  2. logging.debug('debug message') # debug 调试模式 级别最低
  3. logging.info('info message') # info 显示正常信息
  4. logging.warning('warning message') # warning 显示警告信息
  5. logging.error('error message') # error 显示错误信息
  6. logging.critical('critical message') # critical 显示验证错误信息

执行输出:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597799263055-22de7a3c-c26c-4d6f-a21c-90cb50f3df80.png)

显示红色字体

root 表示当前文件的权限

为啥少了 2 个?这个问题先放一边

logging 有 2 种配置形式

简单配置

配置 logger 对象

这 2 种形式是完全独立的

使用简单配置,局限性比较大

简单模式:默认情况下 只显示 警告 及警告级别以上信息

如果想显示 debug,需要调整告警级别

  1. import logging
  2. logging.basicConfig(level=logging.DEBUG) # 设置警告级别为debug
  3. logging.debug('debug message') # debug 调试模式 级别最低
  4. logging.info('info message') # info 显示正常信息
  5. logging.warning('warning message') # warning 显示警告信息
  6. logging.error('error message') # error 显示错误信息
  7. logging.critical('critical message') # critical 显示验证错误信息

执行输出:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597799263026-ac455f1f-891b-446e-a0bc-9a8269480a5d.png)

设置 info,只显示 info 以上的错误

能不能只显示一种级别信息呢?不行!

只能打印某个级别以上的信息

增加时间显示

  1. import logging
  2. logging.basicConfig(level=logging.DEBUG,
  3. format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
  4. logging.debug('debug message') # debug 调试模式 级别最低
  5. logging.info('info message') # info 显示正常信息
  6. logging.warning('warning message') # warning 显示警告信息
  7. logging.error('error message') # error 显示错误信息
  8. logging.critical('critical message') # critical 显示验证错误信息

执行输出:

2018-04-23 19:43:56,982 testt.py[line:58] DEBUG debug message

2018-04-23 19:43:56,982 testt.py[line:59] INFO info message

2018-04-23 19:43:56,982 testt.py[line:60] WARNING warning message

2018-04-23 19:43:56,982 testt.py[line:61] ERROR error message

2018-04-23 19:43:56,982 testt.py[line:62] CRITICAL critical message

设置时间格式

  1. import logging
  2. logging.basicConfig(level=logging.DEBUG,
  3. format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s'),
  4. datefmt = '%a, %d %b %y %H:%M:%S',
  5. logging.debug('debug message') # debug 调试模式 级别最低
  6. logging.info('info message') # info 显示正常信息
  7. logging.warning('warning message') # warning 显示警告信息
  8. logging.error('error message') # error 显示错误信息
  9. logging.critical('critical message') # critical 显示验证错误信息

执行输出:

2018-04-23 19:46:01,727 testt.py[line:59] DEBUG debug message

2018-04-23 19:46:01,727 testt.py[line:60] INFO info message

2018-04-23 19:46:01,727 testt.py[line:61] WARNING warning message

2018-04-23 19:46:01,727 testt.py[line:62] ERROR error message

2018-04-23 19:46:01,727 testt.py[line:63] CRITICAL critical message

配置参数

  1. logging.basicConfig()函数中可通过具体参数来更改logging模块默认行为,可用参数有:
  2. filename:用指定的文件名创建FiledHandler,这样日志会被存储在指定的文件中。
  3. filemode:文件打开方式,在指定了filename时使用这个参数,默认值为“a”还可指定为“w”。
  4. format:指定handler使用的日志显示格式。
  5. datefmt:指定日期时间格式。
  6. level:设置rootlogger(后边会讲解具体概念)的日志级别
  7. stream:用指定的stream创建StreamHandler。可以指定输出到sys.stderr,sys.stdout或者文件(f=open(‘test.log’,’w’)),默认为sys.stderr。若同时列出了filenamestream两个参数,则stream参数会被忽略。
  8. format参数中可能用到的格式化串:
  9. %(name)s Logger的名字
  10. %(levelno)s 数字形式的日志级别
  11. %(levelname)s 文本形式的日志级别
  12. %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
  13. %(filename)s 调用日志输出函数的模块的文件名
  14. %(module)s 调用日志输出函数的模块名
  15. %(funcName)s 调用日志输出函数的函数名
  16. %(lineno)d 调用日志输出函数的语句所在的代码行
  17. %(created)f 当前时间,用UNIX标准的表示时间的浮 点数表示
  18. %(relativeCreated)d 输出日志信息时的,自Logger创建以 来的毫秒数
  19. %(asctime)s 字符串形式的当前时间。默认格式是 2003-07-08 16:49:45,896”。逗号后面的是毫秒
  20. %(thread)d 线程ID。可能没有
  21. %(threadName)s 线程名。可能没有
  22. %(process)d 进程ID。可能没有
  23. %(message)s用户输出的消息

写入文件

  1. import logging
  2. logging.basicConfig(level=logging.DEBUG,
  3. format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
  4. datefmt='%a, %d %b %y %H:%M:%S',
  5. filename = 'userinfo.log'
  6. )
  7. logging.debug('debug message') # debug 调试模式 级别最低
  8. logging.info('info message') # info 显示正常信息
  9. logging.warning('warning message') # warning 显示警告信息
  10. logging.error('error message') # error 显示错误信息
  11. logging.critical('critical message') # critical 显示验证错误信息

执行程序,查看文件内容

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597799263030-2c923e1b-d335-42ba-b2ce-3c9031320ed7.png)

某些情况下,查看文件是乱码的。

它的局限性有 2 个

编码格式不能设置

不能同时输出到文件和屏幕

loggin 对象方式

由于简单配置有局限性,logging 对象方式更为灵活

  1. import logging
  2. logger = logging.getLogger() # 实例化了一个logger对象
  3. #在国外叫handler,在中国翻译过来,叫句柄
  4. #设置文件名和编码
  5. fh = logging.FileHandler('test.log',encoding='utf-8') # 实例化了一个文件句柄
  6. sh = logging.StreamHandler() # 用于输出到控制台
  7. #吸星大法
  8. logger.addHandler(fh) # 吸收写文件功能
  9. logger.addHandler(sh) # 吸收输出屏幕功能
  10. logger.warning('warning message')

执行输出:

warning message

查看文件内容,也是

warning message

这样就具备了同时写入文件以及输出屏幕的技能

增加输出格式功能

  1. import logging
  2. logger = logging.getLogger() # 实例化了一个logger对象
  3. #在国外叫handler,在中国翻译过来,叫句柄
  4. #设置文件名和编码
  5. fh = logging.FileHandler('test.log',encoding='utf-8') # 实例化了一个文件句柄 # 格式和文件句柄或者屏幕句柄关联
  6. sh = logging.StreamHandler() # 用于输出到控制台
  7. fmt = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 格式化
  8. fh.setFormatter(fmt) # 格式和文件句柄或者屏幕句柄关联
  9. sh.setFormatter(fmt)
  10. #吸星大法
  11. logger.addHandler(fh) # 吸收写文件功能 和logger关联的只有句柄
  12. logger.addHandler(sh) # 吸收输出屏幕功能
  13. logger.setLevel(logging.DEBUG) # 设置警告级别为debug,此处DEBUG源码为DEBUG = 10
  14. logger.debug('debug message')
  15. logger.info('info message')
  16. logger.warning('warning message')

执行输出:

2018-04-23 20:16:58,850 - root - DEBUG - debug message

2018-04-23 20:16:58,850 - root - INFO - info message

2018-04-23 20:16:58,850 - root - WARNING - warning message

查看文件内容,也是一样的。

那么为什么不能 Logger 来吸?

因为要解耦

写入和屏幕输出,可以不同

比如:

有一个需求,文件记录所有级别信息,屏幕只显示错误信息

那么解耦就很有必要了。

总结:

logging

logging 是记录日志的模块

它不能自己打印内容 只能根据程序员写的代码来完成功能

logging 模块提供 5 中日志级别,从低到高一次:debug info warning error critical

默认从 warning 模式开始显示

只显示一些基础信息,我们还可以对显示的格式做一些配置

简单配置 配置格式 basicCondfig

问题:编码问题,不能同时输出到文件和屏幕

logger 对象配置

高可定制化

首先创造 logger 对象

创造文件句柄 屏幕句柄

创造格式

使用文件句柄和屏幕句柄 绑定格式

logger 对象和句柄关联

logger.setLevel

使用的时候 logger.debug

四、collections 模块

在内置数据类型(dict、list、set、tuple)的基础上,collections 模块还提供了几个额外的数据类型:Counter、deque、defaultdict、namedtuple 和 OrderedDict 等。

1.namedtuple: 生成可以使用名字来访问元素内容的 tuple

2.deque: 双端队列,可以快速的从另外一侧追加和推出对象

3.Counter: 计数器,主要用来计数

4.OrderedDict: 有序字典

5.defaultdict: 带有默认值的字典

namedtuple

我们知道 tuple 可以表示不变集合,例如,一个点的二维坐标就可以表示成:

  1. p = (1, 2)

但是,看到(1, 2),很难看出这个 tuple 是用来表示一个坐标的。

这时,namedtuple 就派上了用场:

  1. from collections import namedtuple
  2. coordinate = namedtuple('Point',['x','y']) # 定义一个namedtuple类型Point,并包含x,y属性。
  3. p = coordinate(1,2) #创建一个p对象
  4. print(p)
  5. print(p.x) # 获取x属性
  6. print(p.y) # 获取y属性

执行输出:

Point(x=1, y=2)

1

2

Point 的名字,可以随便定义,它不是关键字

使用名字来取值,代码更清晰

这段代码,类似于面向对象,但是里面的属性不能修改,毕竟它是一个特殊的元组

修改属性值

  1. from collections import namedtuple
  2. coordinate = namedtuple('Point',['x','y']) # 定义一个namedtuple类型Point,并包含x,y属性。
  3. p = coordinate(1,2) #创建一个p对象
  4. print(p)
  5. print(p.x) # 获取x属性
  6. print(p.y) # 获取y属性
  7. p.x = 4 # 修改值
  8. print(p.x)

执行报错:

p.x = 4

AttributeError: can’t set attribute

看下面的代码,可读性比较差

  1. p1 = (0,1) # 表示x和y
  2. p2 = (0,2)

如果使用 p.x 和 p.y 分别表示 x 和 y 坐标,就比较清晰明了

应用场景

time 模块

面向对象进阶,纸牌游戏

五、deque

使用 list 存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为 list 是线性存储,数据量大的时候,插入和删除效率很低。

deque 是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:

  1. from collections import deque
  2. #双端队列
  3. import queue
  4. #队列 先进先出 fifo<br>#计算机数据结构模型<br>#先进先出<br><br># 栈 先进后出

队列,内部维护了严格的顺序

不能跳着取,必须一个一个取。

栈,先从后面取,写入也是从后面写

双端

可以从左边取,也可以从右边取

但是不能从中间取

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597799263021-e2252073-7fa0-472f-acc0-0a5005928374.png)
  1. from collections import deque
  2. #双端队列
  3. dq = deque()
  4. dq.append(1) # 往右边添加一个元素
  5. dq.append(2)
  6. dq.appendleft(3) # 往左边添加一个元素
  7. print(dq)
  8. print(dq.pop()) # 获取最右边一个元素,并在队列中删除
  9. print(dq.popleft()) # 获取最左边一个元素,并在队列中删除

执行输出:

deque([3, 1, 2])

2

3

队列是为了维护秩序的,

如果需要用到增删改查,不适合用队列

比如抢票,秒杀,会用到队列

比如 500 个,同时请求服务器,那么将这些人,放到队列中

队列它是有顺序的,即是是同一秒,也可以区分谁第一

那么前 10 个,就可以抢到票了,

不能打乱它的顺序

六、OrderedDict

使用 dict 时,Key 是无序的。在对 dict 做迭代时,我们无法确定 Key 的顺序。

如果要保持 Key 的顺序,可以用 OrderedDict:

自动维护 key 的顺序,顺序

那么这个列表的顺序就固定了

  1. dic = {'k1':'v1','k2':'v1','k3':'v1','k4':'v1'}
  2. keys = list(dic.keys()) # 转换为列表,那么顺序就固定了
  3. print(keys)
  4. for k in keys:
  5. print(k,dic[k])

执行输出:

[‘k2’, ‘k3’, ‘k4’, ‘k1’]

k2 v1

k3 v1

k4 v1

k1 v1

上面的代码有缺点,它不能维护 k1,k2,k3,k4 的顺序

下面介绍有序的字典

  1. dic = dict([('k1','v1'),('k2','v2')]) # 使用列表固定了顺序
  2. print(dic)

执行输出

{‘k1’: ‘v1’, ‘k2’: ‘v2’}

使用 OrderedDict 固定顺序

它内部,维护了一个列表

注意,OrderedDict 的 Key 会按照插入的顺序排列,不是 Key 本身排序:

from collections import OrderedDict

dic = OrderedDict([(‘k1’,’v1’),(‘k2’,’v2’)])

print(dic)

  1. from collections import OrderedDict
  2. dic = OrderedDict([('k1','v1'),('k2','v2')])
  3. print(dic)

执行输出:

{‘k1’: ‘v1’, ‘k2’: ‘v2’}

再增加 key,是从后面加

今日作业

写一个函数

参数是两个文件的路径

返回的结果是True/False

答案

  1. import hashlib
  2. def compare(f1,f2): # 校验2个文件是否一致
  3. def check(filename): # 计算文件的md5值
  4. md5obj = hashlib.md5()
  5. with open(filename, 'rb') as f:
  6. while True:
  7. content = f.read(1048576) # 每次读取1048576字节,也就是1MB
  8. if content:
  9. md5obj.update(content)
  10. else:
  11. break # 当内容为空时,终止循环
  12. return md5obj.hexdigest()
  13. f_1 = check(f1) # 计算f1的md5
  14. f_2 = check(f2)
  15. if f_1 == f_2: # 判断md5值
  16. return True
  17. else:
  18. return False
  19. ret = compare('file1','file2') # 校验2个已经存在的文件是否一致
  20. print(ret)

老师的代码:

  1. import hashlib
  2. def compare(filename1,filename2):
  3. md5sum = []
  4. for file in [filename1,filename2]:
  5. md5 = hashlib.md5()
  6. with open(file,'rb') as f:
  7. while True:
  8. content = f.read(1024)
  9. if content:
  10. md5.update(content)
  11. else:break
  12. md5sum.append(md5.hexdigest())
  13. if md5sum[0] == md5sum[1]:return True
  14. else :return False
  15. print(compare('f1','f2'))

明日默写

import logging

logger = logging.getLogger()
# 创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log',encoding='utf-8')

# 再创建一个handler,用于输出到控制台
ch = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setLevel(logging.DEBUG)

fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh) #logger对象可以添加多个fh和ch对象
logger.addHandler(ch)

logger.debug('logger debug message')
logger.info('logger info message')
logger.warning('logger warning message')
logger.error('logger error message')
logger.critical('logger critical message')