一、复习

反射 必须会 必须能看懂 必须知道在哪儿用

hasattr getattr setattr delattr

内置方法 必须能看懂 能用尽量用

len len(obj)的结果依赖于 obj.len()的结果,计算对象的长度

hash hash(obj)的结果依赖于 obj.hash()的结果,计算对象的 hash 值

eq obj1 == obj2 的结果依赖于 obj.eq()的结果,用来判断值相等

str str(obj) print(obj) ‘%s’%obj 的结果依赖于str,用来做输出、显示

repr repr(obj) ‘%r’%obj 的结果依赖于repr,还可以做 str 的备胎

format format() 的结果依赖于format的结果,是对象格式化的

call obj()相当于调用call,实现了call的对象是 callable 的

new 构造方法,在执行init之前执行,负责创建一个对象,在单例模式中有具体的应用

del 析构方法,在对象删除的时候,删除这个对象之前执行,主要用来关闭在对象中打开的系统的资源

  1. class A:
  2. def __getitem__(self, item):
  3. print(item)
  4. a = A()
  5. a['bbb'] # 对象['值'] 触发了__getitem__

执行输出:

bbb

  1. __getitem__ 对象[]的形式对对象进行增删改查
  2. __setitem__
  3. __delitem__
  4. __delattr__ del obj.attr 用来自定义删除一个属性的方法

单例模式

只有一个对象 只开了一个内存空间

创建一个类 单例模式中的对象属性编程类中的静态属性,所有的方法变成类方法

java 的单例模式,是依赖new来完成的

设计模式 —— java

python 中的单例模式 是使用new

只要执行了析构函数,说明对象要删除了

delattr 不需要重写,python 自带

面试题:

写一个类 定义 100 个对象

拥有三个属性 name age sex

如果两个对象的 name 和 sex 完全相同

我们就认为这是一个对象

忽略 age 属性

做这 100 个对象的去重工作

可哈希,跟不可变没有直接关系

开发这个算法的人,叫哈希

hash 算法 一个值 进行一系列的计算得出一个数字在一次程序执行中总是不变

来让每一个不同的值计算出的数字都不相等

创建一个对象

  1. class Person:
  2. def __init__(self,name,age,sex):
  3. self.name = name
  4. self.age = age
  5. self.sex = sex
  6. def __hash__(self):
  7. # hash算法本身就存在了 且直接在python中就能调用
  8. # 姓名相同 性别相同的对象的hash值应该相等才行
  9. # 姓名性别都是字符串
  10. return hash(self.name+self.sex)
  11. def __eq__(self, other):
  12. if self.name == other.name and self.sex == other.sex:
  13. return True
  14. obj_lst = []
  15. #手动创建8个对象,将对象写入列表
  16. obj_lst.append(Person('alex', 80, 'male'))
  17. obj_lst.append(Person('alex', 70, 'male'))
  18. obj_lst.append(Person('alex', 60, 'male'))
  19. obj_lst.append(Person('boss_jin', 50, 'male'))
  20. obj_lst.append(Person('boss_jin', 40, 'male'))
  21. obj_lst.append(Person('boss_jin', 30, 'male'))
  22. obj_lst.append(Person('nezha', 20, 'male'))
  23. obj_lst.append(Person('nezha', 10, 'male'))
  24. #列表去重
  25. obj_lst = set(obj_lst)
  26. #打印列表
  27. for obj in obj_lst:print(obj.name)

执行输出:

alex

boss_jin

nezha

set 对一个序列对象(有索引的对象)去重,依赖于这个对象的两个方法 hash 和 wq

key hash 数字 —》 内存地址 —》value

set hash 数字 —》 内存地址 —》set 中的元素

‘aaa’ hash A

如果值存在,则覆盖

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597920-b8c50934-c934-45b6-a274-baf4430319ed.png)

那么问题来了:

判断第 100 个元素时,需要做 100 次eq

set 对一个对象序列去重,如何判断这 2 个值是否相等

值 a 进行 hash —> 存值

值 b 进行 hash —> 判断值是否相等 -相等-> 说明是一样的

不相等-> 在开辟一个空间 来存放 b

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597815-4a0e81d0-7408-4376-848a-74f75dd360d2.png)

因为 set 依赖hasheq

所以在类里面,重新定义这 2 个方法,可以做个性化需求

最终得到我们想要的结果。

二、序列化模块 json,pickle,shelve

什么是模块?

常见的场景:一个模块就是一个包含了 python 定义和声明的文件,文件名就是模块名字加上.py 的后缀。

简单来说,py 文件就是模块

python 之所以好用,就是因为模块多

比如:

itchat 微信模块,模拟微信操作,自动回复

beautiful soap 爬虫工具

selenium 网页自动化测试工具

diango tornado web 前段框架

所有和 python 有关的模块,都在这个网站上面

https://pypi.org/

python 模块分为 3 种:

1.内置模块 python 安装时自带的

2.扩展模块 比如上面提到的 itchat 等等

3.自定义模块 自己写的模块

什么叫序列化——将原本的字典、列表等内容转换成一个字符串的过程就叫做序列化。

序列化的目的

1、以某种存储形式使自定义对象持久化;

2、将对象从一个地方传递到另一个地方。

3、使程序更具维护性。

比如 a 发送一个数据给 b

由于发送数据,必须是二进制的。所以需要经历编码到解码的过程

  1. dic = {'a':(1,2,3)}
  2. s = str(dic).encode(encoding='utf-8') # 编码
  3. ret = s.decode(encoding='utf-8') # 解码
  4. print(ret) # 查看数据
  5. print(type(ret)) # 查看类型
  6. print(type(eval(ret))) # 还原为字典

执行输出:

{‘a’: (1, 2, 3)}

使用 eval 不安全,有可能是病毒,接收方,啪的一些,就执行了。

这个时候,就需要用到序列化了。

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597879-520e2a03-6dd6-4944-b974-459572cd1e8c.png)

dic —> 字符串 序列化

字符串 —> dic 反序列化

序列化 == 创造一个序列 ==》创造一个字符串

实例化 == 创造一个实例

python 中的序列化模块

json 所有的编程语言都通用的序列化格式

它支持的数据类型非常有限 数字 字符串 列表 字典

pickle 只能在 python 语言的程序之间传递数据用的

pickle 支持 python 中所有的数据类型

shelve python3.* 之后才有的

Json

Json 模块提供了四个功能:dumps、dump、loads、load

序列化

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. ret = json.dumps(dic) # 序列化
  4. print(type(dic),dic) # 查看原始数据类型
  5. print(type(ret),ret) # 查看序列化后的数据

执行输出:

{‘慕容美雪’: (170, 60, ‘赏花’)}

{“\u6155\u5bb9\u7f8e\u96ea”: [170, 60, “\u8d4f\u82b1”]}

从结果中,可以看出:

原始数据类型是字典,序列化之后,就是字符串类型。而且中文变成了看不懂的字符串。

这是因为 json.dumps 序列化时对中文默认使用的 ascii 编码。

想输出真正的中文需要指定 ensure_ascii=False

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. ret = json.dumps(dic,ensure_ascii=False) # 序列化时,不使用ascii码
  4. print(type(dic),dic) # 查看原始数据类型
  5. print(type(ret),ret) # 查看序列化后的数据

执行输出:

{‘慕容美雪’: (170, 60, ‘赏花’)}

{“慕容美雪”: [170, 60, “赏花”]}

由于 json 不识别元组,json 认为元组和列表是一回事,所以变成了列表。

在 json 中,引号,统一使用双引号

反序列化

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. ret = json.dumps(dic,ensure_ascii=False) # 序列化时,不使用ascii码
  4. res = json.loads(ret) # 反序列化
  5. print(type(res),res) # 查看反序列化后的数据

执行输出:

{‘慕容美雪’: [170, 60, ‘赏花’]}

从结果中,可以看出,原来的单引号由还原回来了。

反序列化,比 eval 要安全。所以 eval 尽量少用。

dump 和 load 是直接将对象序列化之后写入文件

依赖一个文件句柄

dump 将序列化内容写入文件

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. f = open('美雪','w',encoding='utf-8')
  4. json.dump(dic,f) # 先接收要序列化的对象,再接收文件句柄
  5. f.close()

执行程序,查看文件美雪内容为:

{“\u6155\u5bb9\u7f8e\u96ea”: [170, 60, “\u8d4f\u82b1”]}

要想文件写入中文,可以加参数 ensure_ascii=False

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. f = open('美雪','w',encoding='utf-8')
  4. json.dump(dic,f,ensure_ascii=False) # 先接收要序列化的对象,再接收文件句柄
  5. f.close()

执行程序,再次查看文件内容:

{“慕容美雪”: [170, 60, “赏花”]}

load 读取文件中的序列化内容

  1. import json # 导入模块
  2. dic = {"慕容美雪":(170,60,'赏花')}
  3. f = open('美雪','r',encoding='utf-8')
  4. ret = json.load(f) #接收文件句柄
  5. print(ret) # 查看内容
  6. print(type(ret)) # 查看变量类型
  7. f.close() # 最后记得关闭文件句柄

执行输出:

{‘慕容美雪’: [170, 60, ‘赏花’]}

其他参数

  1. import json
  2. data = {'username':['李华','二愣子'],'sex':'male','age':16}
  3. json_dic2 = json.dumps(data,sort_keys=True,indent=4,separators=(',',':'),ensure_ascii=False)
  4. print(json_dic2)

执行输出:

  1. {
  2. "age":16,
  3. "sex":"male",
  4. "username":[
  5. "李华",
  6. "二愣子"
  7. ]
  8. }

执行结果和 PHP 的 pre 输出很相似,也是格式化输出

参数说明:

Skipkeys:默认值是 False,如果 dict 的 keys 内的数据不是 python 的基本类型(str,unicode,int,long,float,bool,None),设置为 False 时,就会报 TypeError 的错误。此时设置成 True,则会跳过这类 key

ensure_ascii:当它为 True 的时候,所有非 ASCII 码字符显示为\uXXXX 序列,只需在 dump 时将 ensure_ascii 设置为 False 即可,此时存入 json 的中文即可正常显示。)

indent:应该是一个非负的整型,如果是 0 就是顶格分行显示,如果为空就是一行最紧凑显示,否则会换行且按照 indent 的数值显示前面的空白分行显示,这样打印出来的 json 数据也叫 pretty-printed json

separators:分隔符,实际上是(item_separator, dict_separator)的一个元组,默认的就是(‘,’,’:’);这表示 dictionary 内 keys 之间用”,”隔开,而 KEY 和 value 之间用”:”隔开。

sort_keys:将数据根据 keys 的值进行排序。

下面有 3 个字典,如何写入文件?

  1. dic1 = {"S":(170,60,'唱歌')}
  2. dic2 = {"H":(170,60,'唱歌')}
  3. dic3 = {"E":(170,60,'唱歌')}

使用常规方法 json.dump 一行行写入,再次读取时,会报错。

下面介绍正确做法

写入多行

  1. import json
  2. dic1 = {"S":(170,60,'唱歌')}
  3. dic2 = {"H":(170,60,'唱歌')}
  4. dic3 = {"E":(170,60,'唱歌')}
  5. f = open('she','a',encoding='utf-8')
  6. f.write(json.dumps(dic1)+'\n') # 写入一行内容,注意,一定要加换行符
  7. f.write(json.dumps(dic2)+'\n')
  8. f.write(json.dumps(dic3)+'\n')
  9. f.close() # 关闭文件句柄

执行程序,查看文件 she 内容:

{“S”: [170, 60, “\u5531\u6b4c”]}

{“H”: [170, 60, “\u5531\u6b4c”]}

{“E”: [170, 60, “\u5531\u6b4c”]}

读取多行文件内容

  1. import json
  2. f = open('she','r',encoding='utf-8')
  3. for i in f:
  4. print(json.loads(i.strip()))
  5. f.close()

执行输出:

{‘S’: [170, 60, ‘唱歌’]}

{‘H’: [170, 60, ‘唱歌’]}

{‘E’: [170, 60, ‘唱歌’]}

总结:

dumps 序列化 loads 反序列化 只在内存中操作数据 主要用于网络传输 和多个数据与文件打交道

dump 序列化 load 反序列化 主要用于一个数据直接存在文件里—— 直接和文件打交道

看下面一段代码:

  1. import json
  2. dic = {(170,60,'唱歌'):"S"}
  3. print(json.dumps(dic))

执行报错:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597829-64ff6a50-ed75-4c26-aa53-cb0f264cfec5.png)

get 新知识点:

看 python 代码错误,要从下向上看。

第一个行,一般是错误点,看到蓝色的字体没有?鼠标单击一下,就会跳转,直接定位到错误在具体的哪一行

点击之后,看到是 print 这一行报错了

json 不支持元组 不支持除了 str 数据类型之外的 key

pickle

用于 python 特有的类型 和 python 的数据类型间进行转换

pickle 模块提供了四个功能:dumps、dump(序列化,存)、loads(反序列化,读)、load (不仅可以序列化字典,列表…可以把 python 中任意的数据类型序列化

pickle 的方法,和 json 是一样的,有 4 个

上一个例子的代码,pickle 可以序列化

dumps 序列化

  1. import pickle
  2. dic = {(170,60,'唱歌'):"S"}
  3. print(pickle.dumps(dic))

执行输出:

b’\x80\x03}q\x00K\xaaK<X\x06\x00\x00\x00\xe5\x94\xb1\xe6\xad\x8cq\x01\x87q\x02X\x01\x00\x00\x00Sq\x03s.’

输出结果是 bytes 类型的

dump 写入文件

文件模式必须带 b,因为它是 bytes 类型

  1. import pickle
  2. dic = {(170,60,'唱歌'):"S"}
  3. f = open('s','wb') #使用dump必须以+b的形式打开文件,编码不需要指定,因为是bytes类型
  4. pickle.dump(dic,f)
  5. f.close() # 注意要关闭文件句柄

执行程序,查看文件,文件内容是乱码的

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597919-87e05282-bfd4-4bfb-8918-1d2625dcb45b.png)

load 读取文件内容

  1. import pickle
  2. f = open('s','rb') # bytes类型不需要指定编码
  3. print(pickle.load(f))
  4. f.close() # 注意要关闭文件句柄

执行输出:

{(170, 60, ‘唱歌’): ‘S’}

dump 写入多行内容

  1. import pickle
  2. dic1 = {"张靓颖":(170,60,'唱歌')}
  3. dic2 = {"张韶涵":(170,60,'唱歌')}
  4. dic3 = {"梁静茹":(170,60,'唱歌')}
  5. f = open('singer','wb')
  6. pickle.dump(dic1,f)
  7. pickle.dump(dic2,f)
  8. pickle.dump(dic3,f)
  9. f.close()

执行程序,查看文件内容,是乱码的

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597938-c26db482-301d-48e1-9512-78ed905e015a.png)

load 读取文件内容

  1. import pickle
  2. f = open('singer','rb')
  3. print(pickle.load(f))
  4. print(pickle.load(f))
  5. print(pickle.load(f))
  6. print(pickle.load(f)) # 多读取一行,就会报错
  7. f.close()

执行输出:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597863-a87331d6-cc1e-4f72-b7f2-f5c09cbebf21.png)

为了解决这个问题,需要用到 while 循环+try

  1. import pickle
  2. f = open('singer','rb')
  3. while True:
  4. try:
  5. print(pickle.load(f))
  6. except Exception: # 接收一切错误
  7. break # 跳出循环
  8. f.close()

执行输出:

{‘张靓颖’: (170, 60, ‘唱歌’)}

{‘张韶涵’: (170, 60, ‘唱歌’)}

{‘梁静茹’: (170, 60, ‘唱歌’)}

总结:

json 在写入多次 dump 的时候 不能对应执行多次 load 来取出数据,pickle 可以

json 如果要写入多个元素 可以先将元素 dumps 序列化,f.write(序列化+’\n’)写入文件

读出元素的时候,应该先按行读文件,在使用 loads 将读出来的字符串转换成对应的数据类型


关于序列化自定义类的对象

  1. class A:
  2. def __init__(self,name,age):
  3. self.name=name
  4. self.age=age
  5. a = A('alex',80)
  6. import json
  7. json.dumps(a)

执行报错:

TypeError: <__main__.A object at 0x0000022B1BB2BF28> is not JSON serializable

json 不能序列化对象

使用 pickle 序列化

  1. import pickle
  2. class A:
  3. def __init__(self,name,age):
  4. self.name=name
  5. self.age=age
  6. a = A('alex',80)
  7. ret = pickle.dumps(a) # 序列化对象
  8. print(ret)
  9. obj = pickle.loads(ret) # 反序列化
  10. print(obj.__dict__) # 查看对象属性<br>f.close()

执行输出:

b’\x80\x03cmain\nA\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x04\x00\x00\x00alexq\x04X\x03\x00\x00\x00ageq\x05KPub.’

{‘name’: ‘alex’, ‘age’: 80}

将对象 a 写入文件

  1. import pickle
  2. class A:
  3. def __init__(self,name,age):
  4. self.name=name
  5. self.age=age
  6. a = A('alex',80)
  7. f = open('a','wb')
  8. obj = pickle.dump(a,f)
  9. f.close()

执行程序,查看文件内容,是乱码的

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597823-fccdb747-bf47-4886-92bb-62179dea9660.png)

假设是一款 python 游戏,就可以将人物的属性,写入文件。

再次登录时,就可以重新加载了。用 pickle 就比较方便了。

当删除一个类的时候(注释代码),再次读取文件,就会报错

  1. import pickle
  2. # class A:
  3. # def __init__(self,name,age):
  4. # self.name=name
  5. # self.age=age
  6. # a = A('alex',80)
  7. f = open('a','rb')
  8. obj = pickle.load(f)
  9. print(obj.__dict__)
  10. f.close()

执行报错

AttributeError: Can’t get attribute ‘A’ on

提示找不到类 A

将对象反序列时,必须保证该对象的类必须存在,否则读取报错

再次打开注释,执行以下,就正常了。

三、shelve

shelve 也是 python 提供给我们的序列化工具,比 pickle 用起来更简单一些。

shelve 只提供给我们一个 open 方法,是用 key 来访问的,使用起来和字典类似。

特点:

1、shelve 模块是一个简单的 key,value 将内存数据通过文件持久化的模块。

2、shelve 模块可以持久化任何 pickle 可支持的 python 数据格式。

3、shelve 就是 pickle 模块的一个封装。

4、shelve 模块是可以多次 dump 和 load。

优点:轻量级,键值存储系统;

缺点:

适合读,不适合更新,在 writeback 时会把所有的数据都重新写入,结果就是可能消耗内存很多,写入耗时;

不能并发的读写,不过可以使用 Unix 文件锁进行控制(fcntl);

  1. import shelve # python专有的序列化模块
  2. f = shelve.open('shelve_file') # 打开文件
  3. f['key'] = {'int':10, 'float':9.5, 'string':'Sample data'} #直接对文件句柄操作,就可以存入数据
  4. f.close()

执行程序,会产生 3 个文件

shelve_file.bak

shelve_file.dat

shelve_file.dir

这和 MySQL 的 MyISAM 表,有点类似。创建一个表时,也会创建 3 个文件。

读取文件

  1. import shelve
  2. f1 = shelve.open('shelve_file')
  3. existing = f1['key'] #取出数据的时候也只需要直接用key获取即可,但是如果key不存在会报错
  4. f1.close()
  5. print(existing)

执行输出:

{‘string’: ‘Sample data’, ‘int’: 10, ‘float’: 9.5}

这个模块有个限制,它不支持多个应用同一时间往同一个 DB 进行写操作。所以当我们知道我们的应用如果只进行读操作,我们可以让 shelve 通过只读方式打开 DB

  1. import shelve
  2. f = shelve.open('shelve_file', flag='r') # flag = 'r' 表示只读方式
  3. existing = f['key']
  4. f.close()
  5. print(existing)

执行输出:

{‘float’: 9.5, ‘int’: 10, ‘string’: ‘Sample data’}

修改变量

  1. import shelve
  2. f = shelve.open('shelve_file', flag='r')
  3. f['key']['int'] = 50 # 修改一个值
  4. existing = f['key'] # 取值
  5. f.close()
  6. print(existing) # 打印结果

执行输出:

{‘string’: ‘Sample data’, ‘int’: 10, ‘float’: 9.5}

从结果上来,并没有改变,因为此时是只读模式

先把数据覆盖一次

  1. import shelve
  2. f = shelve.open('shelve_file')
  3. f['key'] = {'int':10, 'float':9.5, 'string':'Sample data'}
  4. f.close()

但是下面一种情况,是可以改变的

  1. import shelve
  2. f = shelve.open('shelve_file', flag='r')
  3. f['key']['int'] = 50 # 不能修改已有结构中的值
  4. f['key']['new'] = 'new' # 不能在已有的结构中添加新的项
  5. f['key'] = 'new' # 但是可以覆盖原来的结构
  6. existing = f['key'] # 取值
  7. f.close()
  8. print(existing) # 打印结果

执行输出:

new

明明是只读,却可以改。说明有 bug

shelve 尽量少用,有坑

新增一个属性

  1. import shelve
  2. #重新覆盖数据
  3. f = shelve.open('shelve_file')
  4. f['key'] = {'int':10, 'float':9.5, 'string':'Sample data'} #直接对文件句柄操作,就可以存入数据
  5. f.close()
  6. #新增一个属性
  7. f1 = shelve.open('shelve_file')
  8. f1['key']['new_value'] = 'this was not here before'
  9. print(f1['key'])
  10. f1.close()

执行输出:

{‘float’: 9.5, ‘int’: 10, ‘string’: ‘Sample data’}

发现新增的属性没有

由于 shelve 在默认情况下是不会记录待持久化对象的任何修改的,所以我们在 shelve.open()时候需要修改默认参数,否则对象的修改不会保存。

  1. import shelve
  2. #新增一个属性
  3. f1 = shelve.open('shelve_file', writeback=True)
  4. f1['key']['new_value'] = 'this was not here before'
  5. print(f1['key'])
  6. f1.close()

执行输出:

{‘new_value’: ‘this was not here before’, ‘string’: ‘Sample data’, ‘int’: 10, ‘float’: 9.5}

新增的属性就出现了

writeback 方式有优点也有缺点。优点是减少了我们出错的概率,并且让对象的持久化对用户更加的透明了;但这种方式并不是所有的情况下都需要,首先,使用 writeback 以后,shelf 在 open()的时候会增加额外的内存消耗,并且当 DB 在 close()的时候会将缓存中的每一个对象都写入到 DB,这也会带来额外的等待时间。因为 shelve 没有办法知道缓存中哪些对象修改了,哪些对象没有修改,因此所有的对象都会被写入。

shelve 由很多坑,不建议使用

推荐使用 Json 和 picker

四、hashlib 模块

Python 的 hashlib 提供了常见的摘要算法,如 MD5,SHA1 等等。

什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用 16 进制的字符串表示)。

摘要算法就是通过摘要函数 f()对任意长度的数据 data 计算出固定长度的摘要 digest,目的是为了发现原始数据是否被人篡改过。

摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算 f(data)很容易,但通过 digest 反推 data 却非常困难。而且,对原始数据做一个 bit 的修改,都会导致计算出的摘要完全不同。

hash 算法,每次执行,都会变。跟时间有关系

hash 哈希算法 可 hash 数据类型——>数字的过程

hashlib —— 摘要算法

也是一些算法的集合,有好多算法

字符串 —> 数字

不同的字符串 —> 数字一定不同

无论在哪台机器上,在什么时候计算,对相同的字符串结果总是一样的

摘要过程不可逆

用法:

文件的一致性校验

密文验证的时候加密

比如文件下载

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597951-15b1f2fc-1aba-4ddf-a617-0f8f74ad094d.png)

通过对文件做 SHA1 算法,判断下载的文件,和服务器的文件是否一致。就能知道文件是否下载完成。

密码验证

有一个密码文件,内容如下:

下划线左边是用户名,右边是密码

  1. ALEX|ALEX3714
  2. EGON|123456
  3. NAZHA|9876543

假如这个文件泄露了,那么别人就可以直接登录了。

所有的密码,都不应该明文存储,必须通过算法转换才行

比如把 ALEX3714 转换成 f58be8e9cb357ad3d59ec47b165c2b19

这段 32 位字符串,是无法解密成 ALEX3714 的

因为摘要过程不可逆

那么登录的时候,把密码通过算法转换,和文件的内容值做对比。如果一致,则登录成功。

下面介绍 2 种常用的算法

md5 算法 通用的算法

sha 算法 安全系数更高,sha 算法有很多种,后面的数字越大安全系数越高,

得到的数字结果越长,计算的时间越长。目前最高是 sha512

加密密码 ALEX3714

  1. import hashlib
  2. m = hashlib.md5()
  3. m.update('ALEX3714'.encode('utf-8')) # update只接收bytes类型
  4. print(m.hexdigest()) # 返回摘要,作为十六进制数据字符串值

执行输出:

f58be8e9cb357ad3d59ec47b165c2b19

返回结果是一个 32 位长度的十六进制数据字符串,结果是定长的。

hexdigest 这个单词,Pycharm 没有自动补全。怎么记呢?hex 表示十六进制,digest 表示摘要

理论上来讲,md5 不能反解

无论在哪台机器上,在什么时候计算,对相同的字符串结果总是一样的

针对这个特点,有专门的 md5 破解网站,将所有常见的密码,进行 md5 计算

将返回的摘要结果写入数据库,比如

  1. 123 -> 202cb962ac59075b964b07152d234b70
  2. 123456 -> e10adc3949ba59abbe56e057f20f883e
  3. abc -> 900150983cd24fb0d6963f7d28e17f72
  4. ...

那么就可以通过摘要反向得出对应的密码

这种行为称之为暴力破解撞库

为了确保存储的用户口令不是那些已经被计算出来的常用口令的 MD5

通过对原始口令加一个复杂字符串来实现,俗称”加盐”:

  1. hashlib.md5("salt".encode("utf8"))

经过 Salt 处理的 MD5 口令,只要 Salt 不被黑客知道,即使用户输入简单口令,也很难通过 MD5 反推明文口令。

对 12345 做加盐处理

  1. import hashlib
  2. m = hashlib.md5('wahaha'.encode('utf-8'))
  3. m.update('123456'.encode('utf-8'))
  4. print(m.hexdigest())

执行输出:

d1c59b7f2928f9b1d63898133294ad2c

将这段密文复制到一下网站

http://www.cmd5.com/

就不那么容易反解出来了

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597878-07ee3c4b-2da9-475c-bf58-52c2cfe17bd9.png)

我擦勒,特么的,计算出来了???

请忽略我说的上一句话

注册和登录校验,都要用加盐

就算数据库泄露,也不影响,因为加盐了

除非盐被泄露了,才能破解登录

黑客注册 500 个用户,密码都不一样

只要拿到数据库,通过一定的规律,也可以反解出来加密盐

那怎么办呢?

有更高级的办法,动态加盐

  1. import hashlib
  2. username = 'alex' # 用户名
  3. m = hashlib.md5(username.encode('utf-8')) # 密码盐为用户名
  4. m.update('123456'.encode('utf-8'))
  5. print(m.hexdigest())

执行输出:

94e4ccf5e2749b0bfe0428603738c0f9

因为每个用户名是不一样的,即便是注册了 500 个账号,也不能随便破解

再高级一点的密码盐

  1. import hashlib
  2. username = 'alex' # 用户名
  3. m = hashlib.md5(username[:2:2].encode('utf-8')) # 密码盐为用户名
  4. m.update('123456'.encode('utf-8'))
  5. print(m.hexdigest())

执行输出:

dc483e80a7a0bd9ef71d8cf973673924

再试一波看看

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597914-aeee90ae-19e9-41eb-bd28-d5d472a8c724.png)

卧槽!不行,我得再升级一下

双层密码盐

  1. import hashlib
  2. username = 'alex' # 用户名
  3. salt = 'what the fuck!叫你破解!叫你破解!你来啊!!!'
  4. m = hashlib.md5((username+salt).encode('utf-8')) # 双层密码盐
  5. m.update('123456'.encode('utf-8'))
  6. print(m.hexdigest())

我就不信这个邪!

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597797597914-4cac36ca-05b5-47d5-85f7-3a2ffe0017da.png)

看吧,破解不了吧。

只要加盐方法,不被泄露就行。登录以及注册,务必要加盐。