大家好,又见面了,我是你们的朋友全栈君。
1:正则表达式:正则表达式是包含文本和特殊字符的字符串,该字符串描述一个可以识别各种字符串的模式
[A-Za-z]\w+ 的含义是第一个字符是字母,也就是说要么A~Z,要么a~z,后面是至少一个(+)由字母数字组成的字符(\w)
管道符号(|) 表示择一匹配,,表示一个“从多个模式中选择其一”的操作。它用于分割不同的正则表达式。
有了这个符号,就能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不仅仅只是一个字符串。择一匹配有时候也称作并(union)或者逻辑或(logical OR)
句点(.) 匹配除了换行符\n以外的任意单个字符(Python正则表达式有一个编译标记[S或者DOTALL],该标记能够推翻这个限制,使点号能够匹配换行符)
无论字母、数字、空格(并不包括“\n”换行符)、可打印字符、不可打印字符,还是一个符号,使用点号都能够匹配它们
“\.” 怎样才能匹配句点(dot)或者句号(period)字符?,要显式匹配一个句点符号本身,必须使用反斜线转义句点符号的功能,例如“\.”
脱字符(^)或者特殊字符\A(反斜线和大写字母A) 字符串的起始部分指定用于搜索的模式,匹配字符串的开始位置
美元符号($)或者\Z 匹配字符串的末尾位置。
如果想要逐字匹配这些字符中的任何一个(或者全部),就必须使用反斜线进行转义,就必须使用反斜线进行转义
如果你想要匹配任何以美元符号结尾的字符串,一个可行的正则表达式方案就是使用模式.*$$
\b和\B 特殊字符\b和\B可以用来匹配字符边界。而两者的区别在于\b将用于匹配一个单词的边界,
这意味着如果一个模式必须位于单词的起始部分,就不管该单词前面(单词位于字符串中间)是否有任何字符(单词位于行首)。
同样,\B将匹配出现在一个单词中间的模式(即,不是单词边界)
res = re.match(r"\bthe\b", "the") 记得加 r 转义,让 \b保持原本的含义
[] 方括号能够匹配一对方括号中包含的任何字符 [abc] 匹配a,b,c任意字符都可以,匹配一次
关于[cr][23][dp][o2]这个正则表达式有一点需要说明,如果仅允许“r2d2”或者“c3po”作为有效字符串,就需要更严格限定的正则表达式
因为方括号仅仅表示逻辑或的功能,所以使用方括号并不能实现这一限定要求。
唯一的方案就是使用择一匹配,例如,r2d2|c3po 匹配r2d2或者c3po
“ab” 该正则表达式只匹配包含字母“a”且后面跟着字母“b”的字符串,
[ab] 要么匹配“a”,要么匹配“b”,此时字母“a”和字母“b”是相互独立的字符串,字符集的方法只适合单个字符串
a|b 要么匹配“a”,要么匹配“b”
(-) 方括号中两个符号中间用连字符(-)连接,用于指定一个字符的范围
A-Z、a-z或者0-9分别用于表示大写字母、小写字母和数值数字,这是一个按照字母顺序的范围,所以不能将它们仅仅限定用于字母和十进制数字上
脱字符(^) 这个符号就表示不匹配给定字符集中的任何一个字符。
(*) 将匹配其左边的正则表达式出现零次或者多次的情况(在计算机编程语言和编译原理中,该操作称为Kleene闭包)
(+) 操作符将匹配一次或者多次出现的正则表达式(也叫做正闭包操作符)
(?) 操作符将匹配零次或者一次出现的正则表达式
({}) 里面或者是单个值或者是一对由逗号分隔的值。这将最终精确地匹配前面的正则表达式N次(如果是{N})或者一定范围的次数;
例如,{M,N}将匹配M~N次出现。这些符号能够由反斜线符号转义;\*匹配星号,等等
如果问号紧跟在任何使用闭合操作符的匹配后面,它将直接要求正则表达式引擎匹配尽可能少的次数
\d 表示匹配任何十进制数字
\w 能够用于表示全部字母数字的字符集,相当于[A-Za-z0-9_]的缩写形式
\s 可以用来表示空格字符。 这些特殊字符的大写版本表示不匹配;例如,\D表示任何非十进制数(与[^0-9]相同),等等。
使用圆括号指定分组 不仅想要知道整个字符串是否匹配我们的标准,而且想要知道能否提取任何已经成功匹配的特定字符串或者子字符串
一对圆括号可以实现以下任意一个(或者两个)功能:
一:对正则表达式进行分组;
二:匹配子组
(\w+)-(\d+)就能够分别访问每一个匹配子组。我们更倾向于使用子组,这是因为择一匹配通过编写代码来判断是否匹配,
然后执行另一个单独的程序(该程序也需要另行创建)来解析整个匹配仅仅用于提取两个部分。为什么不让Python自己实现呢?
这是re模块支持的一个特性,所以为什么非要重蹈覆辙呢?
(?…) 通常用于在判断匹配之前提供标记,实现一个前视(或者后视)匹配,或者条件检查,尽管圆括号使用这些符号,但是只有(?P<name>)表述一个分组匹配。
所有其他的都没有创建一个分组。然而,你仍然需要知道它们是什么,因为它们可能最适合用于你所需要完成的任务。
(?#comment) 此处不做匹配,只是作为注释
(?=.com) 如果一个字符串后面跟着".com"才做匹配,
(?:) 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来
import re
a = "123abc456ww"
pattern = "([0-9]*)([a-z]*)([0-9]*)"
print(re.search(pattern, a).group(0, 1, 2, 3))
pattern = "(?:[0-9]*)([a-z]*)([0-9]*)"
print(re.search(pattern, a).group(0, 1, 2)) #(?:[0-9]*) 这个分组的值不会保持
python中group(0)返回匹配到的整体
(?=pattern) 正向肯定预查(look ahead positive assert),匹配pattern前面的位置。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。
xxx(?=pattern)为例,就是捕获以pattern结尾的内容xxx
"Windows(?=95|98|NT|2000)"能匹配"Windows2000"中的"Windows",
但不能匹配"Windows3.1"中的"Windows"。预查不消耗字符,
也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
import re
a = "Windows20001235"
pattern = "Windows(?:95|98|NT|2000)"
print(re.search(pattern, a).group(0)) #匹配Windows2000
import re
a = "Windows20001235"
pattern = "Windows(?=95|98|NT|2000)"
print(re.search(pattern, a).group(0)) #匹配Windows 后面的(?=95|98|NT|2000)匹配到的字段不显示
(?!pattern) 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。
xxx(?!pattern)为例,就是捕获不以pattern结尾的内容xxx
"Windows(?!95|98|NT|2000)"能匹配"Windows3.1"中的"Windows",但不能匹配"Windows2000"中的"Windows"。
预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?<=pattern) 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。
以(?<=pattern)xxx为例,就是捕获以pattern开头的内容xxx。
"(?<=95|98|NT|2000)Windows
" 能匹配"2000Windows
"中的"Windows
",但不能匹配"3.1Windows
"中的"Windows
"。
(?<!pattern) (?<!pattern)xxx为例,就是捕获不以pattern开头的内容xxx。
反向否定预查,与正向否定预查类似,只是方向相反。
例如"(?<!95|98|NT|2000)Windows
"能匹配"3.1Windows
"中的"Windows
",但不能匹配"2000Windows
"中的"Windows
"
(?(1)y|x) 如果一个匹配组1()存在,就与y匹配,否则,就与x匹配
(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)是一个匹配邮件格式的正则表达式,可以匹配 user@fishc.com 和 ‘user@fishc.com’,
但是不会匹配user@fishc.com、user@fishc.com
(?i) 该表达式右边的字符忽略大小写
(?-i) 该表达式右边的字符区分大小写
(?i:x) x 忽略大小写
(?-i:x) x 区分大小写
?和懒惰匹配——尽可能少的匹配,例如:源字符串str=“dxxddxxd”
中,d\w*?
会匹配 dx,而d\w*?d
会匹配 dxxd
2:python re模块的使用
re.compile() 正则表达式预编译
其实模块函数会对已编译的对象进行缓存,所以不是所有使用相同正则表达式模式的 search()和 match()都需要编译。
即使这样,你也节省了缓存查询时间,并且不必对于相同的字符串反复进行函数调用。
在不同的 Python 版本中,缓存中已编译过的正则表达式对象的数目可能不同,而且没有文档记录。purge()函数能够用于清除这些缓存。
group() 要么返回整个匹配对象,要么根据要求返回特定子组。
groups() 仅返回一个包含唯一或者全部子组的元组。如果没有子组的要求,那么当group()仍然返回整个匹配时,groups()返回一个空元组。
match() 试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象;
如果匹配失败,就返回 None,匹配对象的 group()方法能够用于显示那个成功的匹配
search() 在一个字符串中查找模式(搜索与匹配的对比),search()的工作方式与match()完全一致,
不同之处在于search()会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;
否则,返回None
等价的正则表达式对象方法使用可选的pos和endpos参数来指定目标字符串的搜索范围
bt = 'bat|bet|bit' 正则表达式模式:bat、bet、bit,m = re.match(bt, 'bat') 'bat' 是一个匹配,择一匹配符号|
. 单号匹配单个字符串
[] [cr][23][dp][o2]和r2d2|c3po之间的差别
[cr][23][dp][o2] 匹配四个字段,随机从四个[]里选一个
r2d2|c3po 只匹配r2d2和c3po两种结果
() 分组:
m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
m.group() #abc-123
m.group(1) #"abc"
m.group(2) #123
m.groups() #('abc', '123')
group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。可以使用groups()方法来获取一个包含所有匹配子字符串的元组。
m = re.match('(a(b))', 'ab') # 两个子组
m.group() # 完整匹配
‘ab’
m.group(1) # 子组 1
‘ab’
m.group(2) # 子组 2
’b’
m.groups() # 所有子组 (’ab’, ‘b’)
^ 匹配字符串得起始:
m = re.search(’^The’, ‘The end.’) # 匹配 匹配:The
\b匹配单词边界 m = re.search(r’\bthe’, ‘bite the dog’) # 在边界 匹配:the \b匹配单词边界
m = re.search(r’\bthe’, ‘bitethe dog’) # 匹配单词边界,没有以the开头得单词,匹配不上
m = re.search(r’\Bthe’, ‘bitethe dog’) # 不匹配边界,ok
r”” 通常情况下,在正则表达式中使用原始字符串是个好主意
findall() 查询字符串中某个正则表达式模式全部的非重复出现情况。与 search()在执行字符串搜索时类似,
但与match()和search()的不同之处在于,findall()总是返回一个列表
如果 findall()没有找到匹配的部分,就返回一个空列表,但如果匹配成功,列表将包含所有成功的匹配部分(从左向右按出现顺序排列)
m = re.findall('(the).*?(123)', 'bitethe dog the123')
print(m) #输出:[('the', '123')]
print(m[0][0]) #输出:the
finditer() 返回一个迭代器,finditer()在匹配对象中迭代。
s = 'This and that.'
re.findall(r'(th\w+) and (th\w+)', s, re.I) 返回:[('This', 'that')]
re.finditer(r'(th\w+) and (th\w+)', s, re.I) 返回:迭代器,需要.next().groups()
finditer()的用法如下:
import re
s = 'This and that.'
res = re.findall(r'(th\w+) and (th\w+)', s, re.I) #[('This', 'that')]
res = re.finditer(r'(th\w+) and (th\w+)',s, re.I)
print(res.__next__().groups()) #('This', 'that')
res = re.finditer(r'(th\w+) and (th\w+)', s, re.I)
print(res.__next__().group(1)) #this
res = re.finditer(r'(th\w+) and (th\w+)',s, re.I)
print(res.__next__().group(2)) #that
res = [g.groups() for g in re.finditer(r'(th\w+) and (th\w+)', s, re.I) ]
print(res) #[('This', 'that')]
findter()的用法二:
import re
s = 'This and that.'
res = re.findall(r'(th\w+)', s, re.I)
print(res) #['This', 'that']
it = re.finditer(r'(th\w+)', s, re.I)
g = it.__next__() #g 返回的是匹配的第一个元素对象
print(g.groups()) #('This',)
print(g.group(1)) #This
g2 = it.__next__() #g2 返回的是匹配的第二个元素对象
print(g2.groups()) #('that',)
print(g2.group(1)) #that
res = [g.group(1) for g in re.finditer(r'(th\w+)', s, re.I)]
print(res) #['This', 'that']
for循环遍历一个对象的本质就是这个对象调用__iter__方法转化成一个迭代器后使用__next__方法循环从迭代器里面取值
使用finditer()函数完成的所有额外工作都旨在获取它的输出来匹配findall()的输出
与match()和search()类似,findall()和finditer()方法的版本支持可选的pos和endpos参数,
这两个参数用于控制目标字符串的搜索边界,
sub()和subn()搜索与替换 有两个函数/方法用于实现搜索和替换功能:sub()和subn()。两者几乎一样,
都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,
但它也可能是一个函数,该函数返回一个用来替换的字符串。subn()和 sub()一样,但 subn()还返回一个表示替换的总数,
替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回
sub和subn的使用示例
import re
res = re.sub('X', 'Mr.Smith', 'attn:X\n\nDear X,\n')
print(res) #把所有的x替换成Mr.Smith,返回替换后的字符串
res = re.subn('X', 'Mr.Smith', 'attn:X\n\nDear X,\n')
print(res) #返回替换后的字符串和替换的次数,返回一个元组 :('attn:Mr.Smith\n\nDear Mr.Smith,\n', 2)
res = re.sub('[ae]', 'X', 'abcdef')
print(res) #把所有的a或者e都替换成X,返回替换后的字符串:XbcdXf
res = re.subn('[ae]', 'X', 'abcdef')
print(res) #('XbcdXf', 2)
除了使用匹配对象的group()方法除了能够取出匹配分组编号外,还可以使用\N,
其中 N 是在替换字符串中使用的分组编号。下面的代码仅仅只是将美式的日期表示法MM/DD/YY{,YY}格式转换为其他国家常用的格式DD/MM/YY{,YY}。
import re
res = re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', r'//', '2/20/91') #把(\d{1,2})匹配到的替换成(也就是字符串里面的第二段20)
print(res) #20/2/91
res = re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', r'//', '2/20/1991')
print(res) #20/2/1991
re.split 基于正则表达式的模式分隔字符串,如果你不想为每次模式的出现都分割字符串,就可以通过为max参数设定一个值(非零)来指定最大分割数。
如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么 re.split()与str.split()的工作方式相同,如下所示(基于单引号分割)
import re
res = re.split(':', 'str1:str2:str3')
print(res) #['str1', 'str2', 'str3']
DATA = (
'Mountain View, CA 94040',
'Sunnyvale, CA',
'Los Altos, 94023',
'Cupertino 95014',
'Palo Alto CA',
)
for datum in DATA:
print(re.split(',|(?= (?:\d{5}|[A-Z]{2})) ', datum)) #后面带空格
用户需要输入城市和州名,或者城市名加上ZIP编码,还是三者都同时输入才能识别,
使用 split 语句基于逗号分割字符串。
如果空格紧跟在五个数字(ZIP编码)或者两个大写字母(美国联邦州缩写)之后,就用split语句分割该空格。这就允许我们在城市名中放置空格。
(?i):该表达式右边的字符忽略大小写
(?m) :该表达式让右边的正则规则匹配的时候实现多行混合
(?iLmsux) re.I/IGNORECASE的示例和re.M/MULTILINE
import re
res = re.findall(r'(?i)yes', 'yes? Yes.YES!!')
print(res) #['yes', 'Yes', 'YES'] (?i):该表达式右边的字符忽略大小写
res = re.findall(r'(?i)th\w+', 'The quickest way is through this')
print(res) #['The', 'through', 'this']
rule = r'(?im)(^th[\w]+)' #^匹配每行一th开头的
content = '''
This line is the first,
another line,
that line, it's the best
'''
r = re.findall(rule, content)
print(r) #['This', 'that']
在前两个示例中,显然是不区分大小写的。在最后一个示例中,通过使用“多行”,能够在目标字符串中实现跨行搜索,
而不必将整个字符串视为单个实体。注意,此时忽略了实例“the”,因为它们并不出现在各自的行首
(?s) 表明点号(.)能够用来匹配\n符号(反之其通常用于表示除了\n之外的全部字符):
(?x) 该标记允许用户通过抑制在正则表达式中使用空白符(除了在字符类中或者在反斜线转义中)来创建更易读的正则表达式。
此外,散列、注释和井号也可以用于一个注释的起始,只要它们不在一个用反斜线转义的字符类中
注释模式,可以忽略表达式中的空格、换行,以及 # 开始到换行的所有字符
import re
res = re.findall(r'th.+', '''
The first line
the second line
the third line
''')
print(res) # ['the second line', 'the third line']
res = re.findall(r'(?s)th.+', '''
The first line
the second line
the third line
''')
print(res) # ['the second line\nthe third line\n']
# (?s)能够让 . 匹配空字符串
res = re.search(r'''(?x)
\((\d{3})\)
[]
(\d{3})
-
(\d{4})
''', '(800) 555-1212')
(?:…) 符号将更流行;通过使用该符号,可以对部分正则表达式进行分组,但是并不会保存该分组用于后续的检索或者应用。当不想保存今后永远不会使用的多余匹配时,这个符号就非常有用。
import re
res = re.findall(r'http://(?:\w+\.)*(\w+\.com)', 'http://google.com http://www.google.com http://code.google.com')
print(res) # ['google.com', 'google.com', 'google.com']
res = re.search(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})', '(800) 555-1212')
print(res.groups()) # {'areacode': '800', 'prefix': '555'}
可以同时一起使用 (?P<name>) 和 (?P=name)符号。前者通过使用一个名称标识符而不是使用从 1 开始增加到 N 的增量数字来保存匹配,
如果使用数字来保存匹配结果,我们就可以通过使用, ……,\N \来检索。如下所示,可以使用一个类似风格的\g<name>来检索它们
res = re.findall( r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})', '(800) 555-1212')
print(res) #[('800', '555')]
res = re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})', '(\g<areacode>) \g<prefix>-xxxx', '(800) 555-1212')
print(res) # (800) 555-xxxx \g<areacode> 替换成找到的分组的字段
使用后者,可以在一个相同的正则表达式中重用模式,而不必稍后再次在(相同)正则表达式中指定相同的模式。
例如,在本示例中,假定让读者验证一些电话号码的规范化。如下所示为一个丑陋并且压缩的版本,后面跟着一个正确使用的 (?x),使代码变得稍许易读
import re
res = bool(re.match(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?P<number>\d{4}) (?P=areacode)-(?P=prefix)-(?P=number)
\(?P=areacode)(?P=prefix)(?P=number)',
'(800) 555-1212 800-555-1212 18005551212'))
print(res) #True
(?=……) 和 (?!…) 前视匹配,而不必实际上使用这些字符串。前者是正向前视断言,后者是负向前视断言。
在后面的示例中,我们仅仅对姓氏为“van Rossum”的人的名字感兴趣,
下一个示例中,让我们忽略以“noreply”或者“postmaster”开头的e-mail地址。
正则:82
3:sorket网络编程
1:服务器就是一系列硬件或软件,为一个或多个客户端(服务的用户)提供所需的“服务”。它存在唯一目的就是等待客户端的请求,并响应它们(提供服务),然后等待更多请求
2:客户端因特定的请求而联系服务器,并发送必要的数据,然后等待服务器的回应,最后完成请求或给出故障的原因。
服务器无限地运行下去,并不断地处理请求;而客户端会对服务进行一次性请求,然后接收该服务,最后结束它们之间的事务。
客户端在一段时间后可能会再次发出其他请求,但这些都被当作不同的事务
3:在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作做准备。
首先会创建一个通信端点,它能够使服务器监听请求。可以把服务器比作公司前台,
或者应答公司主线呼叫的总机接线员。一旦电话号码和设备安装成功且接线员到达时,服务就可以开始了
4:套接字是计算机网络数据结构,它体现了上节中所描述的“通信端点”的概念。在任何类型的通信开始之前,
网络应用程序必须创建套接字。可以将它们比作电话插孔,没有它将无法进行通信
套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。
这就是所谓的进程间通信(Inter Process Communication,IPC)。有两种类型的套接字:基于文件的和面向网络的。
5:所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口
6:Python只支持AF_UNIX、AF_NETLINK、AF_TIPC和AF_INET家族。因为本章重点讨论网络编程,所以在本章剩余的大部分内容中,我们将使用AF_INET
AF_UNIX、:基于文件的套件字
AF_NETLINK:允许使用标准的BSD套接字接口进行用户级别和内核级别代码之间的IPC
AF_TIPC和:明的进程间通信(TIPC)协议
AF_INET:基于网络的套件字
4:面向连接的套接字:SOCK_STREAM
1:不管你采用的是哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在进行通信之前必须先建立一个连接,
例如,使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,
并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序
实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写 TCP)。为 了创建 TCP 套接字,
必须使用 SOCK_STREAM 作为套接字类型。TCP 套接字的名字SOCK_STREAM 基于流套接字的其中一种表示。
因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合这两种协议(TCP和IP)来进行
(当然,也可以使用TCP和本地[非网络的AF_LOCAL/AF_UNIX]套接字,但是很明显此时并没有使用IP)
5:无连接的套接字:SOCK_DGRAM
1:与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。
这意味着,在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。
然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段,例如,使用面向连接的协议。
使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。
既然有这么多副作用,为什么还使用数据报呢(使用流套接字肯定有一些优势)?由于面向连接的套接字所提供的保证,
因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”。
因此,它们通常能提供更好的性能,并且可能适合一些类型的应用程序。
实现这种连接类型的主要协议是用户数据报协议(更为人熟知的是其缩写 UDP)。为 了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。
你可能知道,UDP套接字的SOCK_DGRAM 名字来自于单词“datagram”(数据报)。因为这些套接字也使用因特网协议来寻找网络中的主机,
所以这个系统也有一个更加普通的名字,即这两种协议(UDP和IP)的组合名字,或UDP/IP
6:Python中的网络编程
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): 默认基于网络的套件字,默认SOCK_STREAM基于tcp
socket_family是AF_UNIX或AF_INET(如前所述),socket_type是SOCK_STREAM或SOCK_DGRAM(也如前所述)。protocol通常省略,默认为0。
创建TCP/IP套接字,可以用下面的方式调用socket.socket()。
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
创建UDP/IP套接字,需要执行以下语句。
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
因为有很多socket模块属性,所以此时使用“from module import*”这种导入方式可以接受,不过这只是其中的一个例外。
如果使用“from socket import *”,那么我们就把socket属性引入到了命名空间中。虽然这看起来有些麻烦,但是通过这种方式将能够大大缩短代码,正如下面所示。
tcpSock = socket(AF_INET, SOCK_STREAM) #类对象的实例化,返回一个套件字对象
一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步的交互。
要创建套接字对象,必须使用socket.socket()函数,它一般的语法如下
服务器套接字方法
s.bind() 将地址(主机名,端口号对)绑定到套接字上
s.listen() 设置并启动TCP监听器
s.accept() 被动接受tcp客户端连接,一直等待直到连接到达(阻塞代码)
客户端套件字方法:
s.connect() 主动发起tcp连接
s.connect_ex() connect扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常
普通的套接字方法
s.recv() 接收tcp消息
s.recv_into() 接收tcp消息到指定的缓冲区
s.send() 发送tcp消息
s.sendall() 完整的发送tcp消息
s.reevfrom() 接收udp消息
s.reevfrom_into() 接收UDP消息到指定的缓冲区
s.sendto() 发送UDP消息
s.getpeername() 连接到套接字(TCP)的远程地址
import socket
sk = socket.socket()
sk.connect(("www.baidu.com", 80))
print(sk.getpeername()) ('14.215.177.39', 80)
s.getsockname() 当前套接字的地址
import socket
sk = socket.socket()
sk.connect(("www.baidu.com", 80))
print(sk.getsockname()) ('192.168.1.61', 63099)
s.getsockopt() 返回给定套接字选项的值
s.scetsockopt() 设置给定套接字选项的值
s.shutdown() 关闭连接
s.close() 关闭套接字
s.detach() 在未关闭文件描述符的情况下关闭套接字,返回文件描述符
s.ioctl() 控制套接字的模式(仅支持Windows)
面向阻塞的套接字方法
s.setblocking() 设置套接字的阻塞或非阻塞模式
settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 获取阻塞套接字操作的超时时间
面向文件的套接字方法
sfileno() 套接字的文件描述符
s.makefile() 创建与套接字关联的文件对象
数据属性
s.family 套接字家族
s.type 套接字类型
s.proto 套接字协议
gethostbyname(‘www.baidu.com’) 根据域名查询ip地址
name = socket.gethostbyname(‘www.baidu.com’)
print(name)
7:创建tcp服务器
import socket sk = socket.socket() # 创建服务器套接字 sk.bind() # 套接字与地址绑定 sk.listen() # 监听连接 # inf_loop: # 服务器无限循环 conn, addr = sk.accept() # 接受客户端连接 # comm_loop: # 通信循环 conn.recv() # 接受 conn.send() # 发送 conn.close() # 关闭客户端套接字 sk.close() # 关闭服务器套接字#(可选)
所有套接字都是通过使用socket.socket()函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,
所以它们必须绑定到一个本地地址。因为TCP是一种面向连接的通信系统,所以在TCP服务器开始操作之前,必须安装一些基础设施。
特别地,TCP服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始它的无限循环。
调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。
默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。
另外,套接字确实也支持非阻塞模式,可以参考文档或操作系统教材,以了解有关为什么以及如何使用非阻塞套接字的更多细节。
一旦服务器接受了一个连接,就会返回(利用accept())一个独立的客户端套接字,
用来与即将到来的消息进行交换。使用新的客户端套接字类似于将客户的电话切换给客服代表。
当一个客户电话最后接进来时,主要的总机接线员会接到这个电话,并使用另一条线路将这个电话转接给合适的人来处理客户的需求
这将能够空出主线(原始服务器套接字),以便接线员可以继续等待新的电话(客户请求),
而此时客户及其连接的客服代表能够进行他们自己的谈话。同样地,当一个传入的请求到达时,
服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,
直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接
在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。最后一行代码是可选的,在这里关闭了服务器套接字。
其实,这种情况永远也不会碰到,因为服务器应该在一个无限循环中运行。在示例中这行代码用来提醒读者,
当为服务器实现一个智能的退出方案时,建议调用close()方法。
例如,当一个处理程序检测到一些外部条件时,服务器就应该关闭。在这些情况下,应该调用一个close()方法。
将一个客户端请求切换到一个新线程或进程来完成客户端处理也是相当普遍的(多线程处理客户端请求)
SocketServer模块是一个以socket为基础而创建的高级套接字通信模块,它支持客户端请求的线程和多进程处理
TCP时间戳服务器简单示例:
HOST 变量是空白的,这是对 bind()方法的标识,表示它可以使用任何可用的地址。我们也选择了一个随机的端口号,并且该端口号似乎没有被使用或被系统保留。
另外,对于该应用程序,将缓冲区大小设置为1KB。可以根据网络性能和程序需要改变这个容量。listen()方法的参数是在连接被转接或拒绝之前,传入连接请求的最大数
tcpSerSock = socket(AF_INET, SOCK_STREAM) :分配了TCP服务器套接字(tcpSerSock),
tcpSerSock.bind(ADDR):紧随其后的是将套接字绑定到服务器地址
tcpSerSock.listen(5):以及开启TCP监听器的调用。
一旦进入服务器的无限循环之中,我们就(被动地)等待客户端的连接。当一个连接请求出现时,我们进入对话循环中,
在该循环中我们等待客户端发送的消息。如果消息是空白的,这意味着客户端已经退出,所以此时我们将跳出对话循环,关闭当前客户端连接,
然后等待另一个客户端连接。如果确实得到了客户端发送的消息,就将其格式化并返回相同的数据,但是会在这些数据中加上当前时间戳的前缀。
最后一行永远不会执行,它只是用来提醒读者,如果写了一个处理程序来考虑一个更加优雅的退出方式,正如前面讨论的,那么应该调用close()方法。
服务端:
from socket import *
from time import ctime
HOST = '192.168.1.61' # 变量空白,这是对 bind()方法的标识,表示它可以使用任何可用的地址,我们也选择了一个随机的端口号
PORT = 13001
BUFSIZ = 1024 # 将缓冲区大小设置为1KB。1024个字节
ADDR = (HOST, PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM) # AF_INET:基于网络的套件字,面向连接的套接字:SOCK_STREAM
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5) # listen()方法的参数是在连接被转接或拒绝之前,传入连接请求的最大数。
while 1:
print("等待客户端连接......")
tcpCliSock, addr = tcpSerSock.accept()
print(f"...建立链接成功,连接进来的用户为:{addr}")
while 1:
try:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
tcpCliSock.send(f"[{ctime()}]: ".encode('utf-8')+data)
# # print(data)
# tcpCliSock.send(f"{ctime()}:{data}")
except ConnectionResetError:
tcpCliSock.close()
break
tcpCliSock.close()
tcpSerSock.close()
客户端:
from socket import * ck = socket() # 创建客户端套接字 ck .connect(('192.168.1.61', 13001)) # 尝试连接服务器 while 1: ck.send(input().encode("utf8")) # 对话(发送/接收) print(ck.recv(1024).decode("utf8"))
ck.close() # 关闭客户端套接字
所有套接字都是利用socket.socket()创建的,一旦客户端拥有了一个套接字,它就可以利用套接字的connect()方法直接创建一个到服务器的连接。
当连接建立之后,它就可以参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接
TCP时间戳客户端(tsTclnt.py)简单示例
HOST和PORT变量指服务器的主机名与端口号。因为在同一台计算机上运行测试(在本例中),
所以 HOST 包含本地主机名(如果你的服务器运行在另一台主机上,那么需要进行相应修改)。
端口号PORT应该与你为服务器设置的完全相同(否则,将无法进行通信)。此外,也将缓冲区大小设置为1KB。
客户端也有一个无限循环,但这并不意味着它会像服务器的循环一样永远运行下去。
客户端循环在以下两种条件下将会跳出:用户没有输入(第14~16行),或者服务器终止且对recv()方法的调用失败(第18~20行)。
否则,在正常情况下,用户输入一些字符串数据,把这些数据发送到服务器进行处理。然后,客户端接收到加了时间戳的字符串,并显示在屏幕上。
from socket import * HOST = '192.168.1.61' # 变量空白,这是对 bind()方法的标识,表示它可以使用任何可用的地址,我们也选择了一个随机的端口号 PORT = 13001 BUFSIZ = 1024 # 将缓冲区大小设置为1KB。1024个字节 ADDR = (HOST, PORT) tcpCliSock = socket(AF_INET, SOCK_STREAM) tcpCliSock.connect(ADDR) while 1: data = input(">>>>") if not data: break tcpCliSock.send(data.encode("gbk")) data = tcpCliSock.recv(BUFSIZ) if not data: break print(data.decode("gbk")) tcpCliSock.close()
TCP客户端的IPv6版本:
from socket import * HOST = '::1' PORT = 13001 BUFSIZ = 1024 ADDR = (HOST, PORT) tcpCliSock = socket(AF_INET6, SOCK_STREAM) tcpCliSock.connect(ADDR) while1: data = input(">>>>") if not data: break tcpCliSock.send(data.encode("gbk")) data = tcpCliSock.recv(BUFSIZ) if not data: break print(data.decode("gbk")) tcpCliSock.close()
将本地主机修改成它的 IPv6 地址“::1”,同时请求套接字的AF_INET6家族。
8:创建UDP服务器
UDP 服务器不需要 TCP 服务器那么多的设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作
UDP 和 TCP 服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”的操作。
这些服务器仅仅接受消息并有可能回复数据。
UDP时间戳服务器(tsUserv.py)
UDP时间戳服务器(tsUserv.py)
from socket import * from time import ctime HOST = '192.168.1.61' PORT = 13001 BUFSIZ = 1024 ADDR = (HOST, PORT) udpSerSock = socket(AF_INET, SOCK_DGRAM) # 无连接的套接字:SOCK_DGRAM udpSerSock.bind(ADDR) while 1: print("等待客户端连接......") data, addr = udpSerSock.recvfrom(BUFSIZ) udpSerSock.sendto(ctime().encode("gbk")+data) print("...客户端的ip地址") udpSerSock.close()
SOCK_DGRAM:UDP套接字类型
因为 UDP 是无连接的,所以这里没有调用“监听传入的连接listen()”。
一旦进入服务器的无限循环之中,我们就会被动地等待消息(数据报)。当一条消息到达时,我们就处理它(通过添加一个时间戳),
并将其发送回客户端,然后等待另一条消息。如前所述,套接字的close()方法在这里仅用于显示。
创建UDP客户端:创建了套接字对象,就进入了对话循环之中,在这里我们与服务器交换消息。最后,当通信结束时,就会关闭套接字。
from socket import * HOST = '192.168.1.61' PORT = 13001 BUFSIZ = 1024 ADDR = (HOST, PORT) udpCliSock = socket(AF_INET, SOCK_DGRAM) while 1: data = input(">>>>") if not data: break udpCliSock.sendto(data.encode("utf8"), ADDR) data, ADDR = udpCliSock.recvfrom(BUFSIZ) if not data: break print(data) udpCliSock.close()
UDP 客户端循环工作方式几乎和 TCP 客户端完全一样。唯一的区别是,事先不需要建立与UDP服务器的连接,
只是简单地发送一条消息并等待服务器的回复。在时间戳字符串返回后,将其显示到屏幕上,然后等待更多的消息。最后,当输入结束时,跳出循环并关闭套接字。
事实上,之所以输出客户端的信息,是因为可以同时接收多个客户端的消息并发送回复消息,
这样的输出有助于指示消息是从哪个客户端发送的。利用TCP服务器,可以知道消息来自哪个客户端,因为每个客户端都建立了一个连接。
9:socket模块属性
数据属性 AF_UNIX、AF_INET、 AF_INET6、AF _NETLINK、AF_ TIPC Python中支持的套接字地址家族 SO_ STREAM,SO_ _DGRAM 套接字类型(TCP-流,UDP-数据报) has_ ipv6 指示是否支持IPv6的布尔标记 异常 error 套接字相关错误 herror 主机和地址相关错误 gaierror 地址相关错误 timeout 超时时间 函数 socket() 以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字对象 socketpair() 以给定的地址家族、套接字类型和协议类型(可选)创建一对套接字对象
import socket
sk1, sk2 = socket.socketpair() 返回一对已经建立tcp连接的套接字
sk1.send(b"askdja")
print(sk2.recv(1024))
create_connection() 常规函数,它接收一个地址(主机名,端口号)对,返回套接字对象
res = socket.create_connection(("14.215.177.38", 80))
print(res)
该函数为连接到一个 TCP 服务,该服务正在侦听 Internet address (用二元组 (host, port) 表示)。
连接后返回套接字对象。这是比 socket.connect() 更高级的函数:如果 host 是非数字主机名,
它将尝试从 AF_INET 和 AF_INET6 解析它,然后依次尝试连接到所有可能的地址,直到连接成功。这使得编写兼容 IPv4 和 IPv6 的客户端变得容易。
fromfd() 以一个打开的文件描述符创建一个套接字对象
import socket
sk = socket.socket()
sk.connect(("www.baidu.com", 80))
cliSock = socket.fromfd(sk.fileno(), family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.close()
print(cliSock)
print(sk)
# fromfd() 内存中又复制一个一样的套件字,除了fd文件描述不同以外其他都一模一样,但是还是属于两个不同的套接字,一个关闭不影响另外一个
ssl() 通过套接字启动一个安全套接字层连接;不执行证书验证
getaddrinfo() 获取一个五元组序列形式的地址信息
import socket
infolist = socket.getaddrinfo('baidu.com', 'www')
for i in infolist:
print(i)
infolist = socket.getaddrinfo('www.baidu.com', '80')
for i in infolist:
print(i)
现在python中用到的关于地址查询的函数几乎都可以用getaddrinfo,如果你要想做一些与地址查询,主机名ip转换的操作,都可以用这个函数
提供了主机名 baidu.com 和 想要访问的端口号 www。 getaddrinfo就会为我们返回可以用来访问的地址。从返回的数据看出有多个地址可以访问www.baidu.com
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 0, '', ('220.181.38.148', 80))
这个地址可以简称为ftpca。f是family,t是type,p是protocol,c是cononnical name,a是address。这个ftpca的 前三位可以用来构造一个socket
2 是 AF_INET,1 是 SOCK_STREAM,6 是 IPPROTOTCP:
protocol:网络数据交换规则
这些都是可以用来构建一个socket的。 比如接下来的代码。
ftpca = infolist[0]
s = socket.socket(*ftpca[0:3]) # 解包构建一个套接字
s.connect(ftpca[4]) # tcp连接服务器ip
getaddrinfo:用该函数获取我们需要bind的信息。比如你的程序要把socket bind到当前的机器上。bind所需要的信息就可以用getaddrinfo获取。
import socket res = socket.getaddrinfo(None, 'ssh', 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) print(res) # [(<AddressFamily.AF_INET6: 23>, <SocketKind.SOCK_STREAM: 1>, 0, '', ('::', 22, 0, 0)),
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 0, '', ('0.0.0.0', 22))]
None 是传入主机名的地方,这里用none就是说当前主机。 ssh 是端口号。 0 默认稍后解释是什么参数。 socket.SOCK_STREAM是tcp。
getaddrinfo这个函数返回的值是ftpca。tp就是说返回的值里包含了scoket type。这里的socket.SOCK_STREAM指定了type是TCP 0 more稍后解释是什么参数。 socket.AI_PASSIVE 要结合前面的None来理解。我们用了none + socket.AI_PASSIVE这两个参数。
这样函数把当前机器所有的地址都返回了。 可是如果你只想返回当前机器的某个ip。比如当前机器的loopback interface的信息,你就可以把none 换成127.0.0.1 把socket.AI_PASSIVE去掉。
getnameinfo() 给定一个套接字地址,返回(主机名,端口号)二元组
print(socket.getnameinfo(("127.0.0.1", socket.AF_INET), 0))
返回主机名称和端口号:('LAPTOP-2637ST24', '2')
getfqdn() 返回完整的域名
gethostname() 返回当前主机名
import socket
hostname = socket.gethostname()
print(hostname) #主机名:LAPTOP-2637ST24
gethostbyname() 将一个主机名映射到它的IP地址
import socket
# getnameinfo这个函数可以通过ip获取主机名
res = socket.getnameinfo(("192.168.1.136", socket.AF_INET), 0)
info = res[0]
# gethostbyname这个函数可以通过主机名获取ip
print(socket.gethostbyname(info))
gethostbyname_ex() gethostbyname()的扩展版本,它返回主机名、别名主机集合和IP地址列表
import socket
res = socket.getnameinfo(("192.168.1.136", socket.AF_INET), 0)
info = res[0]
print(socket.gethostbyname_ex(info))
# 返回 ('LAPTOP-0AH0BQKV', [], ['192.168.1.136', '192.168.70.1', '192.168.47.1', '169.254.188.115'])
gethostbyaddr() 将一个IP地址映射到DNS信息;返回与gethostbyname_ex0相同的3元组
print(socket.gethostbyaddr("192.168.1.136")) # ('LAPTOP-0AH0BQKV', [], ['192.168.1.136'])
getprotobyname() 将一个协议名(如'tcp') 映射到一个数字
print(socket.getprotobyname("tcp")) # 6
getservbyname()/getservbyport() 将一个服务名映射到一个端口号,或者反过来;对于任何一个函数来说,协议名都是可选的
import socket
port = socket.getservbyname('https', 'tcp')
print(port) # 443
ntohl()/ntohs() 将来自网络的整数转换为主机字节顺序
import socket
port = socket.ntohl(100)
print(port) # 1677721600
htonl(/htons() 将来自主机的整数转换为网络字节顺序
inet_aton()/inet_ntoa() 将IP地址八进制字符串转换成32位的包格式,或者反过来(仅用于IPv4地址)
inet_pton()/inet_ntop() 将IP地址字符串转换成打包的二进制格式,或者反过来( 同时适用于IPv4和IPv6地址)
getdefaulttimeout()/setdefaulttimeout() 以秒 (浮点数)为单位返回默认套接字超时时间;以秒(浮点数)为单位设置默认套接字超时时间
10:http请求百度的实例
import socket cliSock = socket.create_connection(("14.215.177.38", 80)) # 和百度建立连接返回一个套接字 cliSock.send(b"GET / HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n") # http请求部分:请求行,请求头,请求空行,请求体, get_header函数一次性获取:请求行,请求头,请求空行三部分,请求头后面一个 \r\n,请求空行是一个\r\n def get_header(r): # 解析服务器响应头函数,http请求头+请求空行以 \r\n\r\n结尾, headers = [] v = b'' try: while 1: b = r.recv(1) if b == b'\r': next_b = r.recv(1) if next_b == b'\n': next_2b = r.recv(2) headers.append(v.decode()) v = b'' if next_2b == b'\r\n': break else: v += next_2b else: v += b elif b == b'': return else: v += b except: return else: rtv = dict() rtv['http_type'] = headers[0] for _i in range(1, len(headers)): try: key = headers[_i].split(':')[0] val = headers[_i].split(':')[1].strip() rtv[key] = val except Exception as e: print(headers) raise Exception(e) return rtv headers = get_header(cliSock) # 获取这次http请求的请求头 length = headers['Content-Length'] # 获取这次http请求的请求体的长度 data = b"" while len(data) < int(length): data = data + cliSock.recv(1024) print(data.decode('utf8'))
11:ssl通信的实现,一个ssl库
# 客户端代码
import socket import ssl class client_ssl: def send_hello(self,): # 生成SSL上下文 context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # 加载信任根证书 context.load_verify_locations('cert/ca.crt') # 与服务端建立socket连接 with socket.create_connection(('127.0.0.1', 9443)) as sock: # 将socket打包成SSL socket,其主要工作是完成密钥协商 # 一定要注意的是这里的server_hostname不是指服务端IP,而是指服务端证书中设置的CN,我这里正好设置成127.0.1而已 with context.wrap_socket(sock, server_hostname='127.0.0.1') as ssock: # 向服务端发送信息 msg = "do i connect with server ?".encode("utf-8") ssock.send(msg) # 接收服务端返回的信息 msg = ssock.recv(1024).decode("utf-8") print(f"receive msg from server : {msg}") ssock.close() if __name__ == "__main__": client = client_ssl() client.send_hello()
# 服务端代码
import socket import ssl class server_ssl: def build_listen(self): # 生成SSL上下文 context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # 加载服务器所用证书和私钥 context.load_cert_chain('cert/server.crt', 'cert/server_rsa_private.pem.unsecure') # 监听端口 with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock: sock.bind(('127.0.0.1', 9443)) sock.listen(5) # 将socket打包成SSL socket,其主要工作是完成密钥协商 with context.wrap_socket(sock, server_side=True) as ssock: while True: # 接收客户端连接 client_socket, addr = ssock.accept() # 接收客户端信息 msg = client_socket.recv(1024).decode("utf-8") print(f"receive msg from client {addr}:{msg}") # 向客户端发送信息 msg = f"yes , you have client_socketect with server.\r\n".encode("utf-8") client_socket.send(msg) client_socket.close() if __name__ == "__main__": server = server_ssl() server.build_listen()
11:SocketServer模块:这个模块中有为你创建的各种各样的类
BaseServer 包含核心服务器功能和mix-in类的钩子:仅用于推导,这样不会创建这个类的实例:
可以用TCPServer或UDPServer创建类的实例 TCPServer/UDPServer 基础的网络同步TCP/UDP服务器 UnixStreamServer/UnixDatagramServer 基于文件的基础同步TCP/UDP服务器 ForkingMixIn/ ThreadingMixIn 核心派出或线程功能:只用作mix-in 类与一个服务器类配合实现一些异步性;不能直接实例化这个类 ForkingTCPServer/ForkingUDPServer ForkingMixIn和TCPServer/UDPServer的组合 ThreadingTCPServer/ThreadingUDPServer ThreadingMixIn和TCPServer/UDPServer的组合 BaseRequestHandler 包含处理服务请求的核心功能:仅仅用于推导,这样无法创建这个类的实例:
可以使用StreamRequestHandler或DatagramRequestHandler创建类的实例 StreamRequestHandler/DatagramRequestHandler 实现TCP/UDP服务器的服务处理器
你会看到类定义只包括一个用来接收客户端消息的事件处理程序。所有其他的功能都来自使用的SocketServer类。
在原始服务器循环中,我们阻塞等待请求,当接收到请求时就对其提供服务,
然后继续等待。在此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数
12:创建SocketServer TCP服务器
服务端: from socketserver import TCPServer as TCP # from socketserver import StreamRequestHandler as SRH # from time import ctime HOST = '192.168.1.61' PORT = 13001 BUFSIZ = 1024 ADDR = (HOST, PORT) # 作为 SocketServer中StreamRequestHandler的一个子类,并重写了它的handle()方法, # 该方法在基类Request中默认情况下没有任何行为 # 当接收到一个来自客户端的消息时,它就会调用handle()方法。 # 而StreamRequestHandler类将输入和输出套接字看作类似文件的对象, # 因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送回客户端 # 因此,在客户端和服务器代码中,需要额外的回车和换行符。 # 实际上,在代码中你不会看到它,因为我们只是重用那些来自客户端的符号。 # 除了这些细微的差别之外,它看起来就像以前的服务器 class MyRequestsHandler(SRH): def handle(self): print("连接来自:", self.client_address) self.wfile.write(f"{ctime()}".encode("utf8")+self.rfile.readline()) tcpsecv = TCP(ADDR, MyRequestsHandler) # 给定的主机信息和请求处理类创建了TCP服务器,然后无限循环等待客户端请求 print("等待接入>>>>>>") tcpsecv.serve_forever()
客户端: from socket import * HOST = '192.168.1.61' PORT = 13001 BUFSIZ = 1024 ADDR = (HOST, PORT) while 1: tcpCliSock = socket(AF_INET, SOCK_STREAM) tcpCliSock.connect(ADDR) data = input(">>>>") if not data: break tcpCliSock.send(f"{data}\r\n".encode("utf8")) data = tcpCliSock.recv(BUFSIZ) if not data: break print(data.decode("utf8")) tcpCliSock.close()
SocketServer 请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,
我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。
这种行为使得TCP服务器更像是一个UDP服务器。然而,通过重写请求处理类中适当的方法就可以改变它。不过,我们将其留作本章末尾的一个练习。
除了客户端现在有点“由内而外”(因为我们必须每次都创建一个连接)这个事实之外,
其他一些小的区别已经在服务器代码的逐行解释中给出:因为这里使用的处理程序类对待套接字通信就像文件一样,
所以必须发送行终止符(回车和换行符)。而服务器只是保留并重用这里发送的终止符。
当得到从服务器返回的消息时,用strip()函数对其进行处理并使用由print声明自动提供的换行符
13:Twisted框架介绍
Twisted是一个完整的事件驱动的网络框架,利用它既能使用也能开发完整的异步网络应用程序和协议
它提供了大量的支持来建立完整的系统,包括网络协议、线程、安全性和身份验证、聊天/ IM、DBM及RDBMS数据库集成、
Web/因特网、电子邮件、命令行参数、GUI集成工具包等
与SocketServer类似,Twisted的大部分功能都存在于它的类中。特别是对于该示例,我们将使用Twisted因特网组件中的reactor和protocol子包中的类。
14:创建Twisted Reactor TCP服务器和 创建Twisted Reactor TCP客户端
相比于处理程序类,我们创建了一个协议类,并以与安装回调相同的方式重写了一些方法。另外,这个例子是异步的
# 服务器
from twisted.internet import protocol, reactor from time import ctime PORT = 13001
#获取protocol类并为时间戳服务器调用TSServProtocol。然后重写了connectionMade()和dataReceived()方法,
#当一个客户端连接到服务器时就会执行connectionMade()方法,
#而当服务器接收到客户端通过网络发送的一些数据时就会调用 dataReceived()方法。
#reactor会作为该方法的一个参数在数据中传输,这样就能在无须自己提取它的情况下访问它。
# 此外,传输实例对象解决了如何与客户端通信的问题。你可以看到我们如何在connectionMade()中使用它来获取主机信息,
# 这些是关于与我们进行连接的客户端的信息,以及如何在dataReceived()中将数据返回给客户端 class TSServProtocol(protocol.Protocol): def connectionMade(self): clnt = self.clnt = self.transport.getPeer().host print(f"连接来自{clnt}") def dataReceived(self, data: bytes): self.transport.write(f"{ctime()},{data}".encode("utf8")) # 在服务器代码的最后部分中,创建了一个协议工厂。它之所以被称为工厂,是因为每次得到一个接入连接时,
# 都能“制造”协议的一个实例。然后在reactor中安装一个 TCP 监听器,以此检查服务请求。当它接收到一个请求时,就会创建一个TSServProtocol实例来处理那个客户端的事务。 factory = protocol.Factory() factory.protocol = TSServProtocol print("等待用户连接") reactor.listenTcp(PORT, factory) reactor.run()
# 客户端
from twisted.internet import protocol, reactor HOST = 'localhost' PORT = 13001
# 重写 connectionMade()和 dataReceived()方法来扩展 Protocol,
# 并且这两者都会以与服务器相同的原因来执行。另外,还添加了自己的方法 sendData(),当需要发送数据时就会调用它
# 因为这次我们是客户端,所以我们是开启与服务器对话的一端。一旦建立了连接,就进行第一步,
# 即发送一条消息。服务器回复之后,我们就将接收到的消息显示在屏幕上,并向服务器发送另一个消息
# 以上行为会在一个循环中继续,直到当提示输入时我们不输入任何内容来关闭连接。
# 此时,并非调用传输对象的write()方法发送另一个消息到服务器,而是执行loseConnection()来关闭套接字。
# 当发生这种情况时,将调用工厂的clientConnectionLost()方法以及停止reactor,结束脚本执行。
# 此外,如果因为某些其他的原因而导致系统调用了clientConnectionFailed(),那么也会停止reactor
class TSClntProtocol(protocol.Protocol): def sendData(self): data = input(">>>>") if data: print(f"输入:{data}") self.transport.write(data) else: self.transport.loseConnection() def connectionMade(self): self.sendData() def dataReceived(self, data: bytes): print(data) self.sendData() class TSClntFactory(protocol.ClientFactory): protocol = TSClntProtocol clientConnectionLost = clientConnetionFailen = lambda self, connector,reason: reactor.stop()
# 脚本的最后部分创建了一个客户端工厂,创建了一个到服务器的连接并运行reactor。
# 注意,这里实例化了客户端工厂,而不是将其传递给reactor,正如我们在服务器上所做的那样。这是因为我们不是服务器,需要等待客户端与我们通信,并且它的工厂为每一次连接都创建一个
# 新的协议对象。因为我们是一个客户端,所以创建单个连接到服务器的协议对象,而服务器的工厂则创建一个来与我们通信
reactor.connetTCP(HOST, PORT, TSClntFactory) reactor.run
15:使用 TCP/IP这样底层的协议创建了新的、有专门用途的协议,以此来实现刚刚介绍的高层服务
16:因特网客户端简介
将因特网理解为用来传输数据的地方,数据在服务提供者和服务使用者之间传输。
在某些情况下称为“生产者-消费者”(虽然这个概念一般用于描述操作系统方面的内容)。
服务器就是生产者,提供服务,而客户端使用服务。对特定的服务,一般只有一个服务器(即进程或主机等),
但有多个消费者(就像之前看的客户端/服务器模型那样)。虽然现在不再使用底层的套接字创建因特网客户端,但模型是完全相同的
17:文件传输因特网协议:最流行的包括文件传输协议(FTP)、UNIX 到 UNIX 复制协议(UUCP)、
用于Web的超文本传输协议(HTTP)。另外,还有(UNIX下的)远程文件复制命令rcp(以及更安全、更灵活的scp和rsync)。
HTTP、FTP、scp/rsync的应用仍然非常广泛。
HTTP主要用于基于Web的文件下载以及访问 Web 服务,
一般客户端无须登录就可以访问服务器上的文件和服务。大部分HTTP文件传输请求都用于获取网页(即将网页文件下载到本地)
而scp和rsync需要用户登录到服务器主机。在传输文件之前必须验证客户端的身份,否则不能上传或下载文件。
FTP与scp/rsync相同,它也可以上传或下载文件,并采用了UNIX的多用户概念,用户需要输入有效的用户名和密码。但FTP也允许匿名登录。
18:文件传输协议: FTP详解
文件传输协议(File Transfer Protocol,FTP):FTP主要用于匿名下载公共文件,也可以用于在两台计算机之间传输文件,
特别是在使用Windows进行工作,而文件存储系统使用UNIX的情况下。早在Web流行之前,FTP就是在因特网上进行文件传输以及下载软件和源代码的主要手段之一
FTP要求输入用户名和密码才能访问远程FTP服务器,但也允许没有账号的用户匿名登录。不过管理员要先设置FTP服务器以允许匿名用户登录。
这时,匿名用户的用户名是“anonymous”,密码一般是用户的电子邮件地址。与向特定的登录用户传输文件不同,
这相当于公开某些目录让大家访问。但与登录用户相比,匿名用户只能使用有限的几个FTP命令。
因特网上的FTP客户端和服务器。客户端与服务器在命令与控制端口通过FTP协议通信,而数据通过数据端口传输
其工作流程如下
1.客户端连接远程主机上的FTP服务器。
2.客户端输入用户名和密码(或“anonymous”和电子邮件地址)
3.客户端进行各种文件传输和信息查询操作。
4.客户端从远程FTP 服务器退出,结束传输。
这只是一般情况下的流程。有时,由于网络两边计算机的崩溃或网络的问题,会导致整个传输在完成之前就中断。
如果客户端超过15分钟(900秒)还没有响应,FTP连接就会超时并中断
在底层,FTP只使用TCP,而不使用UDP。
另外,可以将FTP看作客户端/服务器编程中的特殊情况。
因为这里的客户端和服务器都使用两个套接字来通信:一个是控制和命令端口(21号端口),另一个是数据端口(有时是20号端口),
FTP有两种模式:主动和被动。只有在主动模式下服务器才使用数据端口。在服务器把20号端口设置为数据端口后,
它“主动”连接客户端的数据端口。
而在被动模式下,服务器只是告诉客户端随机的数据端口号,客户端必须主动建立数据连接。
在这种模式下,FTP服务器在建立数据连接时是“被动”的。最后,现在已经有了一种扩展的被动模式来支持第6版本的因特网协议(IPv6)地址——详见RFC 2428
Python 已经支持了包括 FTP 在内的大多数据因特网协议。可以在 http://docs.python.org/lib/internet.html中找到支持各个协议的客户端模块。
19: Python和FTP
流程如下:
1.连接到服务器。
2.登录
3.发出服务请求(希望能得到响应)
4.退出
在使用Python 的FTP支持时,所需要做的只是导入ftplib模块,并实例化一个ftplib.FTP类对象。所有的FTP操作(如登录、传输文件和注销等)都要使用这个对象完成
20:ftplib.FTP类的方法
login(user-'anonymous',passwd= ",acct=") 登录FTP服务器,所有参数都是可选的 pwd() 获得当前工作目录 cwd(path) 把当前工作目录设置为path所示的路径 dir([path.....[,cb]]) 显示path目录里的内容,可选的参数cb是-一个回调函数,会传递给retrlines0方法 nlst(path[.....]) 与dir()类似,但返回一个文件名列表,而不是显示这些文件名 retrlines(cmd[, cb]) 给定FTP命令(如“RETR filename"),用于下载文本文件。可选的回调函数cb用于处理文件的每一-行 retrbinary(cmd,cb[.bs=8192[,ra]]) 与retrlines()类似,只是这个指令处理二进制文件。回调函数cb用于处理每一块(块大小默认为 8KB)下载的数据 storlines(cmd,f) 给定FTP命令(如“STOR filename"),用来上传文本文件。要给定一个文件对象 f storbinary(cmd,f[,bs-8192])) 与storlines()类似,只是这个指令处理二进制文件。要给定-一个文件对象f,上传块大小bs默认为8KB rename(old, new) 把远程文件old重命名为new delete(path) 删除位于path的远程文件 mkd(directory) 创建远程目录 rmd(directory) 删除远程目录 quit() 关闭连接并退出
在一般的FTP事务中,要使用到的指令有login()、cwd()、dir()、pwd()、stor*()、retr*()和quit()
表3-1中没有列出的一些FTP对象方法也很有用。关于FTP对象的更多信息,请参阅http://docs.python.org/library/ftplib#ftp-objects中的Python文档。
21:交互式FTP示例
在 Python 中使用 FTP 非常简单,甚至都不用写脚本,直接在交互式解释器中就能实时地看到操作步骤和输出。
下面这个示例会话是在几年前python.org还支持FTP服务器的时候做的。现在这个示例已经无法工作,只是用来演示与正在运行的FTP服务器进行交互的情形。
from ftplib import FTP f = FTP("ftp.python.org'") f.login('anonymous', 'guido@python.org') f.dir() f.retrlines('RETR motd') f.quit()
22: 客户端FTP程序示例
如果直接在交互环境中使用FTP就无须编写脚本。但下面还是编写一段脚本,用来从Mozilla的网站下载最新的Bugzilla代码。
下面就用来完成这个工作。虽然这里在尝试编写一个应用程序,但读者也可以交互式地运行这段代码。这个程序使用FTP库下载文件,其中也包含一些错误检查
# 下载网站中最新版本的文件
import ftplib import os import socket HOST = "ftp.mozilla.org" DIRN = "pub/mozilla.org/webtools" FILE = "bugzilla-LATEST.tar.gz"def main(): try: f = ftplib.FTP(HOST) except (socket.error, socket.gaierror) as e: print(f"ERROE: cannot reach{HOST}") return# 函数的结束print(f"*** Conneted to host {HOST}") try: f.login() except ftplib.error_perm: print("ERROR: cannot login anoymosly") f.quit() returnprint("*** Logged in as anoymosly") try: f.cwd(DIRN) # 修改工作目录路径except ftplib.error_perm: print(f"ERROE: cannot CD to {DIRN}") f.quit() returnprint(f"*** Change to {DIRN}") try: f.retrbinary(f"RETR{FILE}", open(FILE, "wb").write) # 保存FTP上的文件except ftplib.error_perm: print(f"ERROR:cannot read file{FILE}") os.unlink(FILE) else: print(f"*** Dwonloaded {FILE}' to CWD") f.quit() if__name__ == '__main__': main()
main()函数分为以下几步:
1:创建一个FTP对象,尝试连接到FTP服务器(第12~17行),然后返回。如果发生任何错误就退出。
2:接着尝试用“anonymous”登录,如果不行就结束(第19~25行)。
3:下一步就是转到发布目录(第27~33行),
4:最后下载文件(第35~44行)
在第14行和本书中其他的异常处理程序中,需要保存异常实例e。对于Python 2.5或更老的版本,需要将as改为逗号,
因为这里使用的是从Python 2.6引入的新语法。Python 3只会理解如第14行所示的新语法
向retrbinary()传递了一个回调函数,每接收到一块二进制数据的时候都会调用这个回调函数。
这个函数就是创建文件的本地版本时需要用到的文件对象的write()方法。传输结束时,Python解释器会自动关闭这个文件对象,
因此不会丢失数据。虽然很方便,但最好还是不要这样做,作为一个程序员,要尽量做到在资源不再被使用的时候就立即释放,
而不是依赖其他代码来完成释放操作。这里应该把开放的文件对象保存到一个变量(如变量loc),然后把loc.write传给ftp.retrbinary()
完成传输后,调用loc.close()。如果由于某些原因无法保存文件,则os.unlink移除空的文件来避免弄乱文件系统(第40行)。
在os.unlink(FILE)两侧添加一些错误检查代码,以应对文件不存在的情况。最后,为了避免另外两行(第43~44行)关闭FTP连接并返回,使用了else语句(第35~42行)
23:FTP的其他内容
Python同时支持主动和被动模式。注意,在Python 2.0及以前版本中,被动模式默认是关闭的;在Python 2.1及以后版本中,默认是打开的
24: Usenet与新闻组
Usenet 新闻系统是一个全球存档的“电子公告板”。各种主题的新闻组一应俱全,从诗歌到政治,从自然语言学到计算机语言,
从软件到硬件,从种植到烹饪、招聘/应聘、音乐、魔术、相亲等。新闻组可以面向全球,也可以只面向某个特定区域。
整个系统是一个由大量计算机组成的庞大的全球网络,计算机之间共享 Usenet 上的帖子。如果某个用户发了一个帖子到本地的 Usenet 计算机上,
这个帖子会被传播到其他相连的计算机上,再由这些计算机传到与它们相连的计算机上,直到这个帖子传播到了全世界,每个人都收到这个帖子为止。
帖子在Usenet上的存活时间是有限的,这个时间可以由Usenet系统管理员来指定,也可以为帖子指定一个过期的日期/时间。
每个系统都有一个已“订阅”的新闻组列表,系统只接收感兴趣的新闻组里的帖子,而不是接收服务器上所有新闻组的帖子。
Usenet新闻组的内容由提供者安排,很多服务都是公开的。但也有一些服务只允许特定用户使用,
例如付费用户、特定大学的学生等。Usenet系统管理员可能会进行一些设置来要求用户输入用户名和密码,管理员也可以设置是否只能上传或只能下载
Usenet正在逐渐退出人们的视线,主要被在线论坛替代。但依然值得在这里提及,特别是它的网络协议
老的Usenet使用UUCP作为其网络传输机制,在20世纪80年代中期出现了另一个网络协议TCP/IP,之后大部分网络流量转向使用TCP/IP。下一节将介绍这个新的协议
25:网络新闻传输协议
用户使用网络新闻传输协议(NNTP)在新闻组中下载或发表帖子。
该协议由Brain Kantor(加州大学圣地亚哥分校)和Phil Lapsley(加州大学伯克利分校)创建并记录在RFC 977中,于1986年2月公布。
其后在2000年10月公布的RFC 2980中对该协议进行了更新
作为客户端/服务器架构的另一个例子,NNTP 与 FTP 的操作方式相似,但更简单。
在FTP中,登录、传输数据和控制需要使用不同的端口,而NNTP只使用一个标准端口119来通信。用户向服务器发送一个请求,服务器就做出相应的响应
26:Python和NNTP
与FTP一样,所要做的就是导入这个NNTP的Python模块
1.连接到服务器。
2.登录(根据需要)。
3.发出服务请求。
4.退出。
这与FTP协议极其相似。唯一的区别是根据NNTP服务器配置的不同,登录这一步是可选的
一般来说,登录后需要调用group()方法来选择一个感兴趣的新闻组。该方法返回服务器的回复、文章的数量、
第一篇和最后一篇文章的ID、新闻组的名称。有了这些信息后,就可以做一些其他操作,
如从头到尾浏览文章、下载整个帖子(文章的标题和内容),或者发表一篇文章等。
group(name) 选择一个组的名字,返回一个元组(sp,t,st,lst,group), 分别表示服务器响应信息、文章数量、第 一个和最后一个文章的编号、组名,所有数据都是字符串。(返回的group与传进去的name应该是相同的) xhdr(hdr;artrg[.ofile) 返回文章范围artg ("头-尾”的格式)内文章hdr头的列表,或把数据输出到文件ofile中 body(id [,ofile]) 根据id获取文章正文,id 可以是消息的ID (放在尖括号里),也可以是文章编号( 以字符串形式表示), 返回一个元组(rsp, anum, mid, data),分别表示服务器响应信息、文章编号(以字符串形式表示)、 消息ID (放在尖括号里)、文章所有行的列表,或把数据输出到文件ofile中 head(id) 与body()类似,返回相同的元组,只是返回的行列表中只包括文章标题 article(id) 同样与body(类似,返回相同的元组,只是返回的行列表中同时包括文章标题和正文 stat(id) 让文章的“指针”指向id (即前面的消息ID或文章编号)。返回一个与body0相同的元组(rsp, anum,mid),但不包含文章的数据 next() 用法和stat()类似,把文章指针移到下一篇文章, 返回与stat()相似的元组 last() 用法和stat()类似,把文章指针移到最后一篇文章,返回与stat()相似的元组 post(ufile) 上传ufile文件对象里的内容(使用ufiereadine),并发布到当前新闻组中 quit() 关闭连接并退出
交互式NNTP示例
>>> from nntplib import NNTP >>> n = NNTP('your.nntp.server') >>> rsp, ct, fst, lst, grp = n.group('comp.lang.python') >>> rsp, anum, mid, data = n.article('110457') >>> for eachLine in data: …… print eachLine From:"Alex Martelli" <alex@……> Subject:Re:Rounding Question Date:Wed, 21 Feb 2001 17:05:36 +0100 "Remco Gerlich" <remco@……> wrote: > Jacob Kaplan-Moss <jacob@……> wrote in comp.lang.python: >> So I've got a number between 40 and 130 that I want to round up to >> the nearest 10.That is: >> >> 40 ——> 40, 41 ——> 50, ……, 49 ——> 50, 50 ——> 50, 51 ——> 60 > Rounding like this is the same as adding 5 to the number and then > rounding down.Rounding down is substracting the remainder if you were > to divide by 10, for which we use the % operator in Python. This will work if you use +9 in each case rather than +5 (note that he doesn't really want rounding —— he wants 41 to 'round' to 50, for ex). Alex >>> n.quit() '205 closing connection - goodbye!' >>>
客户端程序NNTP示例:如下
1:程序首先包含一些import语句并定义一些常量,与FTP客户端示例相似
2:第11~40行
在第一部分,尝试连接到NNTP主机服务器,如果失败就退出(第13~4行)。
第15行故意注释掉了,如果需要输入用户名和密码进行身份验证,可以启用这一行并修改第14行。
接着尝试读取指定的新闻组。同样,如果新闻组不存在,或服务器没有保存这个新闻组,或需要身份验证,就退出(第26~40行)
3:第42~55行
这一部分读取并显示一些头消息(第42~51行)。最有用的头消息包括作者、主题、日期。程序会读取这些数据并显示给用户。
每次调用xhdr()方法时,都要给定想要提取消息头的文章的范围。因为这里只想获取一条消息,所以范围就是“X-X”,其中X是最新一条消息的号码。
xhdr()方法返回一个长度为 2 的元组,其中包含了服务器的响应(rsp)和指定范围的消息头的列表。因为只指定了一个消息(最新一条),
所以只取列表的第一个元素(hdr[0])。数据元素是一个长度为2的元组,其中包含文章编号和数据字符串。由于已经知道了文章编号(在请求中给出了),
因此只关心第二个元 素,即数据字符串(hdr[0][1])。
4:最后一部分是下载文章的内容(第53~55行)。先调用body()方法,然后至多显示前20个有意义的行(在该部分开始定义的),最后从服务器注销,完成处理。
5:第57~80行
主要的处理任务由 displayFirst20()函数完成(第 57~80 行)。该函数接收文章的一些内容,并做一些预处理,如把计数器清 0,
创建一个生成器表达式对文章内容的所有行做一些处理,然后“假装”刚碰到并显示了一行空行(第 59~61 行,稍后细说)。
“Genexp”添加自Python 2.4,如果读者使用的是2.0~2.3版本,需要将这两行改为列表推导(实际上,读者不应该使用 2.4 之前的版本)。
由于前导空格可能是Python 代码的一部分,因此在去掉字符串中的空格的时候,只删除字符串尾随的空格(rstrip())。
6:由于不想显示引用的文本和引用文本指示行;因此在第65~71行(也包含第64行)使用了一个大if语句。只有在当前行不是空行时,
才做这个检查(第63行)。检查的时候,会把字符串转成小写,这样就能做到比较的时候不区分大小写(第64行)。
如果一行以“>”或“|”开头,说明这一般是一个引用。不过,将以“>>>”开头的行特殊处理,因为这有可能是交互命令行的提示,
虽然这样可能有问题,会导致显示一条引用了三次的消息(比如一段文本到第4个回复的帖子时就被引用了3次)。
(本章末尾有一个练习会处理这个问题)。另外,以“in article……”开头,以“writes:”或“wrote:”结尾,行末含有冒号的行,
都是引用文本。使用continue语句跳过这些内容。
现在来处理空行。程序应该能智能处理并显示文章中的空行。如果有多个连续的空行,则只显示第一个,
这样用户就不会看到许多空行,导致必须滚动才能看到有用的信息。同时也不能把空行计算到有意义的20行之中。所有这些都在第72~78行中实现。
第72行的if语句表示只有在上一行不为空,或者上一行为空但当前行不为空的时候才显示。也就是说,如果显示了当前行,就说明要么当前行不为空,
要么当前行为空但上一行不为空。这是另一个比较有技巧的地方:如果遇到一个非空行,计数器加1,
并将lastBlank标志设置为False,以表示这一行非空(第74~76行)。否则,如果遇到了空行,则把标志设为True。
现在回到第61行,先将lastBlank标志设为True,因为如果内容的第一行(不是前导数据或引用数据)是空行,则不会显示。需要显示的第一行是实际的数据。
最后,如果遇到了20个非空行就退出,丢弃其余内容(第79~80行)。否则,就应该已经遍历了所有内容,循环正常结束。
其他先省略:查看多线程:217页
27:多线程编程:整个程序实际上包含多个子任务,都需要按照这种顺序方式执行
假如这些子任务相互独立,没有因果关系(也就是说,各个子任务的结果并不影响其他子任务的结果)
这种做法是不是不符合逻辑呢?要是让这些独立的任务同时运行,会怎么样呢?很明显,这种并行处理方式可以显著地提高整个任务的性能。这就是多线程编程。
多线程编程对于具有如下特点的编程任务而言是非常理想的:本质上是异步的;需要多个并发活动;
每个活动的处理顺序可能是不确定的,或者说是随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,
其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
计算密集型的任务可以比较容易地划分成多个子任务,然后按顺序执行或按照多线程方式执行。
而那种使用单线程处理多个外部输入源的任务就不那么简单了。如果不使用多线程,
要实现这种编程任务就需要为串行程序使用一个或多个计时器,并实现一个多路复用方案
一个串行程序需要从每个I/O终端通道来检查用户的输入;然而,有一点非常重要,程序在读取I/O终端通道时不能阻塞,
因为用户输入的到达时间是不确定的,并且阻塞会妨碍其他I/O通道的处理。串行程序必须使用非阻塞I/O或拥有计时器的阻塞I/O(以保证阻塞只是暂时的)。
由于串行程序只有唯一的执行线程,因此它必须兼顾需要执行的多个任务,确保其中的某个任务不会占用过多时间,
并对用户的响应时间进行合理的分配。这种任务类型的串行程序的使用,往往造成非常复杂的控制流,难以理解和维护
28:线程和进程
进程:计算机程序只是存储在磁盘上的可执行二进制(或其他类型)文件。只有把它们加载到内存中并被操作系统调用,才拥有其生命期。
进程(有时称为重量级进程)则是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。
操作系统管理其上所有进程的执行,并为这些进程合理地分配时间。进程也可以通过派生(fork或spawn)新的进程来执行其他任务,
不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息
线程:线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文。
可以将它们认为是在一个主进程或“主线程”中并行运行的一些“迷你进程”
线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其他线程运行时,
它可以被抢占(中断)和临时挂起(也称为睡眠)——这种做法叫做让步(yielding)
一个进程中的各个线程与主线程共享同一片数据空间,因此相比于独立的进程而言,线程间的信息共享和通信更加容易。
线程一般是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作成为可能。当然,在单核CPU系统中,
因为真正的并发是不可能的,所以线程的执行实际上是这样规划的:每个线程运行一小会儿,然后让步给其他线程(再次排队等待更多的CPU时间)。
在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行结果通信
当然,这种共享并不是没有风险的。如果两个或多个线程访问同一片数据,由于数据访问顺序不同,
可能导致结果不一致。这种情况通常称为竞态条件(race condition)。幸运的是,大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
另一个需要注意的问题是,线程无法给予公平的执行时间。这是因为一些函数会在完成前保持阻塞状态,
如果没有专门为多线程情况进行修改,会导致CPU的时间分配向这些贪婪的函数倾斜。
29:全局解释器锁
Python代码的执行是由Python虚拟机(又名解释器主循环)进行控制的。Python在设计时是这样考虑的,
在主循环中同时只能有一个控制线程在执行,就像单核 CPU系统中的多进程一样。内存中可以有许多程序,
但是在任意给定时刻只能有一个程序在运行。同理,尽管 Python 解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行
Python虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。
在多线程环境中,Python虚拟机将按照下面所述的方式执行
1.设置GIL。
2.切换进一个线程去运行。
3.执行下面操作之一。
a.指定数量的字节码指令。
b.线程主动让出控制权(可以调用time.sleep(0)来完成)
4.把线程设置回睡眠状态(切换出线程)。
5.解锁GIL。
6.重复上述步骤。
当调用外部代码(即,任意C/C++扩展的内置函数)时,GIL会保持锁定,
直至函数执行结束(因为在这期间没有Python字节码计数)。编写扩展函数的程序员有能力解锁GIL,
然而,作为Python开发者,你并不需要担心Python代码会在这些情况下被锁住
例如,对于任意面向 I/O 的 Python 例程(调用了内置的操作系统 C 代码的那种),
GIL会在I/O调用前被释放,以允许其他线程在I/O执行的时候运行。而对于那些没有太多 I/O 操作的代码而言,
更倾向于在该线程整个时间片内始终占有处理器(和 GIL)。换句话说就是,I/O 密集型的 Python 程序要比计算密集型的代码能够更好地利用多线程环境
30:退出线程
当一个线程完成函数的执行时,它就会退出。另外,还可以通过调用诸如thread.exit()之类的退出函数,
或者 sys.exit()之类的退出 Python 进程的标准方法,亦或者抛出 SystemExit异常,来使线程退出。不过,你不能直接“终止”一个线程
不建议使用thread 模块。给出这个建议有很多原因,其中最明显的一个原因是在主线程退出之后,所有其他线程都会在没有清理的情况下直接退出。
而另一个模块threading会确保在所有“重要的”子线程退出前,保持整个进程的存活(对于“重要的”这个含义的说明,请阅读下面的核心提示:“避免使用thread模块”)
而主线程应该做一个好的管理者,负责了解每个单独的线程需要执行什么,每个派生的线程需要哪些数据或参数,
这些线程执行完成后会提供什么结果。这样,主线程就可以收集每个线程的结果,然后汇总成一个有意义的最终结果
31:在Python中使用线程
Python虽然支持多线程编程,但是还需要取决于它所运行的操作系统。
如下操作系统是支持多线程的:绝大多数类 UNIX 平台(如 Linux、Solaris、Mac OS X、*BSD 等),
以及Windows平台。Python使用兼容POSIX的线程,也就是众所周知的pthread。
默认情况下,从源码构建的Python(2.0及以上版本)或者Win32二进制安装的Python,
线程支持是已经启用的。要确定你的解释器是否支持线程,只需要从交互式解释器中尝试导入thread模块即可,
32:Python的threading模块
Python提供了多个模块来支持多线程编程,包括thread、threading和Queue模块等。
程序是可以使用thread和threading模块来创建与管理线程。thread模块提供了基本的线程和锁定支持;
而threading模块提供了更高级别、功能更全面的线程管理。
使用Queue模块,用户可以创建一个队列数据结构,用于在多线程之间进行共享。我们将分别来查看这几个模块,并给出几个例子和中等规模的应用。
推荐使用更高级别的threading模块,而不使用thread模块有很多原因。threading模块更加先进,有更好的线程支持,
并且thread模块中的一些属性会和threading模块有冲突。另一个原因是低级别的 thread 模块拥有的同步原语很少(实际上只有一个),而threading模块则有很多
避免使用 thread 模块的另一个原因是它对于进程何时退出没有控制。当主线程结束时,所有其他线程也都强制结束,
不会发出警告或者进行适当的清理。如前所述,至少threading模块能确保重要的子线程在进程退出前结束
33:thread模块
除了派生线程外,thread模块还提供了基本的同步数据结构,称为锁对象(lock object,也叫原语锁、简单锁、互斥锁、互斥和二进制信号量)。
如前所述,这个同步原语和线程管理是密切相关的。
thread模块的核心函数是start_new_thread()。它的参数包括函数(对象)、函数的参数以及可选的关键字参数。将专门派生新的线程来调用这个函数
thread模块的函数 _thread.start_new_thread(function, args, kwargs=None) 派生一个新的线程,使用给定的args和可选的kowargs来执行function _thread.allocate_lock() 分配LockType锁对象 _thread.exit() 给线程退出指令 LockType锁对象的方法 acquire(wait=None) 尝试获取锁对象 locked() 如果获取了锁对象则返回True,否则,返回False release() 释放锁
# 普通主线程使用sleep等待子线程全部结束后再执行的代码
import _thread from time import sleep, ctime def loop0(): print(f"start loop0:{ctime()}") sleep(4) print(f"end loop0:{ctime()}") def loop1(): print(f"start loop1:{ctime()}") sleep(2) print(f"end loop1:{ctime()}") def main(): print(f"starting at:{ctime()}") _thread.start_new_thread(loop0, ()) _thread.start_new_thread(loop1, ()) sleep(6) # 如果主线程没有这里睡6s那么主线程会很快跑完然后loop0和loop1两个线程直接终止 print(f"all DONE at:{ctime()}") if __name__ == '__main__': main()
执行结果:
starting at:Sat May 8 16:59:10 2021
start loop0:Sat May 8 16:59:10 2021
start loop1:Sat May 8 16:59:10 2021
end loop1:Sat May 8 16:59:12 2021
end loop0:Sat May 8 16:59:14 2021
all DONE at:Sat May 8 16:59:16 2021
两个循环是并发执行的(很明显,短的那个先结束),因此总的运行时间只与最慢的那个线程相关,而不是每个线程运行时间之和
start_new_thread()必须包含开始的两个参数,即使要执行的函数不需要参数,也需要传递一个空元组
睡眠4秒和睡眠2秒的代码片段是并发执行的,这样有助于减少整体的运行时间。你甚至可以看到loop1是如何在loop0之前结束的。
这个应用程序中剩下的一个主要区别是增加了一个 sleep(6)调用。为什么必须要这样做呢?这是因为如果我们没有阻止主线程继续执行,
它将会继续执行下一条语句,显示“all done”然后退出,而loop0()和loop1()这两个线程将直接终止
我们没有写让主线程等待子线程全部完成后再继续的代码,即我们所说的线程需要某种形式的同步。
在这个例子中,调用sleep()来作为同步机制。将其值设定为6秒是因为我们知道所有线程(用时4秒和2秒的)会在主线程计时到6秒之前完成
你可能会想到,肯定会有比在主线程中额外延时6秒更好的线程管理方式。由于这个延时,整个程序的运行时间并没有比单线程的版本更快。
像这样使用sleep()来进行线程同步是不可靠的。如果循环有独立且不同的执行时间要怎么办呢?我们可能会过早或过晚退出主线程。这就是引出锁的原因
再一次修改代码,引入锁,并去除单独的循环函数,。通过使用锁,我们可以在所有线程全部完成执行后立即退出。
# 使用锁配合多线程代码
# 1:导入了time模块的几个熟悉属性以及thread模块。我们不再把4秒和 2 秒硬编码到不同的函数中,而是使用了唯一的 loop()函数,并把这些常量放进列表loops中
import _thread from time import sleep, ctime loops = [4, 2] # loop()函数代替了之前例子中的loop0和loop1(),因此,我们必须在loop()函数中做一些修改,以便它能使用锁来完成自己的任务。
# 其中最明显的变化是我们需要知道现在处于哪个循环中,以及需要睡眠多久。
# 最后一个新的内容是锁本身。每个线程将被分配一个已获得的锁。当sleep()的时间到了的时候,释放对应的锁,向主线程表明该线程已完成 def loop(nloop, nsec, lock): print(f"start loop,{nloop},at,{ctime()}") sleep(nsec) print(f"loop,{nloop},done at,{ctime()}") lock.release()
# 大部分工作是在main()中完成的,这里使用了3个独立的for循环。首先创建一个锁的列表,
# 通过使用thread.allocate_lock()函数得到锁对象,然后通过acquire()方法取得(每个锁)。
# 取得锁效果相当于“把锁锁上”。一旦锁被锁上后,就可以把它添加到锁列表locks中。
# 下一个循环用于派生线程,每个线程会调用 loop()函数,并传递循环号、睡眠时间以及用于该线程的锁这几个参数。
# 那么为什么我们不在上锁的循环中启动线程呢?这有两个原因:其一,我们想要同步线程,以便“所有的马同时冲出围栏”;其二,获取锁需要花费一点时间。如果线程执行得太快,有可能出现获取锁之前线程就执行结束的情况
# 在每个线程执行完成时,它会释放自己的锁对象。
# 最后一个循环只是坐在那里等待(暂停主线程),直到所有锁都被释放之后才会继续执行。
# 因为我们按照顺序检查每个锁,所有可能会被排在循环列表前面但是执行较慢的循环所拖累。
# 这种情况下,大部分时间是在等待最前面的循环。当这种线程的锁被释放时,剩下的锁可能早已被释放(也就是说,对应的线程已经执行完毕)。
# 结果就是主线程会飞快地、没有停顿地完成对剩下锁的检查。最后,你应该知道只有当我们直接调用这个脚本时,最后几行语句才会执行main()函数
def main(): print(f"starting at:{ctime()}") locks = [] nloops = range(len(loops)) for i in nloops: lock = _thread.allocate_lock() lock.acquire() locks.append(lock) for i in nloops: _thread.start_new_thread(loop, (i, loops[i], locks[i])) for i in nloops: while locks[i].locked(): pass print(f"all done at:{ctime()}")
# 给每个子线程丢一个锁进去,当子线程运行完了后去把锁解开,改变锁的状态好让主线程根据锁状态判断子线程有没有运行结束,进而实现和join类似的功能 if __name__ == '__main__': main()
34:threading模块
Thread 表示一个执行线程的对象 Lock 锁原语对象(和thread模块中的锁一样) RLock 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁) Condition 条件变量对象,使得一个线程等待另一个线程满足特定的“条件”,比如改变状态或某个数据值 Event 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有线程将被激活 Semaphore 为线程间共享的有限资源提供了一个“计数器”,如果没有可用资源时会被阻塞 BoundedSemaphore 与Semaphore相似,不过它不允许超过初始值 Timer 与Thread相似,不过它要在运行前等待一段时间 Barrier 创建一个 “障碍”,必须达到指定数量的线程后才可以继续
35:守护线程:thread.daemon = True
避免使用thread模块的另一个原因是该模块不支持守护线程这个概念。
当主线程退出时,所有子线程都将终止,不管它们是否仍在工作。如果你不希望发生这种行为,就要引入守护线程的概念了
threading 模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。
如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
如同在第 2 章中看到的那样,服务器线程远行在一个无限循环里,并且在正常情况下不会退出
如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记。
该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端请求而不做任何其他事情。
要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon = True(调用thread.setDaemon(True)的旧方法已经弃用了)。
同样,要检查线程的守护状态,也只需要检查这个值即可(对比过去调用 thread.isDaemon()的方法)。
一个新的子线程会继承父线程的守护标记。整个Python程序(可以解读为:主线程)将在所有非守护线程退出之后才退出,换句话说,就是没有剩下存活的非守护线程时。
python中:主线程需要等待所有的非守护线程全部运行完成才会退出,设置守护线程就表示该线程不重要,主线程不需要等他结束才退出
一个新的子线程会继承父线程的守护标记,默认开闭子线程的时候全部基础主线程的标记,默认都是非守护线程,需要自己设置才能变成守护线程
36:Thread类
threading模块的Thread类是主要的执行对象。它有thread模块中没有的很多函数
Thread对象数据属性 name 线程名 ident 线程的标识符 daemon 布尔标志,表示这个线程是否是守护线程 Thread对象方法 _init_ (group=None, tatget=None, name=None, args=(), kwargs={}, verbose=None, daemon=None) 实例化一个线程对象,需要有一个可调用的target, 以及其参数args或kowargs. 还可以传递name或group参数,不过后者还未实现。此外,verbose 标志也是可接受的。 而daemon 的值将会设定thread.daemon属性/标志 start() 开始执行该线程 run() 定义线程功能的方法(通常在子类中被应用开发者重写) join (timeout=None) 调用这个方法的线程直至启动的线程终止之前一直挂起:除非给出了timeout(秒),否则会一直阻塞 getName() 返回线程名
该方法已弃用,更好的方式是设置(或获取)thread.name属性,或者在实例化过程中传递该属性
setName(name) 设定线程名 isAlivel/is_alive() 布尔标志,表示这个线程是否还存活
驼峰式命名已经弃用,并且从Python 2.6版本起已经开始被取代
isDaemon() 如果是守护线程,则返回True;否则,返回False setDaemon(daemonic) 把线程的守护标志设定为布尔值daemonic (必须在线程start()之前调用)
is/setDaemon()已经弃用,应当设置thread.daemon属性;从Python 3.3版本起,也可以通过可选的 daemon值在实例化过程中设定thread.daemon属性。
# run()函数的使用方法如下:定义一个类,继承了Thread类之后,重构类里面的类方法,里面可以写复杂逻辑,每次实例化后run就是创建一个线程运行run()函数里面的内容
import threading import time class MyThread(threading.Thread): def run(self): print("牛皮牛皮") time.sleep(3) def test(self): pass start_time = time.time() t1 = MyThread() t1.start() t2 = MyThread() t2.start() t1.join() t2.join() end_time = time.time() print(f"多个线程运行时间{end_time-start_time}")
使用Thread类,可以有很多方法来创建线程。我们将介绍其中比较相似的三种方法。选择你觉得最舒服的,或者是最适合你的应用和未来扩展的方法(我们更倾向于最后一种方案)
创建多线程的三种方法: 1:创建Thread的实例,传给它一个函数。 2:创建Thread的实例,传给它一个可调用的类实例 3:派生Thread的子类,并创建子类的实例。 你会发现你将选择第一个或第三个方案。当你需要一个更加符合面向对象的接口时,会选择后者;否则会选择前者。老实说,你会发现第二种方案显得有些尴尬并且稍微难以阅读。
1:创建Thread的实例,传给它一个函数:如下 在第一个例子中,我们只是把Thread类实例化,然后将函数(及其参数)传递进去,和之前例子中采用的方式一样。当线程开始执行时,这个函数也会开始执行 threading模块的Thread类有一个join()方法,可以让主线程等待所有线程执行完毕 import threading from time import sleep, ctime loops = [4, 2] def loop(nloop, nsec): print(f"start loop,{nloop}, at:{ctime()}") sleep(nsec) print(f"loop,{nloop}, done at:{ctime()}") def main(): print(f"starting at:{ctime()}") threads = [] # 存储每个线程的 nloops = range(len(loops)) for i in nloops: t = threading.Thread(target=loop, args=(i, loops[i])) # target指向函数,args给函数传参,therad是一个类 threads.append(t) # 把每个创建的线程实例放到threads这个列表里 for i in nloops: # 启动每个线程 threads[i].start() for i in nloops: # 主线程等待子线程结束 threads[i].join() print(f"all done at:{ctime()}") if __name__ == '__main__': main()
使用thread模块时实现的锁没有了,取而代之的是一组 Thread 对象。当实例化每个 Thread 对象时,把函数(target)和参数(args)传进去,
然后得到返回的Thread实例。实例化Thread(调用Thread())和调用thread.start_new_thread()的最大区别是新线程不会立即开始执行。
这是一个非常有用的同步功能,尤其是当你并不希望线程立即开始执行时。
当所有线程都分配完成之后,通过调用每个线程的 start()方法让它们开始执行,而不是在这之前就会执行。
相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只需要为每个线程调用 join()方法即可。
join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。使用join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因
对于join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们就会一直执行,
直到给定的函数完成后退出。如果主线程还有其他事情要去做,而不是等待这些线程完成(例如其他处理或者等待新的客户端请求),就可以不调用join()。
join()方法只有在你需要等待线程完成的时候才是有用的
2:创建Thread的实例,传给它一个可调用的类实例 # 创建线程时,与传入函数相似的一个方法是传入一个可调用的类的实例,用于线程执行——这种方法更加接近面向对象的多线程编程。
# 这种可调用的类包含一个执行环境,比起一个函数或者从一组函数中选择而言,有更好的灵活性。现在你有了一个类对象,而不仅仅是单个函数或者一个函数列表/元组 import threading from time import sleep, ctime loops = [4, 2] class ThreadFunc(): def __init__(self, func, args, name=""): # func:函数 args:函数参数 name:函数名称 self.name = name self.func = func self.args = args def __call__(self, *args, **kwargs): # 双下call方法,对象ThreadFunc实例化的时候就会自己调用, self.func(*self.args)
# 把ThreadFunc()类的实例化传递到线程类Thread的构造方法,因为ThreadFunc类重构了双下call方法,
# threadFunc类实例化后得到的每个实例加()就会调用 __call__方法,跟传参个函数名称类似,函数名加()也会调用函数
# 这里类实例化后得到的实例加()就会调用__call__方法,调用__call__方法就会调用func()函数,就会调用类初始化传递的loop函数 原理类似
def loop(nloop, nsec): print(f"start loop,{nloop}, at:{ctime()}") sleep(nsec) print(f"loop,{nloop}, done at:{ctime()}") # 当创建新线程时,Thread 类的代码将调用 ThreadFunc 对象,此时会调用__call__()这个特殊方法。
# 由于我们已经有了要用到的参数,这里就不需要再将其传递给 Thread()的构造函数了,直接调用即可 def main(): print(f"starting at:{ctime()}") threads = [] # 存储每个线程的 nloops = range(len(loops)) for i in nloops: t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__)) threads.append(t) # 把每个创建的线程实例放到threads这个列表里 for i in nloops: # 启动每个线程 threads[i].start() for i in nloops: # 主线程等待子线程结束 threads[i].join() print(f"all done at:{ctime()}") if __name__ == '__main__': main()
双下call函数的使用: class CLanguage: # 定义__call__方法 def __call__(self,name,add): print("调用__call__()方法",name,add) clangs = CLanguage() clangs("C语言中文网","http://c.biancheng.net")
3:派生Thread的子类,并创建子类的实例 # 最后要介绍的这个例子要调用Thread()的子类,和上一个创建可调用类的例子有些相似。当创建线程时使用子类要相对更容易阅读 # 对Thread子类化,而不是直接对其实例化。这将使我们在定制线程对象时拥有更多的灵活性,也能够简化线程创建的调用过程。 import threading from time import sleep, ctime loops = [4, 2] class MyThread(threading.Thread): def __init__(self, func, args, name=""): threading.Thread.__init__(self) self.name = name self.func = func self.args = args def run(self): # 自定义run方法,当MyThread类指向某个函数,也就是某个函数传参进来创建了MyThread类的实例,实例.start的时候会执行run函数里面的内容 self.func(*self.args) def loop(nloop, nsec): print(f"start loop,{nloop}, at:{ctime()}") sleep(nsec) print(f"loop,{nloop}, done at:{ctime()}") def main(): print(f"starting at:{ctime()}") threads = [] # 存储每个线程的 nloops = range(len(loops)) for i in nloops: t = MyThread(loop, (i, loops[i]), loop.__name__) threads.append(t) # 把每个创建的线程实例放到threads这个列表里 for i in nloops: # 启动每个线程 threads[i].start() for i in nloops: # 主线程等待子线程结束 threads[i].join() print(f"all done at:{ctime()}") if __name__ == '__main__': main()
# 1:MyThread子类的构造函数必须先调用其基类的构造函数;threading.Thread.__init__(self)
# 2:之前的特殊方法__call__() 在这个子类中必须要写为run()
# 对 MyThread 类进行修改,增加一些调试信息的输出,并将其存储为一个名为myThread的独立模块
# 以便在接下来的例子中导入这个类。除了简单地调用函数外,还将把结果保存在实例属性self.res中,并创建一个新的方法getResult()来获取这个值
4:为了让mtsleepE.py中实现的Thread的子类更加通用,将这个子类移到一个专门的模块中,并添加了可调用的getResult()方法来取得返回值
import threading from time import ctime, sleep, time class MyThread(threading.Thread): def __init__(self, func, args, name=""): threading.Thread.__init__(self) self.name = name self.func = func self.args = args def getResult(self): return self.res def run(self): print(f"starting,{self.name},at:{ctime()}") self.res = self.func(*self.args) # 调用函数得到的结果存储下来 print(f"{self.name},finished at:{ctime()}") def loop(index, i): print(f"loop{index} starting at:{ctime()}") sleep(i) print(f"loop{index} finished at:{ctime()}") start_time = time() t1 = MyThread(loop, (1, 4), loop.__name__) t2 = MyThread(loop, (2, 2), loop.__name__) t1.start() t2.start() t1.join() t2.join() end_time = time() print(end_time - start_time)
5:创建子类MyThread类,继承Thread类,run里面写逻辑,不指向其他函数 import threading import time class MyThread(threading.Thread): def run(self): print("牛皮牛皮") time.sleep(3) def test(self): pass start_time = time.time() t1 = MyThread() t1.start() t2 = MyThread() t2.start() t1.join() t2.join() end_time = time.time() print(f"多个线程运行时间{end_time-start_time}")
37:threading模块的其他函数
threading.active_count() 当前活动的Thread对象个数,返回数字类型,返回当前进程有多少线程在运行 current Thread()/current_thread() 返回当前的 Thread对象,返回当前运行的是哪个线程:<_MainThread(MainThread, started 14912)> threading.enumerate() 返回当前活动的 Thread对象列表,返回[<_MainThread(MainThread, started 5704)>, <Thread(Thread-1, started 14476)>,
<Thread(Thread-2, started 13396)>] threading.main_thread() 返回主线程 输出<_MainThread(MainThread, started 6656)> threading.BoundedSemaphore(5) 设置只能允许5个线程同时进行 threading.get_ident() 返回线程的标识符 输出6656 threading.settrace(func) 为所有线程设置一个trace函数,为从线程模块启动的所有线程设置跟踪函数。在每个线程之前,func将传递给sys.StReTrace()。 threading.setprofile(func) 为所有线程设置一个 profile函数 threading.stack_size(65536) 返回新创建线程的栈大小:或为后续创建的线程设定栈的大小为size,避免线程多导致内存爆炸
38: 单线程和多线程执行对比
import threading from time import ctime, sleep, time class MyThread(threading.Thread): def __init__(self, func, args, name=""): threading.Thread.__init__(self) self.name = name self.func = func self.args = args def getResult(self): return self.res def run(self): print(f"starting,{self.name},at:{ctime()}") self.res = self.func(*self.args) # 调用函数得到的结果存储下来 print(f"{self.name},finished at:{ctime()}") def fib(x): # 斐波拉契数 sleep(0.1) if x < 2: return 1 return (fib(x-2) + fib(x-1)) def fac(x): # 阶乘函数 sleep(0.1) if x < 2: return 1 return (x * fac(x-1)) def sum(x): # 累加函数 sleep(0.1) if x < 2:return 1 return (x + sum(x - 1)) funcs = [fib, fac, sum] n = 12 def main(): nfuncs = range(len(funcs)) print(f"xxx SINGLE THREAD at:{ctime()}xxx") for i in nfuncs: print(f"starting:{funcs[i].__name__}:at:{ctime()}") print(funcs[i](n)) # 执行递归函数 print(f"{funcs[i].__name__}:finished at:{ctime()}") print(f"\nxxx MULTIPLE THREAD at:{ctime()}xxx") print(f"starting time at {ctime()}") threads = [] # 存储每个线程的 for i in nfuncs: t = MyThread(funcs[i], (n,), funcs[i].__name__) threads.append(t) for i in nfuncs: threads[i].start() for i in nfuncs: threads[i].join() print(threads[i].getResult()) print(f"aLL DONE at{ctime()}") if __name__ == '__main__': main()
# 以单线程模式运行时,只是简单地依次调用每个函数,并在函数执行结束后立即显示相应的结果。
# 而以多线程模式运行时,并不会立即显示结果。因为我们希望让 MyThread 类越通用越好(有输出和没有输出的调用都能够执行),
我们要一直等到所有线程都执行结束,然后调用getResult()方法来最终显示每个函数的返回值
# 因为这些函数执行起来都非常快(斐波那契函数除外),所以你会发现在每个函数中都加入了sleep()调用,
# 用于减慢执行速度,以便让我们看到多线程是如何改善性能的。在实际工作中,如果确实有不同的执行时间,你肯定不会在其中调用sleep()函数。
39:多线程实践
由于Python虚拟机是单线程(GIL)的原因,只有线程在执行I/O密集型的应用时才能更好地发挥Python的并发性(对比计算密集型应用,它只需要做轮询),
如下:一个I/O密集型的例子
atexit.register() 函数来告知脚本何时结束(你将在后面看到原因)。
atexit.register() 是什么呢?这个函数(这里使用了装饰器的方式)会在Python解释器中注册一个退出函数,
也就是说,它会在脚本退出之前请求调用这个特殊函数。(如果不使用装饰器的方式,也可以直接使用register(_atexit()))
正则表达式的re.compile()函数,用于匹配Amazon商品页中图书排名的模式。
threading.Thread模块,
为显示时间戳字符串导入了time.ctime(),
为访问每个链接导入了urllib2.urlopen()
_func 单下划线+函数名:表示特殊函数,,只能被本模块的代码使用,不能被其他使用本文件作为库或者工具模块的应用导入。
_main() 函数同样是一个特殊函数,只有这个模块从命令行直接运行时才会执行该函数(并且不能被其他模块导入)
40:锁示例
锁有两种状态:锁定和未锁定。而且它也只支持两个函数:获得锁和释放锁
当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程将被阻塞,
直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。
不过请记住,那些被阻塞的线程是没有顺序的(即不是先到先执行),胜出线程的选择是不确定的,而且还会根据Python实现的不同而有所区别。
两个线程修改同一个变量(剩余线程名集合)时,
I/O 和访问相同的数据结构都属于临界区,因此需要用锁来防止多个线程同时进入临界区。
为了加锁,需要添加一行代码来引入 Lock(或 RLock),然后创建一个锁对象
# 锁和一些其他线程工具的使用示例 from atexit import register from random import randrange from threading import Thread, current_thread, Lock from time import sleep, ctime # 集合的子类。它包括一个对__str__()的实现,可以将默认输出改变为将其所有元素按照逗号分隔的字符串 class CleanOutputSet(set): def __str__(self): return ", ".join(x for x in self) # 锁;上面提到的修改后的集合类的实例;随机数量的线程(3~6个线程),每个线程暂停或睡眠2~4秒 lock = Lock() # 创建锁对象 loops = (randrange(2, 5) for x in range(randrange(3, 7))) # 返回一个迭代器,生成随机返回一个列表 # loops = [randrange(2, 5) for x in range(randrange(3, 7))] remaining = CleanOutputSet() # loop函数首先保存当前执行它的线程名, # 然后获取锁,以便使添加该线程名到remaining集合以及指明启动线程的输出操作是原子的(没有其他线程可以进入临界区) # 释放锁之后,这个线程按照预先指定的随机秒数执行睡眠操作,然后重新获得锁,进行最终输出,最后释放锁 def loop(nsec): myname = current_thread().name lock.acquire() # 上锁 remaining.add(myname) # 集合对象里面添加元素 print(f"[{ctime()} Stared {myname}]") lock.release() # 解锁 sleep(nsec) lock.acquire() # 上锁 remaining.remove(myname) # 集合对象里面删除元素 print(f"[{ctime()}] Completed {myname} ({nsec} secs)") print(" (remaining: %s)" % (remaining or 'NONE')) lock.release() # 解锁 # 只有不是为了在其他地方使用而导入的情况下,_main()函数才会执行。 # 它的任务是派生和执行每个线程。 # 使用 atexit.register() 来注册_atexit()函数,以便让解释器在脚本退出前执行该函数。 # 作为维护你自己的当前运行线程集合的一种替代方案,可以考虑使用 threading.enumerate(), # 该方法会返回仍在运行的线程列表(包括守护线程,但不包括没有启动的线程)。 # 在本例中并没有使用这个方案,因为它会显示两个额外的线程,所以我们需要删除这两个线程以保持输出的简洁。 # 这两个线程是当前线程(因为它还没结束),以及主线程(没有必要去显示) # 如果只需要对当前运行的线程进行计数,那么可以使用threading.activeCount() # (2.6版本开始重命名为active_count())来代替。 def _main(): for pause in loops: Thread(target=loop, args=(pause,)).start() @register def _atexit(): print(f"aLL DONE at:{ctime()}")
if __name__ == '__main__': _main()
41:使用上下文管理 with()方法
不再调用锁的acquire()和release()方法,从而更进一步简化代码。
使用with语句,此时每个对象的上下文管理器负责在进入该套件之前调用acquire()并在完成执行之后调用release()
threading模块的对象Lock、RLock、Condition、Semaphore和BoundedSemaphore都包含上下文管理器,
也就是说,它们都可以使用with语句。当使用with时,可以进一步简化loop()循环
from __future__ import with_statement from atexit import register from random import randrange from threading import Thread, current_thread, Lock, active_count from time import sleep, ctime class CleanOutputSet(set): def __str__(self): return ", ".join(x for x in self) lock = Lock() loops = (randrange(2, 5) for x in range(randrange(3, 7))) remaining = CleanOutputSet() def loop(nsec): myname = current_thread().name with lock: remaining.add(myname) print(f"[{ctime()} Stared {myname}]") sleep(nsec) with lock: remaining.remove(myname) print(f"[{ctime()}] Completed {myname} ({nsec} secs)") print(" (remaining: %s)" % (remaining or 'NONE')) def _main(): for pause in loops: Thread(target=loop, args=(pause,)).start() @register def _atexit(): print(f"aLL DONE at:{ctime()}") if __name__ == '__main__': print(f"当前存活得线程数目{active_count()}") _main() print(f"当前存活得线程数目{active_count()}")
42:信号量示例
锁得本质:a线程上锁了,但是没有释放,b线程同步运行的时候,如果b线程也要上锁,同一把锁去上锁,b线程就会阻塞,直到a线程释放锁了b线程才会运行
锁非常易于理解和实现,也很容易决定何时需要它们。然而,如果情况更加复杂,
你可能需要一个更强大的同步原语来代替锁。对于拥有有限资源的应用来说,使用信号量可能是个不错的决定。
信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。你可以认为信号量代表它们的资源可用或不可用。
消耗资源使计数器递减的操作习惯上称为P(() 来源于荷兰单词probeer/proberen),也称为wait、try、acquire、pend或procure。
相对地,当一个线程对一个资源完成操作时,该资源需要返回资源池中。这个操作一般称为V()(来源于荷兰单词verhogen/verhoog),
也称为signal、increment、release、post、vacate。
Python简化了所有的命名,使用和锁的函数/方法一样的名字:acquire和release。信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例
模拟一个简化的糖果机。这个特制的机器只有5个可用的槽来保持库存(糖果)。如果所有的槽都满了,
糖果就不能再加到这个机器中了;相似地,如果每个槽都空了,想要购买的消费者就无法买到糖果了。我们可以使用信号量来跟踪这些有限的资源(糖果槽)
# 该脚本使用了锁和信号量来模拟一个糖果机:糖果机最多五个槽来保存库存,生产者生成糖,消费者买糖的简单模拟 from atexit import register from random import randrange # threading模块包括两种信号量类:Semaphore和BoundedSemaphore。 # BoundedSemaphore:也是生成一个锁对象,这个锁可以设置上锁的次数,也就是对这个资源可以指定使用次数,指定可以上锁的次数 # 如你所知,信号量实际上就是计数器,它们从固定数量的有限资源起始 # 当分配一个单位的资源时,计数器值减 1,而当一个单位的资源返回资源池时,计数器值加1。 # BoundedSemaphore的一个额外功能是这个计数器的值永远不会超过它的初始值, # 换句话说,它可以防范其中信号量释放次数多于获得次数的异常用例 from threading import BoundedSemaphore, Lock, Thread from time import sleep, ctime lock = Lock() # 创建锁对象 MAX = 5 # 库存商品最大值的常量, candytray = BoundedSemaphore(MAX) # 糖果托盘,最多五个托盘 # 当虚构的糖果机所有者向库存中添加糖果时,会执行 refill()函数。 # 这段代码是一个临界区,这就是为什么获取锁是执行所有行的仅有方法。 # 代码会输出用户的行动,并在某人添加的糖果超过最大库存时给予警告 def refill(): lock.acquire() print("Refilling candy") try: candytray.release() # 添加糖果,releas释放一个资源,资源+1 except: print("full ,skipping") else: print("ok") lock.release() # buy()是和refill()相反的函数,它允许消费者获取一个单位的库存。 # 检测是否所有资源都已经消费完。计数器的值不能小于 0, # 因此这个调用一般会在计数器再次增加之前被阻塞。 # 通过传入非阻塞的标志False,让调用不再阻塞,而在应当阻塞的时候返回一个False,指明没有更多的资源了 def buy(): lock.acquire() print("Buying candy.....") if candytray.acquire(False): # false参数让candytray锁对象申请锁就算计数器归0了也不会阻塞,直接返回false print("ok") # 如果有资源返回ok else: print("empty, skipping") #如果没资源了返回:empty lock.release() # producer()和consumer()函数都只包含一个循环,进行对应的refill()和buy()调用,并在调用间暂停 def producer(loops): for i in range(loops): refill() sleep(randrange(3)) def consumer(loops): for i in range(loops): buy() sleep(randrange(3)) # _main的调用,退出函数的注册,以及最后的_main()函数提供表示糖果库存生产者和消费者的新创建线程对。 # 创建消费者/买家的线程时进行了额外的数学操作,用于随机给出正偏差, # 使得消费者真正消费的糖果数可能会比供应商/生产者放入机器的更多(否则,代码将永远不会进入消费者尝试从空机器购买糖果的情况 def _main(): print(f"starting at:{ctime()}") nloops = randrange(2, 6) print(f"THE CANDY MACHINE (full with {MAX} bars") Thread(target=consumer, args=(randrange(nloops, nloops+MAX+2),)).start() # 开启消费资源的线程 Thread(target=producer, args=(nloops,)).start() # 开启创建资源的线程 @register def _atexit(): print(f"all DONE at{ctime()}") if __name__ == '__main__': _main()
threading 模块的同步原语并不是类名,即便它们使用了驼峰式拼写方法,看起来像是类名。
实际上,它们是仅有一行的函数,用来实例化你认为的那个类的对象。这里有两个问题需要考虑:其一,你不能对它们子类化(因为它们是函数);其二,变量名在2.x和3.x版本间发生了改变
计数器的值只是类的一个属性,所以可以直接访问它,这个变量名从Python 2版本的self.__value,即self._Semaphore__value,变成了Python 3版本的self._value
对于开发者而言,最简洁的 API是继承 threading._BoundedSemaphore类,并实现一个__len__()方法,
不过要注意,如果你计划对2.x和3.x版本都支持,还是需要使用刚才讨论过的那个正确的计数器值
43:生产者-消费者问题和Queue/queue模块 队列
生产者-消费者模型这个场景:商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的
使用queue模块来提供线程间通信的机制,从而让线程之间可以互相分享数据。具体而言,就是创建一个队列,让生产者(线程)在其中放入新的商品,而消费者(线程)消费这些商品
Queue/queue模块常用属性 queue.Queue(maxsize=1000) 创建一个先入先出队列。如果给定最大值,则在队列没有空间时阻塞,没有指定最大值,为无限队列 queue.LifoQueue(maxsize=1000) 创建一个后入先出队列。如果给定最大值,则在队列没有空间时阻塞;没有指定最大值,为无限队列 queue.PriorityQueue(maxsize=1000) 创建一个优先级队列。如果给定最大值,则在队列没有空间时阻塞,否则(没有指定最大值),为无限队列 Queue/queue异常 Empty 当对空队列调用get()方法时抛出异常 Full 当对已满的队列调用put()方法时抛出异常 Queue/queue对象方法 q.qsize() 返回队列大小(由于返回时队列大小可能被其他线程修改,所以该值为近似值) q.empty() 如果队列为空,则返回 True:否则,返回False q.full() 如果队列已满,则返回True:否则,返回False put(self, item, block=True, timeout=None): 将元素放入队列。如果 block为True(默认True代表阻塞)且 timeout为None,往满了的队列里面丢东西一直阻塞,直到能插入元素 如果block为False,这时候为非阻塞,往满了的队列里面丢东西阻塞直接报错 如果timeout不等于None,等于负数,直接报错 如果timeout不等于None,等于一个正数,那么就阻塞这个正数的时间后报错 q.put_nowait(1) 和put(item, False)相同,只有在空闲插槽立即可用时才将项目排队,否则引发完全异常。 get(self, block=True, timeout=None): 从队列中取得元素。如果可选参数“block”为true,“timeout”为None(默认值)从空了的队列里取值,就一直阻塞,直到能取值元素 如果timeout超时为一个非负数,它最多阻塞“超时”秒,如果在该时间内没有可用项,则返回空异常 block'为false,从空了的队列里取值就马上抛出异常 q.get_nowait() 和 get(False)相同,从空了的队列里取值就马上抛出异常 q.task_done() 用于表示队列中的某个元素已执行完成,使用在get方法从队列里取值后面,每次取值后调用task_done()函数, 队列里未完成的任务-1,直到unfinished <= 0未完成任务等于0的时候self.all_tasks_done.notify_all()抛出一个信息让join不再阻塞 q.join() 在队列中所有元素执行完毕并调用上面的task_done()信号之前,保持阻塞,必须要q.task_done()这个方法里面未完成的值小于等于0后返回信息才不会阻塞
# 优先级队列PriorityQueue使用实例
import queue q = queue.PriorityQueue() q.put((2, 'code')) q.put((1, 'eat')) q.put((3, 'sleep')) q.put((-1, 'liergou')) print(q.full()) whilenot q.empty(): next_item = q.get() print(next_item)
输出:
(-1, 'liergou') (1, 'eat') (2, 'code') (3, 'sleep')
优先级队列传参是一个元组:(优先级数字, 元素),数字越小优先级越高代表越先被取出来
# task_down和join的配合使用
import time import threading from queue import Queue def set_value(q): # 写线程 for x in range(5): q.put(x) time.sleep(1) def get_value(q): # 读的线程 while True: time.sleep(2) if q.empty(): # 如果队列为空停止,不为空往下走继续取值 break # print(q.get()) item = q.get() print(item) res = q.task_done() # 注释状况下程序不会停止,因为join阻塞这一直在等他!! # print(res) return def main(): q = Queue(4) # 创建一个队列,最大值是4 t1 = threading.Thread(target=set_value, args=(q,)) t2 = threading.Thread(target=get_value, args=(q,)) t1.start() t2.start() q.join() # 阻塞在这里,直到队列中的所有项目都已获取和处理 print("谁能阻塞我?") if __name__ == '__main__': main()
# set_value往队列里写,get_value往队列里读,main函数主线程一开始start()开辟子线程之后主线程就join阻塞了
# 队列里写五次,然后读五次,
# join()的作用:task_done()不发未完成任务数为0之前,阻塞代码
# join没有收到队列为空的响应!所以我这里如果在每次get()后面都进行一次task_done() (这里相当于一次是否还阻塞的判断,判断当前队列里未完成的任务是否为0)
# 成功取出所有值后task_done() 就返回给join()未完成任务为0,不阻塞了
# 生产者-消费者问题实例 # 该生产者-消费者问题的实现使用了Queue对象,以及随机生产(消费)的商品的数量。 # 生产者和消费者独立且并发地执行线程
# random.randint 随机数使生产和消费的数量有所不同 from random import randint from time import time, sleep, ctime from queue import Queue import threading class MyThread(threading.Thread): def__init__(self, func, args, name=""): # 这里调用threading.Thread类的构造方法开启多线程,参数可以全部为空,反正通过MyThread调用多线程的时候辉执行run函数,就会调用func函数了 threading.Thread.__init__(self) self.func = func self.args = args self.name = name def run(self): print(f"starting,{self.name},at:{ctime()}") self.res = self.func(*self.args) # 调用函数得到的结果存储下来print(f"{self.name},finished at:{ctime()}") # write_q()和read_q()函数分别用于将一个对象(例如,我们这里使用的字符串’xxx’)放入队列中和消费队列中的一个对象。注意,我们每次只会生产或读取一个对象 def write_q(queue): print("生产数据写到队列q里面") queue.put("xxx", 1) print(f"queue队列里现在的元素大小{queue.qsize()}") def read_q(queue): val = queue.get(1) print(f"从队列里取出元素一次后队列的长度{queue.qsize()}") # writer()将作为单个线程运行,其目的只有一个:向队列中放入一个对象,等待片刻,
# 然后重复上述步骤,直至达到每次脚本执行时随机生成的次数为止。reader()与之类似,只不过变成了消耗对象
# writer睡眠的随机秒数通常比reader的要短。这是为了阻碍reader从空队列中获取对象。通过给writer一个更短的等候时间,使得轮到reader时,已存在可消费对象的可能性更大 def writer(queue, loops): # 生产 for i in range(loops): write_q(queue) sleep(randint(1, 3)) def reader(queue, loops): # 消费 for i in range(loops): read_q(queue) sleep(randint(2, 5)) # 派生和执行的线程总数 2个线程 funcs = [writer, reader] # 函数名称存储在列表里 nfuncs = range(len(funcs)) # range(0, 2) # 创建合适的线程并让它们执行,当两个线程都执行完毕后结束
# 对于一个要执行多个任务的程序,可以让每个任务使用单独的线程。相比于使用单线程程序完成所有任务,这种程序设计方式更加整洁。def main(): nloops = randint(2, 5) q = Queue(32) threads = [] for i in nfuncs: t = MyThread(funcs[i], (q, nloops), funcs[i].__name__) threads.append(t) for i in nfuncs: threads[i].start() for i in nfuncs: threads[i].join() print("all DONE") if__name__ == '__main__': main()
# 生产者和消费者并不需要轮流执行。而是随机抢占执行(感谢随机数!)严格来说,现实生活通常都是随机和不确定的
# 单线程进程是如何限制应用的性能的。尤其是对于那些任务执行顺序存在着独立性、不确定性以及非因果性的程序而言,
# 把多个任务分配到不同线程执行对性能的改善会非常大。由于线程的开销以及Python解释器是单线程应用这个事实,并不是所有应用都可以从多线程中获益,
# 不过现在你已经了解到了Python多线程的功能,你可以在适当的时候使用该工具来发挥它的优势
44:线程的替代方案
多线程是一个好东西。不过由于Python的GIL的限制,多线程更适合于I/O密集型应用(I/O释放了GIL,可以允许更多的并发)
而不是计算密集型应用。对于后一种情况而言,为了实现更好的并行性,你需要使用多进程,以便让CPU的其他内核来执行
subprocess模块 这是派生进程的主要替代方案,可以单纯地执行任务,或者通过标准文件(stdin、stdout、stderr)进行进程间通信 multiprocessing模块 允许为多核或多CPU 派生进程,其接口与threading模块非常相似。该模块同样也包括在共享任务的进程间传输数据的多种 concurrent.futures模块 这是一个新的高级库,它只在“任务”级别进行操作,也就是说,你不再需要过分关注同步和线程/进程的管理了。 你只需要指定一个给定了“worker”数量的线程/进程池,提交任务,然后整理结果。 with ThreadPoolExecutor(3) as executor: for isbn in ISBNs: executor.submit(_showRanking, isbn) # submit用来开启线程执行 传递给concurrent.futures.ThreadPoolExecutor的参数是线程池的大小, 在这个应用里就是指要查阅排名的3本书。当然,这是个I/O密集型应用,因此多线程更有用 而对于计算密集型应用而言,可以使用concurrent.futures.ProcessPoolExecutor来代替 当我们得到执行器(无论线程还是进程)之后,它负责调度任务和整理结果,就可以调用它的submit()方法, 来执行之前需要派生线程才能运行的那些操作了 with ThreadPoolExecutor(3) as executor: for isbn in ISBNs: executor.map(_showRanking, isbn) # map也能开启线程的执行
45:GUI之:Tcl、Tk和Tkinter
Tkinter是Python的默认GUI库。它基于Tk工具包,该工具包最初是为工具命令语言(Tool Command Language,Tcl)设计的。
Tk普及后,被移植到很多其他的脚本语言中,包括Perl(Perl/Tk)、Ruby(Ruby/Tk)和Python(Tkinter)。
结合Tk的GUI开发的可移植性与灵活性,以及与系统语言功能集成的脚本语言的简洁性,可以让你快速开发和实现很多与商业软件品质相当的GUI应用
一旦设计好了应用程序及其外观,就可以使用称为控件(widget)的基础构建块来拼凑出你想要的东西,最后再添加功能使其真实可用
GUI程序启动和运行起来需要以下5个主要步骤。
1.导入Tkinter模块(或from Tkinter import *)。
2.创建一个顶层窗口对象,用于容纳整个GUI应用。
3.在顶层窗口对象之上(或者“其中”)构建所有的GUI组件(及其功能)。
4.通过底层的应用代码将这些GUI组件连接起来
5.进入主事件循环。
创建一个GUI应用就像艺术家作画一样。传统上,艺术家使用单一的画布开展创作。其工作方式如下:首先会从一块干净的石板开始,
这相当于用来构建其余组件的顶层窗口对象。可以将其想象为房屋的地基或艺术家的画架。换句话说,必须在浇灌好混凝土或搭建起画架之后,
才能把真实的结构或画布拼装在上面。在Tkinter中,这个基础称为顶层窗口对象
窗口和控件
在GUI编程中,顶层的根窗口对象包含组成GUI应用的所有小窗口对象。它们可能是文字标签、按钮、列表框等。
这些独立的GUI组件称为控件。所以当我们说创建一个顶层窗口时,只是表示需要一个地方来摆放所有的控件。在Python中,一般会写成如下语句。
top = Tkinter.Tk() # or just Tk() with "from Tkinter import *"
Tkinter.Tk()返回的对象通常称为根窗口,这也是一些应用使用root而不是top来指代它的原因
顶层窗口是那些在应用中独立显示的部分。GUI程序中可以有多个顶层窗口,但是其中只能有一个是根窗口。
可以选择先把控件全部设计好,再添加功能;也可以边设计控件边添加功能(这意味着上述步骤中的第3步和第4步会混合起来做)
控件可以独立存在,也可以作为容器存在。如果一个控件包含其他控件,就可以将其认为是那些控件的父控件。
相应地,如果一个控件被其他控件包含,则将其认为是那个控件的子控件,而父控件就是下一个直接包围它的容器控件。
通常,控件有一些相关的行为,比如按下按钮、将文本写入文本框等。这些用户行为称为事件,而GUI对这类事件的响应称为回调。
事件驱动处理
事件可以包括按钮按下(及释放)、鼠标移动、敲击回车键等。一个GUI应用从开始到结束就是通过整套事件体系来驱动的。这种方式称为事件驱动处理
最简单的鼠标移动就是一个带有回调的事件的例子。假设鼠标指针正停在GUI应用顶层窗口的某处。如果你将鼠标移动到应用的另一部分,
鼠标移动的行为会被复制到屏幕的光标上,于是看起来像是根据你的手移动的。系统必须处理的这些鼠标移动事件可以绘制窗口上的指针移动。
当释放鼠标时,不再有事件需要处理,此时屏幕会重新恢复闲置的状态
事件驱动的GUI处理本质上非常适合于客户端/服务端架构。当启动一个GUI应用时,需要一些启动步骤来准备核心部分的执行,
就像网络服务器启动时必须先分配套接字并将其绑定到本地地址上一样。GUI应用必须先创建所有的GUI组件,
然后将它们绘制在屏幕上。这是布局管理器(geometry manager)的职责所在(稍后会详细介绍)。
当布局管理器排列好所有控件(包括顶层窗口)后,GUI应用进入其类似服务器的无限循环。这个循环会一直运行,直到出现GUI事件,进行处理,然后再等待更多的事件去处理
布局管理器
Tk有3种布局管理器来帮助控件集进行定位。最原始的一种称为 Placer。它的做法非常直接:你提供控件的大小和摆放位置,然后管理器就会将其摆放好。
问题是你必须对所有控件进行这些操作,这样就会加重编程开发者的负担,因为这些操作本应该是自动完成的
第二种布局管理器会是你主要使用的,它叫做Packer,这个命名十分恰当,因为它会把控件填充到正确的位置(即指定的父控件中),
然后对于之后的每个控件,会去寻找剩余的空间进行填充。这个处理很像是旅行时往行李箱中填充行李的过程
第三种布局管理器是Grid。你可以基于网格坐标,使用Grid来指定GUI控件的放置。Grid会在它们的网格位置上渲染GUI应用中的每个对象。
一旦Packer确定好所有控件的大小和对齐方式,它就会在屏幕上将其放置妥当
当所有控件摆放好后,可以让应用进入前述的无限主循环中
一般这是程序运行的最后一段代码。当进入主循环后,GUI就从这里开始接管程序的执行。
所有其他行为都会通过回调来处理,甚至包括退出应用。当选择File菜单并单击Exit菜单选项,或者直接关闭窗口时,就会调用一个回调函数来结束这个GUI应用
46: 顶层窗口:Tkinter.Tk()
所有主要控件都是构建在顶层窗口对象之上的。该对象在Tkinter中使用Tk类进行创建,然后进行如下实例化:
>>> import Tkinter >>> top = Tkinter.Tk()
在这个窗口中,可以放置独立的控件,也可以将多个组件拼凑在一起来构成GUI程序。那么有哪些种类的控件呢?现在就介绍这些Tk控件。
47:常用的Tk控件
tkinter.Button() 与Label类似,但提供额外的功能,如鼠标悬浮、按下、释放以及键盘活动/事件 tkinter.Canvas() 提供绘制形状的功能(线段、椭圆、多边形、矩形),可以包含图像或位图 tkinter.Checkbutton() 一组选框,可以勾选其中的任意个(与 HTML的checkbox输入类似) tkinter.Entry() 单行文本框,用于收集键盘输入(与HTML的文本输入类似) tkinter.Frame() 包含其他控件的纯容器 tkinter.Label() 用于包含文本或图像 tkinter.LabelFrame() 标签和框架的组合,拥有额外的标签属性 tkinter.Listbox() 给用户显示一个选项列表来进行选择 tkinter.Menu() 按下Menubutton后弹出的选项列表,用户可以从中选择 tkinter.Menubutton() 用于包含菜单(下拉、级联等) tkinter.Message() 消息。与 Label类似,不过可以显示成多行 tkinter.PanedWindow() 一个可以控制其他控件在其中摆放的容器控件 tkinter.Radiobutton() 一组按钮,其中只有一个可以“按下”(与HTML 的radio输入类似) tkinter.Scale() 线性“滑块”控件,根据已设定的起始值和终止值,给出当前设定的精确值 tkinter.Scrollbar() 为Text、Canvas、Listbox、Enter等支持的控件提供滚动功能 tkinter.Spinbox() Entry和 Button的组合,允许对值进行调整 tkinter.Text() 多行文本框,用于收集(或显示)用户输入的文本(与HTML 的textarea类似) tkinter.Toplevel() 与 Frame类似,不过它提供了一个单独的窗口容器
GUI开发利用了Python的默认参数,因为Tkinter的控件中有很多默认行为。除非你非常清楚自己所使用的每个控件的每个可用选项的用法,
否则最好还是只关心你要设置的那些参数,而让系统去处理剩下的参数。这些默认值都是精心选择出来的。
即使没有提供这些值,也不用担心应用程序在屏幕上的显示会有什么问题。
作为一条基本规则,程序是由一系列优化后的默认参数创建的,只有当你知道如何精确定制你的控件时,才应该使用非默认值
48: Label控件
import tkinter top = tkinter.Tk() label = tkinter.Label(top, text="Hello World") label.pack() tkinter.mainloop()
# 创建一个顶层窗口。接下来是Label控件,它包含了那串久负盛名的字符串。
# 然后让Packer来管理和显示控件,最后调用mainloop()运行这个GUI应用
49:Button控件
import tkinter top = tkinter.Tk() quit = tkinter.Button(top, text="Hello World", command=top.quit) quit.pack() tkinter.mainloop()
该按钮有一个额外的参数:Tkinter.quit()方法。该参数会给按钮安装一个回调函数,当按钮被按下(并且释放)后,整个程序就会退出。最后两行是通用的 pack()方法和 mainloop()调用。
50:Label和Button控件
除了控件的额外参数之外,还可以看到Packer的一些参数。fill参数告诉Packer让QUIT按钮占据剩余的水平空间,
而expand参数则会引导它填充整个水平可视空间,将按钮拉伸到左右窗口边缘
import tkinter top = tkinter.Tk() hello = tkinter.Label(top, text="Hello World") hello.pack() quit = tkinter.Button(top, text="QUIT", command=top.quit, bg="red", fg="white") quit.pack(fill=tkinter.X, expand=1) tkinter.mainloop()
#,在Packer没有收到其他指示时,所有控件都是垂直排列的(自上而下依次排列)。如果想要水平布局则需要创建一个新的 Frame 对象来添加按钮。该框架将作为单个子对象来代替父对象的位置
51:Label、Button和Scale控件
这里Scale用于与Label控件进行交互。Scale滑块是用来控制Label控件中文字字体大小的工具。滑块的位置值越大,字体越大;反之亦然
本脚本的新功能包括一个resize()回调函数,该函数会依附于Scale控件。当Scale控件的滑块移动时,这个函数就会被激活,用来调整Label控件中的文本大小
还定义了顶层窗口的大小为250*150。本脚本与之前3个脚本的最后一个不同之处是导入Tkinter模块的属性到命名空间时使用的是from Tkinter import *。
尽管因为会污染命名空间而不推荐这种做法,但是这里依然如此使用的主要原因是这个应用会涉及对Tkinter属性的大量引用。
直接导入Tkinter模块会造成访问每个属性时都需要使用其完整写法。而使用这种不推荐的简写方式虽然付出了一定代价,但是可以减少输入,并使得代码更加易读
了解控件是如何通过回调函数(如resize())与其他控件进行通信的。Label控件中的文本会受到Scale控件上操作的影响。
from tkinter import * def resize(ev=None): label.config(font=f"Helvetica -{scale.get()} bold") top = Tk() top.geometry('250x150') label = Label(top, text="Hello World", font="Helvetica -12 bold") label.pack(fill=Y, expand=1) scale = Scale(top, from_=10, to=40, orient=HORIZONTAL, command=resize) scale.set(12) # 应用启动时滑块的初始值设定为12 scale.pack(fill=X, expand=1) quit = Button(top, text="QUIT", command=top.quit, activeforeground="white", activebackground="red") quit.pack() mainloop()
52:使用sorket模拟http请求发送formdata类型的数据给服务器
import socket import re import ssl import json import requests def get_header(r): # 解析服务器响应头 headers = [] v = b'' try: while 1: b = r.recv(1) if b == b'\r': next_b = r.recv(1) if next_b == b'\n': next_2b = r.recv(2) headers.append(v.decode()) v = b'' if next_2b == b'\r\n': break else: v += next_2b else: v += b elif b == b'': return else: v += b except: return else: rtv = dict() rtv['http_type'] = headers[0] for _i in range(1, len(headers)): try: key = headers[_i].split(':')[0] val = headers[_i].split(':')[1].strip() rtv[key] = val except Exception as e: print(headers) raise Exception(e) return rtv def read_line(r): res = [] while 1: b = r.recv(1) if b == b'\r': next_b = r.recv(1) if next_b == b'\n': break else: res.append(b) else: res.append(b) return b''.join(res) def my_form_data_upload(url, files, headers={}, data=None): boundary = 'kahsdakhdalksjdlsasda9203' _headers = { 'Content-Type': f'multipart/form-data; boundary={boundary}', 'Sec-Fetch-Dest': 'document', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Sec-Fetch-Site': 'cross-site', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-User': '?1', 'Sec-Fetch-Dest': 'document' } headers.update(_headers) form_data = form_data_body_gen(boundary, files, data) headers['Content-Length'] = len(form_data) res = analysis_url(url) if res: host = res.group('host') print('host: ', host) headers['Host'] = host path = res.group('path') if len(host.split(':')) == 2: port = int(host.split(':')[1]) _host = host.split(':')[0] else: port = 80 _host = host request_type = res.group('method') bin_headers = get_headers(headers) bin_headers = f'POST /{path} HTTP/1.1\r\n'.encode('utf-8') + bin_headers print(bin_headers) _s = socket.socket() if request_type == 'http': _s.connect((_host, port)) s = _s else: _s.connect((_host, 443)) context = ssl.create_default_context() s = context.wrap_socket(_s, server_hostname=_host) s.send(bin_headers + form_data) resp_header = get_header(s) print(resp_header) if resp_header['content-type'] == 'application/json;charset=UTF-8': buf = read_line(s) lens = eval('0x' + buf.decode('utf-8')) content = s.recv(lens) return json.loads(content.decode('utf-8')) s.close() else: raise Exception('url error!') def analysis_url(url): rule = "^(?P<method>https?)://(?P<host>.*?)/(?P<path>.*?)$" return re.match(rule, url) def form_data_body_gen(boundary, files, data): for k, v in files.items(): _name = k # file fd = v # 文件句柄 print(f"文件为:{_name}, {fd}") file_name = fd.name # 根据文件句柄获取文件名词 form_data_body = f'--{boundary}\r\n'.encode('utf-8') form_data_body += f'Content-Disposition: form-data; name="{_name}"; filename="{file_name}"\r\nContent-Type: image/png\r\n\r\n'.encode( 'utf-8') + fd.read() form_data_body += f'\r\n--{boundary}--\r\n'.encode('utf-8') if data is not None: for k, v in data.items(): form_data_body += f"{k}={v}\r\n".encode('utf-8') # print(form_data_body.decode('utf-8')) return form_data_body
def get_headers(headers): headers_strs = '' for k, v in headers.items(): _tmp = f"{k}: {v}\r\n" headers_strs += _tmp headers_strs += '\r\n' return headers_strs.encode('utf-8') if __name__ == '__main__': file_path = '005A0PMely1fv97jt4atcj30dc0dcjrs.jpg' url = 'https://dev-gateway.zhuanxin.com/sdk/v1/upload/file' files = {'file': open(file_path, 'rb')} headers = { 'accessToken': '86V6AjRkaBHgQgBf5vjZrq/59fWnBJ0PlvR+poqHooRekYSHCj3ASfgyMZ+1fVD4AjxYp8BgbvN5+s31BeMQn/6g93Jbip/SrhvdGlilH9E/goEqM006yBPoUBgL+WZv' } data = {"scene": "CHAT_IMAGE"} res = my_form_data_upload(url, files, headers=headers, data=data) print(res)
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/155832.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...