概述
Python3 正则表达式
2017年4月13日
11:20
正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。
Python 自1.5版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。
re 模块使 Python 语言拥有全部的正则表达式功能。
compile 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。
re 模块也提供了与这些方法功能完全一致的函数,这些函数使用一个模式字符串做为它们的第一个参数。
本章节主要介绍Python中常用的正则表达式处理函数。
re.match函数
re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none。
函数语法:
re.match(pattern, string,flags=0)
函数参数说明:
参数 | 描述 |
pattern | 匹配的正则表达式 |
string | 要匹配的字符串。 |
flags | 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。 |
匹配成功re.match方法返回一个匹配的对象,否则返回None。
我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。
匹配对象方法 | 描述 |
group(num=0) | 匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。 |
groups() | 返回一个包含所有小组字符串的元组,从 1 到所含的小组号。 |
实例 1:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import re
print(re.match('www', 'www.runoob.com').span()) # 在起始位置匹配
print(re.match('com', 'www.runoob.com')) # 不在起始位置匹配
以上实例运行输出结果为:
(0, 3)
None
实例 2:
#!/usr/bin/python3
import re
line = "Cats are smarterthan dogs"
matchObj = re.match( r'(.*) are(.*?) .*', line, re.M|re.I)
if matchObj:
print ("matchObj.group() :", matchObj.group())
print ("matchObj.group(1) :", matchObj.group(1))
print ("matchObj.group(2) :", matchObj.group(2))
else:
print ("No match!!")
以上实例执行结果如下:
matchObj.group() : Cats are smarter than dogs
matchObj.group(1) : Cats
matchObj.group(2) : smarter
re.search方法
re.search 扫描整个字符串并返回第一个成功的匹配。
函数语法:
re.search(pattern, string,flags=0)
函数参数说明:
参数 | 描述 |
pattern | 匹配的正则表达式 |
string | 要匹配的字符串。 |
flags | 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。 |
匹配成功re.search方法返回一个匹配的对象,否则返回None。
我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。
匹配对象方法 | 描述 |
group(num=0) | 匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。 |
groups() | 返回一个包含所有小组字符串的元组,从 1 到所含的小组号。 |
实例 1:
#!/usr/bin/python3
import re
print(re.search('www', 'www.runoob.com').span()) # 在起始位置匹配
print(re.search('com', 'www.runoob.com').span()) # 不在起始位置匹配
以上实例运行输出结果为:
(0, 3)
(11, 14)
实例 2:
#!/usr/bin/python3
import re
line = "Cats are smarterthan dogs";
searchObj = re.search( r'(.*)are (.*?) .*', line, re.M|re.I)
if searchObj:
print ("searchObj.group() :", searchObj.group())
print ("searchObj.group(1) :", searchObj.group(1))
print ("searchObj.group(2) :", searchObj.group(2))
else:
print ("Nothing found!!")
以上实例执行结果如下:
searchObj.group() : Cats are smarter than dogs
searchObj.group(1) : Cats
searchObj.group(2) : smarter
re.match与re.search的区别
re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。
实例:
#!/usr/bin/python3
import re
line = "Cats are smarterthan dogs";
matchObj = re.match( r'dogs',line, re.M|re.I)
if matchObj:
print ("match -->matchObj.group() : ", matchObj.group())
else:
print ("No match!!")
matchObj = re.search( r'dogs',line, re.M|re.I)
if matchObj:
print ("search -->matchObj.group() : ", matchObj.group())
else:
print ("No match!!")
以上实例运行结果如下:
No match!!
search --> matchObj.group() : dogs
检索和替换
Python 的re模块提供了re.sub用于替换字符串中的匹配项。
语法:
re.sub(pattern, repl, string,count=0)
参数:
· pattern : 正则中的模式字符串。
· repl : 替换的字符串,也可为一个函数。
· string : 要被查找替换的原始字符串。
· count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
实例:
#!/usr/bin/python3
import re
phone = "2004-959-559 # 这是一个电话号码"
# 删除注释
num = re.sub(r'#.*$', "", phone)
print ("电话号码 : ", num)
# 移除非数字的内容
num = re.sub(r'D', "", phone)
print ("电话号码 : ", num)
以上实例执行结果如下:
电话号码 : 2004-959-559
电话号码: 2004959559
repl 参数是一个函数
以下实例中将字符串中的匹配的数字乘于 2:
#!/usr/bin/python
import re
# 将匹配的数字乘于2
def double(matched):
value = int(matched.group('value'))
return str(value * 2)
s = 'A23G4HFD567'
print(re.sub('(?P<value>d+)', double, s))
执行输出结果为:
A46G8HFD1134
正则表达式修饰符 - 可选标志
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。多个标志可以通过按位 OR(|) 它们来指定。如 re.I | re.M 被设置成 I 和 M 标志:
修饰符 | 描述 |
re.I | 使匹配对大小写不敏感 |
re.L | 做本地化识别(locale-aware)匹配 |
re.M | 多行匹配,影响 ^ 和 $ |
re.S | 使 . 匹配包括换行在内的所有字符 |
re.U | 根据Unicode字符集解析字符。这个标志影响 w, W, b, B. |
re.X | 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。 |
正则表达式模式
模式字符串使用特殊的语法来表示一个正则表达式:
字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。
多数字母和数字前加一个反斜杠时会拥有不同的含义。
标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。
反斜杠本身需要使用反斜杠转义。
由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 r'/t',等价于'//t')匹配相应的特殊字符。
下表列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。
模式 | 描述 |
^ | 匹配字符串的开头 |
$ | 匹配字符串的末尾。 |
. | 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。 |
[...] | 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k' |
[^...] | 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。 |
re* | 匹配0个或多个的表达式。 |
re+ | 匹配1个或多个的表达式。 |
re? | 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式 |
re{ n} |
|
re{ n,} | 精确匹配n个前面表达式。 |
re{ n, m} | 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 |
a| b | 匹配a或b |
(re) | G匹配括号内的表达式,也表示一个组 |
(?imx) | 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。 |
(?-imx) | 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。 |
(?: re) | 类似 (...), 但是不表示一个组 |
(?imx: re) | 在括号中使用i, m, 或 x 可选标志 |
(?-imx: re) | 在括号中不使用i, m, 或 x 可选标志 |
(?#...) | 注释. |
(?= re) | 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 |
(?! re) | 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功 |
(?> re) | 匹配的独立模式,省去回溯。 |
w | 匹配字母数字 |
W | 匹配非字母数字 |
s | 匹配任意空白字符,等价于 [tnrf]. |
S | 匹配任意非空字符 |
d | 匹配任意数字,等价于 [0-9]. |
D | 匹配任意非数字 |
A | 匹配字符串开始 |
Z | 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。c |
z | 匹配字符串结束 |
G | 匹配最后匹配完成的位置。 |
b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'erb' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。 |
B | 匹配非单词边界。'erB' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。 |
n, t, 等. | 匹配一个换行符。匹配一个制表符。等 |
1...9 | 匹配第n个分组的子表达式。 |
10 | 匹配第n个分组的子表达式,如果它经匹配。否则指的是八进制字符码的表达式。 |
正则表达式实例
字符匹配
实例 | 描述 |
python | 匹配 "python". |
字符类
实例 | 描述 |
[Pp]ython | 匹配 "Python" 或 "python" |
rub[ye] | 匹配 "ruby" 或 "rube" |
[aeiou] | 匹配中括号内的任意一个字母 |
[0-9] | 匹配任何数字。类似于 [0123456789] |
[a-z] | 匹配任何小写字母 |
[A-Z] | 匹配任何大写字母 |
[a-zA-Z0-9] | 匹配任何字母及数字 |
[^aeiou] | 除了aeiou字母以外的所有字符 |
[^0-9] | 匹配除了数字外的字符 |
特殊字符类
实例 | 描述 |
. | 匹配除 "n" 之外的任何单个字符。要匹配包括 'n' 在内的任何字符,请使用象 '[.n]' 的模式。 |
d | 匹配一个数字字符。等价于 [0-9]。 |
D | 匹配一个非数字字符。等价于 [^0-9]。 |
s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ fnrtv]。 |
S | 匹配任何非空白字符。等价于 [^ fnrtv]。 |
w | 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。 |
W | 匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。 |
常见数据正则分析
2017年5月17日
21:51
QQ号表达式:
分析:
1、首先扣扣号开头不能为0;
2、QQ号必须大于5且小于11(或12,13,QQ号最长位);
则正则表达式为: “[1-9]\d{4,10}"
解析:
[1-9]为第一个数(第一个数不为0);
\d:第一''为转义字符,'d'为产生[0-9]的数字(第二位往后数字任意);
{4,10}表示至少4次最多10次(因为[1-9]占1位,剩下4或10位,这里默认扣扣号最短5位,最长10位);
手机号表达式:
分析:
1、手机号位数为11位;
2、开头为1,第二位为3或4或5或8;
则表达式为: ”1[3458]\d{9}
解析:
1:开头必须为1;
[3458]:第二位;
\d: 第一个为转义字符,'d'为产生任意数字;
{9}:恰好出现9次;
扩展:写出手机号表达式,且后5位相同;
”1[3458]\d{4}(\d)\1{4}"
解析:
1:开头必须为1;
[3458]:第二位;
"\d{4}":产生[0-9]的数字恰好出现4次(由于前面占2位,后面重复5次,11减去7,还剩4位);
"(\d)\1{4}”:首先圆括号()表示组的意思,'\1'中的1表示的是第一组,第一个‘''表示转义,{4}出现4次,产生一个数最为一个组,将这组元素再重复4次; 注:(组:(\d)\1(\d)\2中的\2表示第二组);
邮箱表达式:
xxxx@xxx.com/cn/com.cn
eg:haha@163.com;ha_ha@alibaba.com.cn; ha-ha@sina.com;123456789@qq.com等;
分析:
1、@符号前面的可以为字母,数字,下划线,中划线,或'.';
2、@后面的可以是xxx.com、xxx.cn、xxx.com.cn;
表达式:"[\w-\.]+@([\w]+\.)+[a-z]{2,3}"
解析:
[\w-\.]: "\w"为产生单个字符(a-z或A-Z或[0-9]),‘-’可能出现为中划线,“\."表示可能出现'.' ;
’+‘表示'[]'里面出现一次或多次;
’@‘:为邮箱里面的@符号;
([\w]+\.)+:首先[\w]+表示单个字符(a-z或A-Z或[0-9])出现一次或多次(如以上邮箱:@163,@alibaba,@sina,@qq,分别出现3,7,4,2次); ([\w]+\.)+:将()里面看成一组,()+这组出现一次或多次(为什么加上'.'?? ,因为后缀有两种格式:xxx@xxx.com或者xxx@xxx.com.cn,将(xxx.)和(com.)可看成相同格式的组);
[a-z]{2,3}: 产生后缀.com或.cn,所以出现2到3次;
用户名表达式:
题目:必须以字母开头,长度在10位以内
表达式:"[a-zA-z]\w{0,9}"
解析:
[a-zA-Z]:用户名的第一位数为字母,[a-zA-z]表示产生这个a-z或A-Z范围的字母;
\w{0,9}: \w产生单个字符 (a-z或A-Z或[0-9]),{0,9}:为至少0次最多9次;
密码表达式:
题目:任意字符,6~16位
表达式:".{6,16}"
解析:
'.' : 为产生任意字符;
{6,16}:至少6位,最多16位;
Python正则表达式七种兵器
2017年5月17日
22:14
1. 正则表达式基础
1.1. 简单介绍
正则表达式并不是Python的一部分。正则表达式是用于处理字符串的强大工具,拥有自己独特的语法以及一个独立的处理引擎,效率上可能不如str自带的方法,但功能十分强大。得益于这一点,在提供了正则表达式的语言里,正则表达式的语法都是一样的,区别只在于不同的编程语言实现支持的语法数量不同;但不用担心,不被支持的语法通常是不常用的部分。如果已经在其他语言里使用过正则表达式,只需要简单看一看就可以上手了。
下图展示了使用正则表达式进行匹配的流程:
正则表达式的大致匹配过程是:依次拿出表达式和文本中的字符比较,如果每一个字符都能匹配,则匹配成功;一旦有匹配不成功的字符则匹配失败。如果表达式中有量词或边界,这个过程会稍微有一些不同,但也是很好理解的,看下图中的示例以及自己多使用几次就能明白。
下图列出了Python支持的正则表达式元字符和语法:
1.2. 数量词的贪婪模式与非贪婪模式
正则表达式通常用于在文本中查找匹配的字符串。Python里数量词默认是贪婪的(在少数语言里也可能是默认非贪婪),总是尝试匹配尽可能多的字符;非贪婪的则相反,总是尝试匹配尽可能少的字符。例如:正则表达式"ab*"如果用于查找"abbbc",将找到"abbb"。而如果使用非贪婪的数量词"ab*?",将找到"a"。
1.3. 反斜杠的困扰
与大多数编程语言相同,正则表达式里使用""作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符"",那么使用编程语言表示的正则表达式里将需要4个反斜杠"\\":前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。Python里的原生字符串很好地解决了这个问题,这个例子中的正则表达式可以使用r"\"表示。同样,匹配一个数字的"\d"可以写成r"d"。有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。
1.4. 匹配模式
正则表达式提供了一些可用的匹配模式,比如忽略大小写、多行匹配等,这部分内容将在Pattern类的工厂方法re.compile(pattern[, flags])中一起介绍。
2. re模块
2.1. 开始使用re
Python通过re模块提供对正则表达式的支持。使用re的一般步骤是先将正则表达式的字符串形式编译为Pattern实例,然后使用Pattern实例处理文本并获得匹配结果(一个Match实例),最后使用Match实例获得信息,进行其他的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # encoding: UTF-8 import re
# 将正则表达式编译成Pattern对象 pattern = re.compile(r'hello')
# 使用Pattern匹配文本,获得匹配结果,无法匹配时将返回None match = pattern.match('hello world!')
if match: # 使用Match获得分组信息 print match.group()
### 输出 ### # hello |
re.compile(strPattern[, flag]):
这个方法是Pattern类的工厂方法,用于将字符串形式的正则表达式编译为Pattern对象。 第二个参数flag是匹配模式,取值可以使用按位或运算符'|'表示同时生效,比如re.I | re.M。另外,你也可以在regex字符串中指定模式,比如re.compile('pattern', re.I | re.M)与re.compile('(?im)pattern')是等价的。
可选值有:
· re.I(re.IGNORECASE): 忽略大小写(括号内是完整写法,下同)
· M(MULTILINE): 多行模式,改变'^'和'$'的行为(参见上图)
· S(DOTALL): 点任意匹配模式,改变'.'的行为
· L(LOCALE): 使预定字符类 w W b B s S 取决于当前区域设定
· U(UNICODE): 使预定字符类 w W b B s S d D 取决于unicode定义的字符属性
· X(VERBOSE): 详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。以下两个正则表达式是等价的:
1 2 3 4 | a = re.compile(r"""d + # the integral part . # the decimal point d * # some fractional digits""", re.X) b = re.compile(r"d+.d*") |
re提供了众多模块方法用于完成正则表达式的功能。这些方法可以使用Pattern实例的相应方法替代,唯一的好处是少写一行re.compile()代码,但同时也无法复用编译后的Pattern对象。这些方法将在Pattern类的实例方法部分一起介绍。如上面这个例子可以简写为:
1 2 | m = re.match(r'hello', 'hello world!') print m.group() |
re模块还提供了一个方法escape(string),用于将string中的正则表达式元字符如*/+/?等之前加上转义符再返回,在需要大量匹配元字符时有那么一点用。
2.2. Match
Match对象是一次匹配的结果,包含了很多关于此次匹配的信息,可以使用Match提供的可读属性或方法来获取这些信息。
属性:
· string: 匹配时使用的文本。
· re: 匹配时使用的Pattern对象。
· pos: 文本中正则表达式开始搜索的索引。值与Pattern.match()和Pattern.seach()方法的同名参数相同。
· endpos: 文本中正则表达式结束搜索的索引。值与Pattern.match()和Pattern.seach()方法的同名参数相同。
· lastindex: 最后一个被捕获的分组在文本中的索引。如果没有被捕获的分组,将为None。
· lastgroup: 最后一个被捕获的分组的别名。如果这个分组没有别名或者没有被捕获的分组,将为None。
方法:
· group([group1, …]):
获得一个或多个分组截获的字符串;指定多个参数时将以元组形式返回。group1可以使用编号也可以使用别名;编号0代表整个匹配的子串;不填写参数时,返回group(0);没有截获字符串的组返回None;截获了多次的组返回最后一次截获的子串。
· groups([default]):
以元组形式返回全部分组截获的字符串。相当于调用group(1,2,…last)。default表示没有截获字符串的组以这个值替代,默认为None。
· groupdict([default]):
返回以有别名的组的别名为键、以该组截获的子串为值的字典,没有别名的组不包含在内。default含义同上。
· start([group]):
返回指定的组截获的子串在string中的起始索引(子串第一个字符的索引)。group默认值为0。
· end([group]):
返回指定的组截获的子串在string中的结束索引(子串最后一个字符的索引+1)。group默认值为0。
· span([group]):
返回(start(group), end(group))。
· expand(template):
将匹配到的分组代入template中然后返回。template中可以使用id或g<id>、g<name>引用分组,但不能使用编号0。id与g<id>是等价的;但10将被认为是第10个分组,如果你想表达1之后是字符'0',只能使用g<1>0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import re m = re.match(r'(w+) (w+)(?P<sign>.*)', 'hello world!')
print "m.string:", m.string print "m.re:", m.re print "m.pos:", m.pos print "m.endpos:", m.endpos print "m.lastindex:", m.lastindex print "m.lastgroup:", m.lastgroup
print "m.group(1,2):", m.group(1, 2) print "m.groups():", m.groups() print "m.groupdict():", m.groupdict() print "m.start(2):", m.start(2) print "m.end(2):", m.end(2) print "m.span(2):", m.span(2) print r"m.expand(r'2 13'):", m.expand(r'2 13')
### output ### # m.string: hello world! # m.re: <_sre.SRE_Pattern object at 0x016E1A38> # m.pos: 0 # m.endpos: 12 # m.lastindex: 3 # m.lastgroup: sign # m.group(1,2): ('hello', 'world') # m.groups(): ('hello', 'world', '!') # m.groupdict(): {'sign': '!'} # m.start(2): 6 # m.end(2): 11 # m.span(2): (6, 11) # m.expand(r'2 13'): world hello! |
2.3. Pattern
Pattern对象是一个编译好的正则表达式,通过Pattern提供的一系列方法可以对文本进行匹配查找。
Pattern不能直接实例化,必须使用re.compile()进行构造。
Pattern提供了几个可读属性用于获取表达式的相关信息:
· pattern: 编译时用的表达式字符串。
· flags: 编译时用的匹配模式。数字形式。
· groups: 表达式中分组的数量。
· groupindex: 以表达式中有别名的组的别名为键、以该组对应的编号为值的字典,没有别名的组不包含在内。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import re p = re.compile(r'(w+) (w+)(?P<sign>.*)', re.DOTALL)
print "p.pattern:", p.pattern print "p.flags:", p.flags print "p.groups:", p.groups print "p.groupindex:", p.groupindex
### output ### # p.pattern: (w+) (w+)(?P<sign>.*) # p.flags: 16 # p.groups: 3 # p.groupindex: {'sign': 3} |
实例方法[ | re模块方法]:
match(string[,pos[, endpos]]) | re.match(pattern, string[, flags]):
这个方法将从string的pos下标处起尝试匹配pattern;如果pattern结束时仍可匹配,则返回一个Match对象;如果匹配过程中pattern无法匹配,或者匹配未结束就已到达endpos,则返回None。
pos和endpos的默认值分别为0和len(string);re.match()无法指定这两个参数,参数flags用于编译pattern时指定匹配模式。
注意:这个方法并不是完全匹配。当pattern结束时若string还有剩余字符,仍然视为成功。想要完全匹配,可以在表达式末尾加上边界匹配符'$'。
示例参见2.1小节。
search(string[,pos[, endpos]]) | re.search(pattern, string[, flags]):
这个方法用于查找字符串中可以匹配成功的子串。从string的pos下标处起尝试匹配pattern,如果pattern结束时仍可匹配,则返回一个Match对象;若无法匹配,则将pos加1后重新尝试匹配;直到pos=endpos时仍无法匹配则返回None。
pos和endpos的默认值分别为0和len(string));re.search()无法指定这两个参数,参数flags用于编译pattern时指定匹配模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # encoding: UTF-8 import re
# 将正则表达式编译成Pattern对象 pattern = re.compile(r'world')
# 使用search()查找匹配的子串,不存在能匹配的子串时将返回None # 这个例子中使用match()无法成功匹配 match = pattern.search('hello world!')
if match: # 使用Match获得分组信息 print match.group()
### 输出 ### # world |
split(string[,maxsplit]) | re.split(pattern, string[, maxsplit]):
按照能够匹配的子串将string分割后返回列表。maxsplit用于指定最大分割次数,不指定将全部分割。
1 2 3 4 5 6 7 | import re
p = re.compile(r'd+') print p.split('one1two2three3four4')
### output ### # ['one', 'two', 'three', 'four', ''] |
findall(string[,pos[, endpos]]) | re.findall(pattern, string[, flags]):
搜索string,以列表形式返回全部能匹配的子串。
1 2 3 4 5 6 7 | import re
p = re.compile(r'd+') print p.findall('one1two2three3four4')
### output ### # ['1', '2', '3', '4'] |
finditer(string[,pos[, endpos]]) | re.finditer(pattern, string[, flags]):
搜索string,返回一个顺序访问每一个匹配结果(Match对象)的迭代器。
1 2 3 4 5 6 7 8 | import re
p = re.compile(r'd+') for m in p.finditer('one1two2three3four4'): print m.group(),
### output ### # 1 2 3 4 |
sub(repl,string[, count]) | re.sub(pattern, repl, string[, count]):
使用repl替换string中每一个匹配的子串后返回替换后的字符串。
当repl是一个字符串时,可以使用id或g<id>、g<name>引用分组,但不能使用编号0。
当repl是一个方法时,这个方法应当只接受一个参数(Match对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。
count用于指定最多替换次数,不指定时全部替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import re
p = re.compile(r'(w+) (w+)') s = 'i say, hello world!'
print p.sub(r'2 1', s)
def func(m): return m.group(1).title() + ' ' + m.group(2).title()
print p.sub(func, s)
### output ### # say i, world hello! # I Say, Hello World! |
subn(repl,string[, count]) |re.sub(pattern, repl, string[, count]):
返回 (sub(repl, string[, count]), 替换次数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import re
p = re.compile(r'(w+) (w+)') s = 'i say, hello world!'
print p.subn(r'2 1', s)
def func(m): return m.group(1).title() + ' ' + m.group(2).title()
print p.subn(func, s)
### output ### # ('say i, world hello!', 2) # ('I Say, Hello World!', 2) |
pythonRE模块之purge 方法
照旧,我们使用help:
purge()
Clear the regular expression cache
解释:清空缓存中的正则表达式。
>>> p = re.compile(r'(w+) (w+)')
>>> p.search("hello world 123 zhou write").group() -- 匹配字符串中的两个单词正常
'hello world'
>>> p = re.purge() -- 清空RE缓存
>>> p.search("hello world 123 zhou write").group() -- 再次使用search,报错!此时p 变成了'NoneType'对象,而不是Pattern对象
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'search'
正则匹配
2017年5月17日
22:43
在正则表达式中,如果直接给出字符,就是精确匹配。用d可以匹配一个数字,w可以匹配一个字母或数字,所以:
· '00d'可以匹配'007',但无法匹配'00A';
· 'ddd'可以匹配'010';
· 'wwd'可以匹配'py3';
.可以匹配任意字符,所以:
· 'py.'可以匹配'pyc'、'pyo'、'py!'等等。
要匹配变长的字符,在正则表达式中,用*表示任意个字符(包括0个),用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n-m个字符:
来看一个复杂的例子:d{3}s+d{3,8}。
我们来从左到右解读一下:
· d{3}表示匹配3个数字,例如'010';
· s可以匹配一个空格(也包括Tab等空白符),所以s+表示至少有一个空格,例如匹配' ',' '等;
· d{3,8}表示3-8个数字,例如'1234567'。
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配'010-12345'这样的号码呢?由于'-'是特殊字符,在正则表达式中,要用''转义,所以,上面的正则是d{3}-d{3,8}。
但是,仍然无法匹配'010 - 12345',因为带有空格。所以我们需要更复杂的匹配方式。
进阶
要做更精确地匹配,可以用[]表示范围,比如:
· [0-9a-zA-Z_]可以匹配一个数字、字母或者下划线;
· [0-9a-zA-Z_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','Py3000'等等;
· [a-zA-Z_][0-9a-zA-Z_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;
· [a-zA-Z_][0-9a-zA-Z_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
A|B可以匹配A或B,所以(P|p)ython可以匹配'Python'或者'python'。
^表示行的开头,^d表示必须以数字开头。
$表示行的结束,d$表示必须以数字结束。
你可能注意到了,py也可以匹配'python',但是加上^py$就变成了整行匹配,就只能匹配'py'了。
提取子串非常有用。来看一个更凶残的例子:
>>>t = '19:05:30'
>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9]):(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9]):(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)
>>> m.groups()
('19', '05', '30')
这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:
'^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$'
对于'2-30','4-31'这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。
贪婪匹配
最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:
>>>re.match(r'^(d+)(0*)$', '102300').groups()
('102300', '')
由于d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
必须让d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让d+采用非贪婪匹配:
>>>re.match(r'^(d+?)(0*)$', '102300').groups()
('1023', '00')
正则表达式用法
2017年5月17日
22:08
1 Python正则式的基本用法
Python的正则表达式的模块是 ‘re’,它的基本语法规则就是指定一个字符序列,比如你要在一个字符串s=’123abc456’ 中查找字符串 ’abc’,只要这样写:
>>>import re
>>>s='123abc456eabc789'
>>>re.findall(r’abc’,s)
结果就是:
['abc','abc']
这里用到的函数 ”findall(rule , target [,flag] )” 是个比较直观的函数,就是在目标字符串中查找符合规则的字符串。第一个参数是规则,第二个参数是目标字符串,后面还可以跟一个规则选项(选项功能将在compile函数的说明中详细说明)。返回结果结果是一个列表,中间存放的是符合规则的字符串。如果没有符合规则的字符串被找到,就返回一个空列表。
为什么要用r’ ..‘字符串(raw字符串)?由于正则式的规则也是由一个字符串定义的,而在正则式中大量使用转义字符’’,如果不用raw字符串,则在需要写一个’’的地方,你必须得写成’\’,那么在要从目标字符串中匹配一个’’的时候,你就得写上4个’’成为’\\’!这当然很麻烦,也不直观,所以一般都使用r’’来定义规则字符串。当然,某些情况下,可能不用raw字符串比较好。
以上是个最简单的例子。当然实际中这么简单的用法几乎没有意义。为了实现复杂的规则查找,re规定了若干语法规则。它们分为这么几类:
功能字符 : ‘.’ ‘*’ ‘+’ ‘|’ ‘?’ ‘^’ ‘$’ ‘’ 等,它们有特殊的功能含义。特别是’/’字符,它是转义引导符号,跟在它后面的字符一般有特殊的含义。
规则分界符: ‘[‘ ‘]’ ‘(’ ‘)’ ‘{‘‘}’ 等,也就是几种括号了。
预定义转义字符集: “d” “w” “s”等等,它们是以字符’’开头,后面接一个特定字符的形式,用来指示一个预定义好的含义。
其它特殊功能字符: ’#’ ‘!’ ‘:’ ‘-‘等,它们只在特定的情况下表示特殊的含义,比如(?# …)就表示一个注释,里面的内容会被忽略。
下面来一个一个的说明这些规则的含义,不过说明的顺序并不是按照上面的顺序来的,而是我认为由浅入深,由基本到复杂的顺序来编排的。同时为了直观,在说明的过程中尽量多举些例子以方便理解。
1.1 基本规则
‘[‘‘]’ 字符集合设定符
首先说明一下字符集合设定的方法。由一对方括号括起来的字符,表明一个字符集合,能够匹配包含在其中的任意一个字符。比如 [abc123],表明字符’a’ ‘b’‘c’ ‘1’ ‘2’‘3’都符合它的要求。可以被匹配。
在’[‘‘]’中还可以通过 ’-‘ 减号来指定一个字符集合的范围,比如可以用[a-zA-Z]来指定所以英文字母的大小写,因为英文字母是按照从小到大的顺序来排的。你不可以把大小的顺序颠倒了,比如写成[z-a]就不对了。
如果在’[‘ ‘]’里面的开头写一个 ‘^’号,则表示取非,即在括号里的字符都不匹配。如[^a-zA-Z]表明不匹配所有英文字母。但是如果 ‘^’不在开头,则它就不再是表示取非,而表示其本身,如[a-z^A-Z]表明匹配所有的英文字母和字符’^’。
‘|’或规则
将两个规则并列起来,以‘|’连接,表示只要满足其中之一就可以匹配。比如
[a-zA-Z]|[0-9]表示满足数字或字母就可以匹配,这个规则等价于 [a-zA-Z0-9]
注意:关于’|’要注意两点:
第一, 它在’[‘ ‘]’之中不再表示或,而表示他本身的字符。如果要在’[‘ ‘]’外面表示一个’|’字符,必须用反斜杠引导,即’/|’ ;
第二, 它的有效范围是它两边的整条规则,比如‘dog|cat’匹配的是‘dog’和’cat’,而不是’g’和’c’。如果想限定它的有效范围,必需使用一个无捕获组 ‘(?: )’包起来。比如要匹配 ‘I have a dog’或’I have a cat’,需要写成r’Ihave a (?:dog|cat)’ ,而不能写成 r’Ihave a dog|cat’
例
>>>s = ‘I have a dog , I have a cat’
>>>re.findall( r’I have a (?:dog|cat)’ , s )
['I have adog', 'I have a cat'] #正如我们所要的
下面再看看不用无捕获组会是什么后果:
>>>re.findall( r’I have a dog|cat’ , s )
['I have adog', 'cat'] #它将’I have a dog’ 和’cat’当成两个规则了
至于无捕获组的使用,后面将仔细说明。这里先跳过。
‘.’匹配所有字符
匹配除换行符’n’外的所有字符。如果使用了’S’选项,匹配包括’n’的所有字符。
例:
>>>s=’123 n456 n789’
>>>findall(r‘.+’,s)
['123','456', '789']
>>>re.findall(r‘.+’ , s, re.S)
['123n456n789']
‘^’和’$’ 匹配字符串开头和结尾
注意’^’不能在‘[ ]’中,否则含意就发生变化,具体请看上面的’[‘ ‘]’说明。 在多行模式下,它们可以匹配每一行的行首和行尾。具体请看后面compile函数说明的’M’选项部分
‘d’匹配数字
这是一个以’’开头的转义字符,’d’表示匹配一个数字,即等价于[0-9]
‘D’匹配非数字
这个是上面的反集,即匹配一个非数字的字符,等价于[^0-9]。注意它们的大小写。下面我们还将看到Python的正则规则中很多转义字符的大小写形式,代表互补的关系。这样很好记。
‘w’匹配字母和数字
匹配所有的英文字母和数字,即等价于[a-zA-Z0-9]。
‘W’匹配非英文字母和数字
即’w’的补集,等价于[^a-zA-Z0-9]。
‘s’匹配间隔符
即匹配空格符、制表符、回车符等表示分隔意义的字符,它等价于[ trnfv]。(注意最前面有个空格)
‘S’匹配非间隔符
即间隔符的补集,等价于[^ trnfv]
‘A’匹配字符串开头
匹配字符串的开头。它和’^’的区别是,’A’只匹配整个字符串的开头,即使在’M’模式下,它也不会匹配其它行的行首。
‘Z’匹配字符串结尾
匹配字符串的结尾。它和’$’的区别是,’Z’只匹配整个字符串的结尾,即使在’M’模式下,它也不会匹配其它各行的行尾。
例:
>>>s= '12 34n56 78n90'
>>>re.findall( r'^d+' , s , re.M ) #匹配位于行首的数字
['12', '56','90']
>>>re.findall( r’Ad+’,s , re.M ) #匹配位于字符串开头的数字
['12']
>>>re.findall( r'd+$' , s , re.M ) #匹配位于行尾的数字
['34', '78','90']
>>>re.findall( r’d+Z’, s , re.M ) #匹配位于字符串尾的数字
['90']
‘b’匹配单词边界
它匹配一个单词的边界,比如空格等,不过它是一个‘0’长度字符,它匹配完的字符串不会包括那个分界的字符。而如果用’s’来匹配的话,则匹配出的字符串中会包含那个分界符。
例:
>>>s = 'abc abcde bc bcd'
>>>re.findall( r’bbcb’, s ) #匹配一个单独的单词 ‘bc’ ,而当它是其它单词的一部分的时候不匹配
['bc'] #只找到了那个单独的’bc’
>>>re.findall( r’sbcs’, s ) #匹配一个单独的单词 ‘bc’
[' bc '] #只找到那个单独的’bc’,不过注意前后有两个空格,可能有点看不清楚
‘B’匹配非边界
和’b’相反,它只匹配非边界的字符。它同样是个0长度字符。
接上例:
>>>re.findall( r’Bbcw+’ , s ) #匹配包含’bc’但不以’bc’为开头的单词
['bcde'] #成功匹配了’abcde’中的’bcde’,而没有匹配’bcd’
‘(?:)’无捕获组
当你要将一部分规则作为一个整体对它进行某些操作,比如指定其重复次数时,你需要将这部分规则用’(?:’ ‘)’把它包围起来,而不能仅仅只用一对括号,那样将得到绝对出人意料的结果。
例:匹配字符串中重复的’ab’
>>>s=’ababab abbabb aabaab’
>>>re.findall( r’b(?:ab)+b’ , s )
['ababab']
如果仅使用一对括号,看看会是什么结果:
>>>re.findall( r’bab)+b’ , s )
['ab']
这是因为如果只使用一对括号,那么这就成为了一个组(group)。组的使用比较复杂,将在后面详细讲解。
‘(?# )’ 注释
Python允许你在正则表达式中写入注释,在’(?#’ ‘)’之间的内容将被忽略。
(?iLmsux) 编译选项指定
Python的正则式可以指定一些选项,这个选项可以写在findall或compile的参数中,也可以写在正则式里,成为正则式的一部分。这在某些情况下会便利一些。具体的选项含义请看后面的compile函数的说明。
此处编译选项’i’ 等价于IGNORECASE ,L 等价于 LOCAL ,m 等价于 MULTILINE ,s 等价于 DOTALL ,u 等价于 UNICODE , x 等价于 VERBOSE 。
请注意它们的大小写。在使用时可以只指定一部分,比如只指定忽略大小写,可写为 ‘(?i)’,要同时忽略大小写并使用多行模式,可以写为 ‘(?im)’。
另外要注意选项的有效范围是整条规则,即写在规则的任何地方,选项都会对全部整条正则式有效。
1.2 重复
正则式需要匹配不定长的字符串,那就一定需要表示重复的指示符。Python的正则式表示重复的功能很丰富灵活。重复规则的一般的形式是在一条字符规则后面紧跟一个表示重复次数的规则,已表明需要重复前面的规则一定的次数。重复规则有:
‘*’ 0或多次匹配
表示匹配前面的规则0次或多次。
‘+’ 1次或多次匹配
表示匹配前面的规则至少1次,可以多次匹配
例:匹配以下字符串中的前一部分是字母,后一部分是数字或没有的变量名字
>>> s = ‘ aaa bbb111 cc22cc 33dd ‘
>>> re.findall( r’b[a-z]+d*b’ , s ) #必须至少1个字母开头,以连续数字结尾或没有数字
['aaa', 'bbb111']
注意上例中规则前后加了表示单词边界的’b’指示符,如果不加的话结果就会变成:
>>> re.findall( r’[a-z]+d*’ , s )
['aaa', 'bbb111', 'cc22', 'cc', 'dd'] #把单词给拆开了
大多数情况下这不是我们期望的结果。
‘?’ 0或1次匹配
只匹配前面的规则0次或1次。
例,匹配一个数字,这个数字可以是一个整数,也可以是一个科学计数法记录的数字,比如123和10e3都是正确的数字。
>>> s = ‘ 123 10e3 20e4e4 30ee5 ‘
>>> re.findall( r’ bd+[eE]?d*b’ , s )
['123', '10e3']
它正确匹配了123和10e3,正是我们期望的。注意前后的’b’的使用,否则将得到不期望的结果。
1.2.1 精确匹配和最小匹配
Python正则式还可以精确指定匹配的次数。指定的方式是
‘{m}’ 精确匹配m次
‘{m,n}’ 匹配最少m次,最多n次。(n>m)
如果你只想指定一个最少次数或只指定一个最多次数,你可以把另外一个参数空起来。比如你想指定最少3次,可以写成 {3,} (注意那个逗号),同样如果只想指定最大为5次,可以写成{,5},也可以写成{0,5}。
例 寻找下面字符串中
a:3位数
b: 2位数到4位数
c: 5位数以上的数
d: 4位数以下的数
>>> s= ‘ 1 22 333 4444 55555 666666 ‘
>>> re.findall( r’bd{3}b’ , s ) # a:3位数
['333']
>>> re.findall( r’bd{2,4}b’ , s ) # b: 2位数到4位数
['22', '333', '4444']
>>> re.findall( r’bd{5,}b’, s ) # c: 5位数以上的数
['55555', '666666']
>>> re.findall( r’bd{1,4}b’ , s ) # 4位数以下的数
['1', '22', '333', '4444']
‘*?’ ‘+?’ ‘??’ 最小匹配
‘*’ ‘+’ ‘?’通常都是尽可能多的匹配字符。有时候我们希望它尽可能少的匹配。比如一个c语言的注释 ‘/* part 1 */ /* part 2 */’,如果使用最大规则:
>>> s =r ‘/* part 1 */ code /* part 2 */’
>>> re.findall( r’/*.**/’ , s )
[‘/* part 1 */ code /* part 2 */’]
结果把整个字符串都包括进去了。如果把规则改写成
>>> re.findall( r’/*.*?*/’ , s ) #在*后面加上?,表示尽可能少的匹配
['/* part 1 */', '/* part 2 */']
结果正确的匹配出了注释里的内容
1.3 前向界定与后向界定
有时候需要匹配一个跟在特定内容后面的或者在特定内容前面的字符串,Python提供一个简便的前向界定和后向界定功能,或者叫前导指定和跟从指定功能。它们是:
‘(?<=…)’ 前向界定
括号中’…’代表你希望匹配的字符串的前面应该出现的字符串。
‘(?=…)’ 后向界定
括号中的’…’代表你希望匹配的字符串后面应该出现的字符串。
例: 你希望找出c语言的注释中的内容,它们是包含在’/*’和’*/’之间,不过你并不希望匹配的结果把’/*’和’*/’也包括进来,那么你可以这样用:
>>> s=r’/* comment 1 */ code /* comment 2 */’
>>> re.findall( r’(?<=/*).+?(?=*/)’ , s )
[' comment 1 ', ' comment 2 ']
注意这里我们仍然使用了最小匹配,以避免把整个字符串给匹配进去了。
要注意的是,前向界定括号中的表达式必须是常值,也即你不可以在前向界定的括号里写正则式。比如你如果在下面的字符串中想找到被字母夹在中间的数字,你不可以用前向界定:
例:
>>> s = ‘aaa111aaa , bbb222 , 333ccc ‘
>>> re.findall( r’(?<=[a-z]+)d+(?=[a-z]+)' , s ) # 错误的用法
它会给出一个错误信息:
error: look-behind requires fixed-width pattern
不过如果你只要找出后面接着有字母的数字,你可以在后向界定写正则式:
>>> re.findall( r’d+(?=[a-z]+)’, s )
['111', '333']
如果你一定要匹配包夹在字母中间的数字,你可以使用组(group)的方式
>>> re.findall (r'[a-z]+(d+)[a-z]+' , s )
['111']
组的使用将在后面详细讲解。
除了前向界定前向界定和后向界定外,还有前向非界定和后向非界定,它的写法为:
‘(?<!...)’前向非界定
只有当你希望的字符串前面不是’…’的内容时才匹配
‘(?!...)’后向非界定
只有当你希望的字符串后面不跟着’…’内容时才匹配。
接上例,希望匹配后面不跟着字母的数字
>>> re.findall( r’d+(?!w+)’ , s )
['222']
注意这里我们使用了w而不是像上面那样用[a-z],因为如果这样写的话,结果会是:
>>> re.findall( r’d+(?![a-z]+)’ , s )
['11', '222', '33']
这和我们期望的似乎有点不一样。它的原因,是因为’111’和’222’中的前两个数字也是满足这个要求的。因此可看出,正则式的使用还是要相当小心的,因为我开始就是这样写的,看到结果后才明白过来。不过Python试验起来很方便,这也是脚本语言的一大优点,可以一步一步的试验,快速得到结果,而不用经过烦琐的编译、链接过程。也因此学习Python就要多试,跌跌撞撞的走过来,虽然曲折,却也很有乐趣。
1.4 组的基本知识
上面我们已经看过了Python的正则式的很多基本用法。不过如果仅仅是上面那些规则的话,还是有很多情况下会非常麻烦,比如上面在讲前向界定和后向界定时,取夹在字母中间的数字的例子。用前面讲过的规则都很难达到目的,但是用了组以后就很简单了。
‘(‘’)’ 无命名组
最基本的组是由一对圆括号括起来的正则式。比如上面匹配包夹在字母中间的数字的例子中使用的(d+),我们再回顾一下这个例子:
>>> s = ‘aaa111aaa , bbb222 , 333ccc ‘
>>> re.findall (r'[a-z]+(d+)[a-z]+' , s )
['111']
可以看到findall函数只返回了包含在’()’中的内容,而虽然前面和后面的内容都匹配成功了,却并不包含在结果中。
除了最基本的形式外,我们还可以给组起个名字,它的形式是
‘(?P<name>…)’ 命名组
‘(?P’代表这是一个Python的语法扩展’<…>’里面是你给这个组起的名字,比如你可以给一个全部由数字组成的组叫做’num’,它的形式就是’(?P<num>d+)’。起了名字之后,我们就可以在后面的正则式中通过名字调用这个组,它的形式是
‘(?P=name)’ 调用已匹配的命名组
要注意,再次调用的这个组是已被匹配的组,也就是说它里面的内容是和前面命名组里的内容是一样的。
我们可以看更多的例子:请注意下面这个字符串各子串的特点。
>>> s='aaa111aaa,bbb222,333ccc,444ddd444,555eee666,fff777ggg'
我们看看下面的正则式会返回什么样的结果:
>>> re.findall( r'([a-z]+)d+([a-z]+)' , s ) # 找出中间夹有数字的字母
[('aaa', 'aaa'), ('fff', 'ggg')]
>>> re.findall( r '(?P<g1>[a-z]+)d+(?P=g1)' , s ) #找出被中间夹有数字的前后同样的字母
['aaa']
>>> re.findall( r'[a-z]+(/d+)([a-z]+)' , s ) #找出前面有字母引导,中间是数字,后面是字母的字符串中的中间的数字和后面的字母
[('111', 'aaa'), ('777', 'ggg')]
我们可以通过命名组的名字在后面调用已匹配的命名组,不过名字也不是必需的。
‘number’ 通过序号调用已匹配的组
正则式中的每个组都有一个序号,序号是按组从左到右,从1开始的数字,你可以通过下面的形式来调用已匹配的组
比如上面找出被中间夹有数字的前后同样的字母的例子,也可以写成:
>>> re.findall( r’([a-z]+)d+1’ , s )
['aaa']
结果是一样的。
我们再看一个例子
>>> s='111aaa222aaa111 , 333bbb444bb33'
>>> re.findall( r'(d+)([a-z]+)(d+)(2)(1)' , s ) #找出完全对称的 数字-字母-数字-字母-数字 中的数字和字母
[('111', 'aaa', '222', 'aaa', '111')]
Python2.4以后的re模块,还加入了一个新的条件匹配功能
‘(?(id/name)yes-pattern|no-pattern)’ 判断指定组是否已匹配,执行相应的规则
这个规则的含义是,如果id/name指定的组在前面匹配成功了,则执行yes-pattern的正则式,否则执行no-pattern的正则式。
举个例子,比如要匹配一些形如 usr@mail 的邮箱地址,不过有的写成< usr@mail >即用一对<>括起来,有点则没有,要匹配这两种情况,可以这样写
>>> s='<usr1@mail1> usr2@maill2'
>>> re.findall( r'(<)?s*(w+@w+)s*(?(1)>)' , s )
[('<', 'usr1@mail1'), ('', 'usr2@maill2')]
不过如果目标字符串如下
>>> s='<usr1@mail1> usr2@maill2 <usr3@mail3 usr4@mail4> < usr5@mail5 '
而你想得到要么由一对<>包围起来的一个邮件地址,要么得到一个没有被<>包围起来的地址,但不想得到一对<>中间包围的多个地址或不完整的<>中的地址,那么使用这个式子并不能得到你想要的结果
>>> re.findall( r'(<)?s*(w+@w+)s*(?(1)>)' , s )
[('<', 'usr1@mail1'), ('', 'usr2@maill2'), ('', 'usr3@mail3'), ('', 'usr4@mail4'), ('', 'usr5@mail5')]
它仍然找到了所有的邮件地址。
想要实现这个功能,单纯的使用findall有点吃力,需要使用其它的一些函数,比如match或search函数,再配合一些控制功能。这部分的内容将在下面详细讲解。
小结:以上基本上讲述了Python正则式的语法规则。虽然大部分语法规则看上去都很简单,可是稍不注意,仍然会得到与期望大相径庭的结果,所以要写好正则式,需要仔细的体会正则式规则的含义后不同规则之间细微的差别。
详细的了解了规则后,再配合后面就要介绍的功能函数,就能最大的发挥正则式的威力了。
<FONT style="FONT-SIZE: 10.5pt" face=""">
2 re模块的基本函数
在上面的说明中,我们已经对re模块的基本函数 ‘findall’很熟悉了。当然如果光有findall的话,很多功能是不能实现的。下面开始介绍一下re模块其它的常用基本函数。灵活搭配使用这些函数,才能充分发挥Python正则式的强大功能。
首先还是说下老熟人findall函数吧
findall(rule , target [,flag] )
在目标字符串中查找符合规则的字符串。
第一个参数是规则,第二个参数是目标字符串,后面还可以跟一个规则选项(选项功能将在compile函数的说明中详细说明)。
返回结果结果是一个列表,中间存放的是符合规则的字符串。如果没有符合规则的字符串被找到,就返回一个空列表。
2.1 使用compile加速
compile( rule [,flag] )
将正则规则编译成一个Pattern对象,以供接下来使用。
第一个参数是规则式,第二个参数是规则选项。
返回一个Pattern对象
直接使用findall ( rule , target )的方式来匹配字符串,一次两次没什么,如果是多次使用的话,由于正则引擎每次都要把规则解释一遍,而规则的解释又是相当费时间的,所以这样的效率就很低了。如果要多次使用同一规则来进行匹配的话,可以使用re.compile函数来将规则预编译,使用编译过返回的Regular Expression Object或叫做Pattern对象来进行查找。
例
>>> s='111,222,aaa,bbb,ccc333,444ddd'
>>> rule=r’bd+b’
>>> compiled_rule=re.compile(rule)
>>> compiled_rule.findall(s)
['111', '222']
可见使用compile过的规则使用和未编译的使用很相似。compile函数还可以指定一些规则标志,来指定一些特殊选项。多个选项之间用 ’|’(位或)连接起来。
I IGNORECASE 忽略大小写区别。
L LOCAL 字符集本地化。这个功能是为了支持多语言版本的字符集使用环境的,比如在转义符w,在英文环境下,它代表[a-zA-Z0-9],即所以英文字符和数字。如果在一个法语环境下使用,缺省设置下,不能匹配"é" 或 "ç"。加上这L选项和就可以匹配了。不过这个对于中文环境似乎没有什么用,它仍然不能匹配中文字符。
M MULTILINE 多行匹配。在这个模式下’^’(代表字符串开头)和’$’(代表字符串结尾)将能够匹配多行的情况,成为行首和行尾标记。比如
>>> s=’123 456n789 012n345 678’
>>> rc=re.compile(r’^d+’) #匹配一个位于开头的数字,没有使用M选项
>>> rc.findall(s)
['123'] #结果只能找到位于第一个行首的’123’
>>> rcm=re.compile(r’^d+’,re.M) #使用 M 选项
>>> rcm.findall(s)
['123', '789', '345'] #找到了三个行首的数字
同样,对于’$’来说,没有使用M选项,它将匹配最后一个行尾的数字,即’678’,加上以后,就能匹配三个行尾的数字456 012和678了.
>>> rc=re.compile(r’d+$’)
>>> rcm=re.compile(r’d+$’,re.M)
>>> rc.findall(s)
['678']
>>> rcm.findall(s)
['456', '012', '678']
S DOTALL ‘.’号将匹配所有的字符。缺省情况下’.’匹配除换行符’n’外的所有字符,使用这一选项以后,’.’就能匹配包括’n’的任何字符了。
U UNICODE w, W, b, B, d, D, s 和 S都将使用Unicode。
X VERBOSE 这个选项忽略规则表达式中的空白,并允许使用’#’来引导一个注释。这样可以让你把规则写得更美观些。比如你可以把规则
>>> rc = re.compile(r"d+|[a-zA-Z]+") #匹配一个数字或者单词
使用X选项写成:
>>> rc = re.compile(r""" # start a rule
d+ # number
| [a-zA-Z]+ # word
""", re.VERBOSE)
在这个模式下,如果你想匹配一个空格,你必须用' '的形式(''后面跟一个空格)
2.2 match与search
match( rule , targetString [,flag] )
search( rule , targetString [,flag] )
(注:re的match 与search函数同compile过的Pattern对象的match与search函数的参数是不一样的。Pattern对象的match与search函数更为强大,是真正最常用的函数)
按照规则在目标字符串中进行匹配。
第一个参数是正则规则,第二个是目标字符串,第三个是选项(同compile函数的选项)
返回:若成功返回一个Match对象,失败无返回
findall虽然很直观,但是在进行更复杂的操作时,就有些力不从心了。此时更多的使用的是match和search函数。他们的参数和findall是一样的,都是:
match( rule , targetString [,flag] )
search( rule , targetString [,flag] )
不过它们的返回不是一个简单的字符串列表,而是一个MatchObject (如果匹配成功的话).。通过操作这个matchObject,我们可以得到更多的信息。
需要注意的是,如果匹配不成功,它们则返回一个NoneType。所以在对匹配完的结果进行操作之前,你必需先判断一下是否匹配成功了,比如:
>>> m=re.match( rule , target )
>>> if m: #必需先判断是否成功
doSomethin
这两个函数唯一的区别是:match从字符串的开头开始匹配,如果开头位置没有匹配成功,就算失败了;而search会跳过开头,继续向后寻找是否有匹配的字符串。针对不同的需要,可以灵活使用这两个函数。
关于match返回的MatchObject如果使用的问题,是Python正则式的精髓所在,它与组的使用密切相关。我将在下一部分详细讲解,这里只举个最简单的例子:
例:
>>> s= 'Tom:9527 , Sharry:0003'
>>> m=re.match( r'(?P<name>w+):(?P<num>d+)' , s )
>>> m.group()
'Tom:9527'
>>> m.groups()
('Tom', '9527')
>>> m.group(‘name’)
'Tom'
>>> m.group(‘num’)
'9527'
2.3 finditer
finditer( rule , target [,flag] )
参数同findall
返回一个迭代器
finditer函数和findall函数的区别是,findall返回所有匹配的字符串,并存为一个列表,而finditer则并不直接返回这些字符串,而是返回一个迭代器。关于迭代器,解释起来有点复杂,还是看看例子把:
>>> s=’111 222 333 444’
>>> for i in re.finditer(r’d+’ , s ):
print i.group(),i.span() #打印每次得到的字符串和起始结束位置
结果是
111 (0, 3)
222 (4, 7)
333 (8, 11)
444 (12, 15)
简单的说吧,就是finditer返回了一个可调用的对象,使用 for i in finditer()的形式,可以一个一个的得到匹配返回的 Match对象。这在对每次返回的对象进行比较复杂的操作时比较有用。
2.4 字符串的替换和修改
re模块还提供了对字符串的替换和修改函数,他们比字符串对象提供的函数功能要强大一些。这几个函数是
sub ( rule , replace , target [,count] )
subn(rule , replace , target [,count] )
在目标字符串中规格规则查找匹配的字符串,再把它们替换成指定的字符串。你可以指定一个最多替换次数,否则将替换所有的匹配到的字符串。
第一个参数是正则规则,第二个参数是指定的用来替换的字符串,第三个参数是目标字符串,第四个参数是最多替换次数。
这两个函数的唯一区别是返回值。
sub返回一个被替换的字符串
sub返回一个元组,第一个元素是被替换的字符串,第二个元素是一个数字,表明产生了多少次替换。
例,将下面字符串中的’dog’全部替换成’cat’
>>> s=’ I have a dog , you have a dog , he have a dog ‘
>>> re.sub( r’dog’ , ‘cat’ , s )
' I have a cat , you have a cat , he have a cat '
如果我们只想替换前面两个,则
>>> re.sub( r’dog’ , ‘cat’ , s , 2 )
' I have a cat , you have a cat , he have a dog '
或者我们想知道发生了多少次替换,则可以使用subn
>>> re.subn( r’dog’ , ‘cat’ , s )
(' I have a cat , you have a cat , he have a cat ', 3)
split( rule , target [,maxsplit] )
切片函数。使用指定的正则规则在目标字符串中查找匹配的字符串,用它们作为分界,把字符串切片。
第一个参数是正则规则,第二个参数是目标字符串,第三个参数是最多切片次数
返回一个被切完的子字符串的列表
这个函数和str对象提供的split函数很相似。举个例子,我们想把上例中的字符串被’,’分割开,同时要去掉逗号前后的空格
>>> s=’ I have a dog , you have a dog , he have a dog ‘
>>> re.split( ‘s*,s*’ , s )
[' I have a dog', 'you have a dog', 'he have a dog ']
结果很好。如果使用str对象的split函数,则由于我们不知道’,’两边会有多少个空格,而不得不对结果再进行一次处理。
escape( string )
这是个功能比较古怪的函数,它的作用是将字符串中的non-alphanumerics字符(我已不知道该怎么翻译比较好了)用反义字符的形式显示出来。有时候你可能希望在正则式中匹配一个字符串,不过里面含有很多re使用的符号,你要一个一个的修改写法实在有点麻烦,你可以使用这个函数,
例 在目标字符串s中匹配’(*+?)’这个子字符串
>>> s= ‘111 222 (*+?) 333’
>>> rule= re.escape( r’(*+?)’ )
>>> print rule
(*+?
>>>re.findall( rule , s )
['(*+?)']
<FONTstyle="FONT-SIZE: 10.5pt" face=""">
3 更深入的了解re的组与对象
前面对Python正则式的组进行了一些简单的介绍,由于还没有介绍到match对象,而组又是和match对象密切相关的,所以必须将它们结合起来介绍才能充分地说明它们的用途。
不过再详细介绍它们之前,我觉得有必要先介绍一下将规则编译后的生成的patter对象
3.1编译后的Pattern对象
将一个正则式,使用compile函数编译,不仅是为了提高匹配的速度,同时还能使用一些附加的功能。编译后的结果生成一个Pattern对象,这个对象里面有很多函数,他们看起来和re模块的函数非常象,它同样有findall , match , search ,finditer , sub , subn , split 这些函数,只不过它们的参数有些小小的不同。一般说来,re模块函数的第一个参数,即正则规则不再需要了,应为规则就包含在Pattern对象中了,编译选项也不再需要了,因为已经被编译过了。因此re模块中函数的这两个参数的位置,就被后面的参数取代了。
findall ,match , search 和finditer这几个函数的参数是一样的,除了少了规则和选项两个参数外,它们又加入了另外两个参数,它们是:查找开始位置和查找结束位置,也就是说,现在你可以指定查找的区间,除去你不感兴趣的区间。它们现在的参数形式是:
findall (targetString [, startPos [,endPos] ] )
finditer (targetString [, startPos [,endPos] ] )
match (targetString [, startPos [,endPos] ] )
search (targetString [, startPos [,endPos] ] )
这些函数的使用和re模块的同名函数使用完全一样。所以就不多介绍了。
除了和re模块的函数同样的函数外,Pattern对象还多了些东西,它们是:
flags 查询编译时的选项
pattern 查询编译时的规则
groupindex 规则里的组
这几个不是函数,而是一个值。它们提供你一些规则的信息。比如下面这个例子
>>>p=re.compile(r'(?P<word>b[a-z]+b)|(?P<num>bd+b)|(?P<id>b[a-z_]+w*b)', re.I )
>>>p.flags
2
>>>p.pattern
'(?P<word>\b[a-z]+\b)|(?P<num>\b\d+\b)|(?P<id>\b[a-z_]+\w*\b)'
>>>p.groupindex
{'num': 2,'word': 1, 'id': 3}
我们来分析一下这个例子:这个正则式是匹配单词、或数字、或一个由字母或’_’开头,后面接字母或数字的一个ID。我们给这三种情况的规则都包入了一个命名组,分别命名为’word’ , ‘num’和 ‘id’。我们规定大小写不敏感,所以使用了编译选项 ‘I’。
编译以后返回的对象为p,通过p.flag我们可以查看编译时的选项,不过它显示的不是’I’,而是一个数值2 。其实re.I是一个整数,2就是它的值。我们可以查看一下:
>>>re.I
2
>>>re.L
4
>>>re.M
8
…
每个选项都是一个数值。
通过p.pattern可以查看被编译的规则是什么。使用print的话会更好看一些
>>>print p.pattern
(?P<word>b[a-z]+b)|(?P<num>bd+b)|(?P<id>b[a-z_]+w*b)
看,和我们输入的一样。
接下来的p.groupindex则是一个字典,它包含了规则中的所有命名组。字典的key是名字,values是组的序号。由于字典是以名字作为key,所以一个无命名的组不会出现在这里。
3.2 组与Match对象
组与Match对象是Python正则式的重点。只有掌握了组和Match对象的使用,才算是真正学会了Python正则式。
3.2.1 组的名字与序号
正则式中的每个组都有一个序号,它是按定义时从左到右的顺序从1开始编号的。其实,re的正则式还有一个0号组,它就是整个正则式本身。
我们来看个例子
>>>p=re.compile( r’(?P<name>[a-z]+)s+(?P<age>d+)s+(?P<tel>d+).*’ , re.I )
>>>p.groupindex
{'age': 2,'tel': 3, 'name': 1}
>>>s=’Tom 24 88888888 <=’
>>>m=p.search(s)
>>>m.groups() # 看看匹配的各组的情况
('Tom','24', '8888888')
>>>m.group(‘name’)# 使用组名获取匹配的字符串
‘Tom’
>>>m.group( 1 ) # 使用组序号获取匹配的字符串,同使用组名的效果一样
>>>m.group(0) # 0 组里面是什么呢?
'Tom 2488888888 <='
原来0组就是整个正则式,包括没有被包围到组里面的内容。当获取0组的时候,你可以不写这个参数。m.group(0)和m.group()的效果是一样的:
>>>m.group()
'Tom 2488888888 <='
接下来看看更多的Match对象的方法,看看我们能做些什么。
3.2.2 Match对象的方法
group([index|id])获取匹配的组,缺省返回组0,也就是全部值
groups() 返回全部的组
groupdict() 返回以组名为key,匹配的内容为values的字典
接上例:
>>>m.groupindex()
{'age':'24', 'tel': '88888888', 'name': 'Tom'}
start([group] ) 获取匹配的组的开始位置
end( [group]) 获取匹配的组的结束位置
span([group] ) 获取匹配的组的(开始,结束)位置
expand(template ) 根据一个模版用找到的内容替换模版里的相应位置
这个功能比较有趣,它根据一个模版来用匹配到的内容替换模版中的相应位置,组成一个新的字符串返回。它使用g<index|name>或 index 来指示一个组。
接上例
>>>m.expand(r'name is g<1> , age is g<age> , tel is 3')
'name is Tom, age is 24 , tel is 88888888'
除了以上这些函数外,Match对象还有些属性
pos 搜索开始的位置参数
endpos 搜索结束的位置参数
这两个是使用findall或match等函数时,传入的参数。在上面这个例子里,我们没有指定开始和结束位置,那么缺省的开始位置就是0,结束位置就是最后。
>>>m.pos
0
>>>m.endpos
19
lastindex 最后匹配的组的序号
>>>m.lastindex
3
lastgroup 最后匹配的组名
>>>m.lastgroup
'tel'
re 产生这个匹配的Pattern对象,可以认为是个逆引用
>>>m.re.pattern
'(?P<name>[a-z]+)\s+(?P<age>\d+)\s+(?P<tel>\d+).*'
得到了产生这个匹配的规则
string 匹配的目标字符串
>>>m.string
'Tom 2488888888 <='
正则表达式补充
2017年5月18日
21:37
<num> | 引用分组num匹配到的字符串 |
(?P<name>) | 分组起别名 |
(?P=name) | 引用别名为name分组匹配到的字符串 |
需求:匹配出<html>hh</html>
#coding=utf-8
import re
# 能够完成对正确的字符串的匹配
ret = re.match("<[a-zA-Z]*>w*</[a-zA-Z]*>","<html>hh</html>")
ret.group()
# 如果遇到非正常的html格式字符串,匹配出错
ret = re.match("<[a-zA-Z]*>w*</[a-zA-Z]*>","<html>hh</htmlbalabala>")
ret.group()
# 正确的理解思路:如果在第一对<>中是什么,按理说在后面的那对<>中就应该是什么
# 通过引用分组中匹配到的数据即可,但是要注意是元字符串,即类似 r""这种格式
ret = re.match(r"<([a-zA-Z]*)>w*</1>","<html>hh</html>")
ret.group()
# 因为2对<>中的数据不一致,所以没有匹配出来
ret = re.match(r"<([a-zA-Z]*)>w*</1>","<html>hh</htmlbalabala>")
ret.group()
number
需求:匹配出<html><h1>www.tsinghua.edu.cn</h1></html>
#coding=utf-8
import re
ret =re.match(r"<(w*)><(w*)>.*</2></1>","<html><h1>www.tsinghua.edu.cn</h1></html>")
ret.group()
ret =re.match(r"<(w*)><(w*)>.*</2></1>","<html><h1>www.tsinghua.edu.cn</h2></html>")
ret.group()
(?P) (?P=name)
需求:匹配出<html><h1>www.qq.com</h1></html>
#coding=utf-8
import re
ret =re.match(r"<(?P<name1>w*)><(?P<name2>w*)>.*</(?P=name2)></(?P=name1)>","<html><h1>www.qq.com</h1></html>")
ret.group()
ret =re.match(r"<(?P<name1>w*)><(?P<name2>w*)>.*</(?P=name2)></(?P=name1)>","<html><h1>www.qq.com</h2></html>")
ret.group()
Python3 多线程
2017年4月13日
11:20
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
· 使用线程可以把占据长时间的程序中的任务放到后台去处理。
· 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
· 程序的运行速度可能加快
· 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
· 线程可以被抢占(中断)。
· 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
线程可以分为:
· 内核线程:由操作系统内核创建和撤销。
· 用户线程:不需要内核支持而在用户程序中实现的线程。
Python3 线程中常用的两个模块为:
· _thread
· threading(推荐使用)
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用"thread"模块。为了兼容性,Python3 将 thread 重命名为"_thread"。
开始学习Python线程
Python中使用线程有两种方式:函数或者用类来包装线程对象。
函数式:调用 _thread 模块中的start_new_thread()函数来产生新线程。语法如下:
_thread.start_new_thread ( function, args[, kwargs] )
参数说明:
· function - 线程函数。
· args - 传递给线程函数的参数,他必须是个tuple类型。
· kwargs - 可选参数。
实例:
#!/usr/bin/python3
import_thread
importtime
# 为线程定义一个函数
def print_time( threadName, delay):
count =0
while count < 5:
time.sleep(delay)
count +=1
print("%s:%s" % ( threadName, time.ctime(time.time()) ))
# 创建两个线程
try:
_thread.start_new_thread( print_time, ("Thread-1",2, ) )
_thread.start_new_thread( print_time, ("Thread-2",4, ) )
except:
print("Error: 无法启动线程")
while 1:
pass
执行以上程序输出结果如下:
Thread-1: WedApr 6 11:36:31 2016
Thread-1: WedApr 6 11:36:33 2016
Thread-2: WedApr 6 11:36:33 2016
Thread-1: WedApr 6 11:36:35 2016
Thread-1: WedApr 6 11:36:37 2016
Thread-2: WedApr 6 11:36:37 2016
Thread-1: WedApr 6 11:36:39 2016
Thread-2: WedApr 6 11:36:41 2016
Thread-2: WedApr 6 11:36:45 2016
Thread-2: WedApr 6 11:36:49 2016
执行以上程后可以按下 ctrl-c to 退出。
线程模块
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
· threading.currentThread(): 返回当前的线程变量。
· threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
· threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
· run(): 用以表示线程活动的方法。
· start():启动线程活动。
· join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
· isAlive(): 返回线程是否活动的。
· getName(): 返回线程名。
· setName(): 设置线程名。
使用 threading 模块创建线程
我们可以通过直接从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法:
#!/usr/bin/python3
importthreading
importtime
exitFlag =0
classmyThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print("开始线程:"+ self.name)
print_time(self.name, self.counter, 5)
print("退出线程:"+ self.name)
defprint_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print("%s:%s" % (threadName, time.ctime(time.time())))
counter -=1
# 创建新线程
thread1 = myThread(1,"Thread-1",1)
thread2 = myThread(2,"Thread-2",2)
# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("退出主线程")
以上程序执行结果如下;
开始线程:Thread-1
开始线程:Thread-2
Thread-1: WedApr 6 11:46:46 2016
Thread-1: WedApr 6 11:46:47 2016
Thread-2: WedApr 6 11:46:47 2016
Thread-1: WedApr 6 11:46:48 2016
Thread-1: WedApr 6 11:46:49 2016
Thread-2: WedApr 6 11:46:49 2016
Thread-1: WedApr 6 11:46:50 2016
退出线程:Thread-1
Thread-2: WedApr 6 11:46:51 2016
Thread-2: WedApr 6 11:46:53 2016
Thread-2: WedApr 6 11:46:55 2016
退出线程:Thread-2
退出主线程
线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下:
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。
那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
实例:
#!/usr/bin/python3
importthreading
importtime
classmyThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print("开启线程:" + self.name)
#获取锁,用于线程同步
threadLock.acquire()
print_time(self.name, self.counter, 3)
#释放锁,开启下一个线程
threadLock.release()
defprint_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print("%s:%s" % (threadName, time.ctime(time.time())))
counter -=1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1,"Thread-1",1)
thread2 = myThread(2,"Thread-2",2)
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()
print ("退出主线程")
执行以上程序,输出结果为:
开启线程: Thread-1
开启线程: Thread-2
Thread-1: WedApr 6 11:52:57 2016
Thread-1: WedApr 6 11:52:58 2016
Thread-1: WedApr 6 11:52:59 2016
Thread-2: WedApr 6 11:53:01 2016
Thread-2: WedApr 6 11:53:03 2016
Thread-2: WedApr 6 11:53:05 2016
退出主线程
线程优先级队列( Queue)
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。
这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。
Queue 模块中的常用方法:
· Queue.qsize() 返回队列的大小
· Queue.empty() 如果队列为空,返回True,反之False
· Queue.full() 如果队列满了,返回True,反之False
· Queue.full 与 maxsize 大小对应
· Queue.get([block[, timeout]])获取队列,timeout等待时间
· Queue.get_nowait() 相当Queue.get(False)
· Queue.put(item) 写入队列,timeout等待时间
· Queue.put_nowait(item) 相当Queue.put(item, False)
· Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
· Queue.join() 实际上意味着等到队列为空,再执行别的操作
实例:
#!/usr/bin/python3
importqueue
importthreading
importtime
exitFlag =0
classmyThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print("开启线程:"+ self.name)
process_data(self.name, self.q)
print("退出线程:"+ self.name)
defprocess_data(threadName, q):
whilenot exitFlag:
queueLock.acquire()
ifnot workQueue.empty():
data = q.get()
queueLock.release()
print("%sprocessing %s" % (threadName, data))
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1
# 创建新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID +=1
# 填充队列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待队列清空
while notworkQueue.empty():
pass
# 通知线程是时候退出
exitFlag = 1
# 等待所有线程完成
for t in threads:
t.join()
print ("退出主线程")
以上程序执行结果:
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程
现实生活中多任务
2017年5月18日
20:21
现实生活中
有很多的场景中的事情是同时进行的,比如开车的时候手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的;试想,如果把唱歌和跳舞这2件事情分开依次完成的话,估计就没有那么好的效果了(想一下场景:先唱歌,然后在跳舞,O(∩_∩)O哈哈~)
程序中
如下程序,来模拟“唱歌跳舞”这件事情
#coding=utf-8
from time import sleep
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1)
if __name__ == '__main__':
sing() #唱歌
dance() #跳舞
!!!注意
· 很显然刚刚的程序并没有完成唱歌和跳舞同时进行的要求
· 如果想要实现“唱歌跳舞”同时进行,那么就需要一个新的方法,叫做:多线程
多任务的概念
2017年5月18日
20:22
多任务的概念
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
2017年5月18日
20:22
多线程-thread
<1> 实现多任务
#coding=utf-8
import thread
from time importsleep,ctime
def sing():
for i in range(3):
print "正在唱歌...%d"%i
sleep(1)
def dance():
for i in range(3):
print "正在跳舞...%d"%i
sleep(1)
if __name__ == '__main__':
print '---开始---:', ctime()
# 第一个参数:创建的新线程要执行的代码
# 第二个参数给新线程执行sing函数时传递的参数,即使没有参数也要传递空元组
thread.start_new_thread(sing, ())
thread.start_new_thread(dance,())
sleep(5)
print '---结束---:', ctime()
说明:
· 直接调用sing、dance函数,实现的效果是先唱完歌曲之后,在单独进行跳舞
· 通过thread.start_new_thread间接调用sing、dance函数,实现了唱歌和跳舞同时进行的完美结合
<2>过程解析
单任务:
多任务:
<3>试一试
· 现需要把“吹风”这个功能,也要添加到上面程序中同时进行,该怎样办?并编程实现
· 把sleep(5)这行代码屏蔽,再次运行程序,看执行结果有什么不同
多线程-threading
2017年5月18日
20:22
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
<1>使用threading模块
单线程执行
#coding=utf-8
import time
def saySorry():
print "亲爱的,我错了,我能吃饭了吗?"
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
saySorry()
运行结果:
多线程执行
#coding=utf-8
import threading
import time
def saySorry():
print "亲爱的,我错了,我能吃饭了吗?"
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t =threading.Thread(target=saySorry)
t.start()
运行结果:
说明:可以明显看出使用了多线程并发的操作,花费时间要短很多
<2>验证上一小节的第2个问题
#coding=utf-8
import threading
from time import sleep,ctime
def sing():
for i in range(3):
print "正在唱歌...%d"%i
sleep(1)
def dance():
for i in range(3):
print "正在跳舞...%d"%i
sleep(1)
if __name__ == '__main__':
print '---开始---:', ctime()
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
#sleep(5) # 屏蔽此行代码,试试看,程序是否会立马结束?
print '---结束---:', ctime()
<2>线程的执行
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。让我们开始第一个例子:
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm "+self.name+' @ '+str(i)
print msg
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
执行结果:(运行的结果可能不一样,但是大体是一致的)
I'm Thread-1 @ 0
I'm Thread-2 @ 0
I'm Thread-5 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-3 @ 1
I'm Thread-4 @ 1
I'm Thread-5 @ 1
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-4 @ 2
I'm Thread-5 @ 2
I'm Thread-2 @ 2
I'm Thread-1 @ 2
I'm Thread-3 @ 2
说明:
从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。此外需要注意的是:
1. 每个线程一定会有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
2. 当线程的run()方法结束时该线程完成。
3. 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
上面的例子只是简单的演示了创建了线程、主动挂起以及退出线程。下一节,将讨论用互斥锁进行线程同步。
2017年5月18日
20:22
使用互斥锁同步线程
问题的提出上一节的例子中,每个线程互相独立,相互之间没有任何关系。现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1。很容易写出这样的代码:
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
def run(self):
global num
num = num+1
time.sleep(0.5) #用来模拟适当的数据处理
msg = self.name+' set num to '+str(num)
print msg
num = 0
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
啊???为什么结果会这样?
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
互斥锁同步
上面的例子引出了多线程编程的最常见问题:数据共享
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([timeout])
#释放
mutex.release()
其中,锁定方法acquire可以有一个超时时间的可选参数timeout。如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。
使用互斥锁实现上面的例子的代码如下:
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
def run(self):
global num
if mutex.acquire(1):
num = num+1
time.sleep(0.5)
msg = self.name+' set num to '+str(num)
print msg
mutex.release()
num = 0
mutex = threading.Lock()
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
运行结果:
可以看到,加入互斥锁后,运行结果与预期相符。
总结
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“同步阻塞”
直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
锁的好处:
· 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
· 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
· 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁
2017年5月18日
20:22
死锁
现实社会中,男女双方都在等待对方先道歉
如果双方都这样固执的等待对方先开口,弄不好,就分搜了
死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子
#coding=utf-8
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name+'----do1---up----')
time.sleep(1)
if mutexB.acquire():
print(self.name+'----do1---down----')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
if mutexB.acquire():
print(self.name+'----do2---up----')
time.sleep(1)
if mutexA.acquire():
print(self.name+'----do2---down----')
mutexA.release()
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
线程同步之可重入锁
2017年5月18日
21:13
线程同步之可重入锁
可重入锁
更简单的死锁情况是一个线程“迭代”请求同一个资源,直接就会造成死锁:
import threading
import time
class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(1):
num = num+1
msg = self.name+' set num to'+str(num)
print msg
mutex.acquire()
mutex.release()
mutex.release()
num = 0
mutex = threading.Lock()
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
import threading
import time
class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(1):
num = num+1
msg = self.name+' set num to'+str(num)
print msg
mutex.acquire()
mutex.release()
mutex.release()
num = 0
mutex = threading.RLock()
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
运行结果:
Thread-1 set num to 1
Thread-3 set num to 2
Thread-2 set num to 3
Thread-5 set num to 4
Thread-4 set num to 5
线程同步之条件变量
2017年5月18日
21:13
线程同步之条件变量
互斥锁是最简单的线程同步机制,Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。
可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition对象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用notify方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。
Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。
除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire内部锁。由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有线程永远处于沉默状态。
演示条件变量同步的经典问题是生产者与消费者问题:假设有一群生产者(Producer)和一群消费者(Consumer)通过一个市场来交互产品。生产者的”策略“是如果市场上剩余的产品少于1000个,那么就生产100个产品放到市场上;而消费者的”策略“是如果市场上剩余产品的数量多余100个,那么就消费3个产品。用Condition解决生产者与消费者问题的代码如下:
import threading
import time
class Producer(threading.Thread):
def run(self):
globalcount
whileTrue:
ifcon.acquire():
ifcount > 1000:
con.wait()
else:
count = count+100
msg = self.name+' produce 100, count=' + str(count)
print msg
con.notify()
con.release()
time.sleep(1)
class Consumer(threading.Thread):
def run(self):
globalcount
whileTrue:
ifcon.acquire():
ifcount < 100:
con.wait()
else:
count = count-3
msg = self.name+' consume 3, count='+str(count)
print msg
con.notify()
con.release()
time.sleep(1)
count = 500
con = threading.Condition()
def test():
fori in range(2):
p = Producer()
p.start()
fori in range(5):
c = Consumer()
c.start()
if __name__ == '__main__':
test()
线程同步之队列
2017年5月18日
21:13
让我们考虑更复杂的一种场景:产品是各不相同的。这时只记录一个数量就不够了,还需要记录每个产品的细节。很容易想到需要用一个容器将这些产品记录下来。
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
用FIFO队列实现上述生产者与消费者问题的代码如下:
#encoding=utf-8
import threading
import time
from Queue importQueue
class Producer(threading.Thread):
def run(self):
globalqueue
count = 0
whileTrue:
fori in range(100):
ifqueue.qsize() > 1000:
pass
else:
count = count +1
msg = '生成产品'+str(count)
queue.put(msg)
print msg
time.sleep(1)
class Consumer(threading.Thread):
def run(self):
globalqueue
whileTrue:
fori in range(3):
ifqueue.qsize() < 100:
pass
else:
msg = self.name + '消费了 '+queue.get()
print msg
time.sleep(1)
queue = Queue()
def test():
fori in range(500):
queue.put('初始产品'+str(i))
fori in range(2):
p = Producer()
p.start()
fori in range(5):
c = Consumer()
c.start()
if __name__ == '__main__':
test()
线程间通信
2017年5月18日
21:13
线程间通信
很多时候,线程之间会有互相通信的需要。常见的情形是次要线程为主要线程执行特定的任务,在执行过程中需要不断报告执行的进度情况。前面的条件变量同步已经涉及到了线程间的通信(threading.Condition的notify方法)。更通用的方式是使用threading.Event对象。 threading.Event可以使一个线程等待其他线程的通知。其内置了一个标志,初始值为False。线程通过wait()方法进入等待状态,直到另一个线程调用set()方法将内置标志设置为True时,Event通知所有等待状态的线程恢复运行。还可以通过isSet()方法查询Envent对象内置状态的当前值。
举例如下:
import threading
import random
import time
class MyThread(threading.Thread):
def __init__(self,threadName,event):
threading.Thread.__init__(self,name=threadName)
self.threadEvent = event
def run(self):
print"%s is ready" % self.name
self.threadEvent.wait()
print"%s run!" % self.name
sinal =threading.Event()
for i inrange(10):
t = MyThread(str(i),sinal)
t.start()
time.sleep(1)
sinal.set()
线程的合并和后台线程
2017年5月18日
21:16
线程的合并和后台线程
线程的合并
python的Thread类中还提供了join()方法,使得一个线程可以等待另一个线程执行结束后再继续运行。这个方法还可以设定一个timeout参数,避免无休止的等待。因为两个线程顺序完成,看起来象一个线程,所以称为线程的合并。一个例子:
import threading
import random
import time
class MyThread(threading.Thread):
def run(self):
wait_time=random.randrange(1,10)
print "%s will wait %d seconds" % (self.name, wait_time)
time.sleep(wait_time)
print "%s finished!" % self.name
if __name__=="__main__":
threads = []
for i in range(5):
t = MyThread()
t.start()
threads.append(t)
print 'main thread is waitting for exit...'
for t in threads:
t.join(1)
print 'main thread finished!'
运行结果:
Thread-1 willwait 3 seconds
Thread-2 will wait 4 seconds
Thread-3 will wait 1 seconds
Thread-4 will wait 5 seconds
Thread-5 will wait 3 seconds
main thread is waitting for exit...
Thread-3 finished!
Thread-1 finished!
Thread-5 finished!
main thread finished!
Thread-2 finished!
Thread-4 finished!
对于sleep时间过长的线程(这里是2和4),将不被等待。
后台线程
默认情况下,主线程在退出时会等待所有子线程的结束。如果希望主线程不等待子线程,而是在退出时自动结束所有的子线程,就需要设置子线程为后台线程(daemon)。方法是通过调用线程类的setDaemon()方法。如下:
import threading
import random
import time
class MyThread(threading.Thread):
def run(self):
wait_time=random.randrange(1,10)
print "%s will wait %dseconds" % (self.name, wait_time)
time.sleep(wait_time)
print "%s finished!" %self.name
if __name__=="__main__":
print 'main thread is waitting forexit...'
for i in range(5):
t = MyThread()
t.setDaemon(True)
t.start()
print 'main thread finished!'
运行结果:
main thread iswaitting for exit...
Thread-1 will wait 3 seconds
Thread-2 will wait 3 seconds
Thread-3 will wait 4 seconds
Thread-4 will wait 7 seconds
Thread-5 will wait 7 seconds
main thread finished!
可以看出,主线程没有等待子线程的执行,而直接退出。
小结join()方法使得线程可以等待另一个线程的运行,而setDaemon()方法使得线程在结束时不等待子线程。join和setDaemon都可以改变线程之间的运行顺序。
ThreadLocal
2017年5月18日
21:16
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。
如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?
global_dict = {}
def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()
def do_task_1():
# 不传入std,而是根据当前线程查找:
std =global_dict[threading.current_thread()]
...
def do_task_2():
# 任何函数都可以查找出当前线程的std变量:
std =global_dict[threading.current_thread()]
...
这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。
有没有更简单的方式?
ThreadLocal应运而生,不用查找dict,ThreadLocal帮你自动做这件事:
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std,threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
执行结果:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
小结
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
线程相关函数
2017年5月22日
8:28
Event
2017年3月25日
17:39
condtion
2017年3月25日
17:41
semapore
2017年3月25日
17:41
Timer
2017年3月25日
17:42
TLS
2017年3月25日
17:42
多进程概述
2017年5月23日
8:54
老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?
要知其然,更要知其所以然。所以有了下面的深入研究:
首先强调背景:
1. GIL是什么?
GIL的全称是Global InterpreterLock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。
2. 每个CPU在同一时间只能执行一个线程
在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
在Python多线程下,每个线程的执行方式:
1. 获取GIL
2. 执行代码直到sleep或者是python虚拟机将其挂起。
3. 释放GIL
可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器,专门作用于GIL,每次释放后归零,这个计数可以通过sys.setcheckinterval 来调整),进行释放。
而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。
那么是不是python的多线程就完全没用了呢?
在这里我们进行分类讨论:
1. CPU密集型代码(各种循环处理、计数等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
2. IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。
请注意:多核多线程比单核多线程更差,原因是单核下的多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。
回到最开始的问题:经常我们会听到老手说:“python下想要充分利用多核CPU,就用多进程”,原因是什么呢?
原因是:每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率
>
Python多进程
2017年5月18日
21:01
进程的创建-fork
1. 多进程
通过前面的学习已经知道了,可以通过多线程完成多任务的要求,其实只要说到多任务,除了线程之外还有一个能够完成多任务的功能:多进程
2. fork( )
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
import os
# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()
if pid == 0:
print('哈哈1')
else:
print('哈哈2')
说明:
· 程序执行到os.fork()时,操作系统会创建一个新的进程(子进程),然后复制父进程的所有信息到子进程中
· 然后父进程和子进程都会从fork()函数中得到一个返回值,其进程中这个值一定是0,而父进程中是子进程的 id号
在Unix/Linux操作系统中,提供了一个fork()系统函数,它非常特殊。
普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。
这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
多进程修改全局变量
2017年5月18日
21:02
在学习多线程的时候,提到过,多个线程都是可以访问全局变量的,有时会出现多个线程同时访问,而差生数据错误的情况
本小节,来探究一下多进程中,是否也存在这个问题
#coding=utf-8
import os
import time
num = 0
# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()
if pid == 0:
num+=1
print('哈哈1---num=%d'%num)
else:
time.sleep(1)
num+=1
print('哈哈2---num=%d'%num)
运行结果:
总结:
· 多进程中,每个进程中所有数据(包括全局变量)都各有拥有一份,互不影响
多次fork问题
2017年5月18日
21:02
如果在一个程序,有2次的fork函数调用,是否就会有3个进程呢?
#coding=utf-8
import os
import time
# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()
if pid == 0:
print('哈哈1')
else:
print('哈哈2')
pid = os.fork()
if pid == 0:
print('哈哈3')
else:
print('哈哈4')
time.sleep(1)
运行结果:
获取进程号
2017年5月18日
21:02
Ubuntu中获取PID
操作系统为了更好的有序的管理每个运行的进程,需要对每个进行进行分配一个号码,这个号码成为进程号(PID)
在类UNIX系统中,查看当前系统中有哪些进程正常运行,ps-aux
Python中获取PID的方法
获取当前进程的pid的方法为:getpid()
获取父进程的pid的方法为:getpid()
#coding=utf-8
import os
import time
# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()
if pid == 0:
print('哈哈1---pid = %d---ppid = %d----'%(os.getpid(), os.getppid()))
else:
print('哈哈2---pid = %d---'%os.getpid())
time.sleep(1)
#coding=utf-8
import os
import time
# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()
if pid == 0:
print('哈哈1---pid = %d---'%os.getpid())
else:
print('哈哈2---pid = %d---'%os.getpid())
pid = os.fork()
if pid == 0:
print('哈哈3---pid = %d---'%os.getpid())
else:
print('哈哈4---pid = %d---'%os.getpid())
time.sleep(1)
subprocess标准子进程
2017年5月22日
23:39
subprocess以及常用的封装函数
当我们运行python的时候,我们都是在创建并运行一个进程。
在Python中,我们通过标准库中的subprocess包来fork一个子进程,并运行一个外部的程序。
subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。
另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。
使用subprocess包中的函数创建子进程的时候,要注意:
1) 在创建子进程之后,父进程是否暂停,并等待子进程运行。
2) 函数返回什么
3) 当returncode不为0时,父进程如何处理。
subprocess.call()
父进程等待子进程完成
返回退出信息(returncode,相当于exitcode)
subprocess.check_call()
父进程等待子进程完成
返回0
检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,
该对象包含有returncode属性,可用try…except…来检查。
subprocess.check_output()
父进程等待子进程完成
返回子进程向标准输出的输出结果
检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,
该对象包含有returncode属性和output属性,output属性为标准输出的输出结果,可用try…except…来检查。
这三个函数的使用方法相类似,以subprocess.call()来说明:
import subprocess
rc =subprocess.call(["ls","-l"])
将程序名(ls)和所带的参数(-l)一起放在一个表中传递给subprocess.call()
可以通过一个shell来解释一整个字符串:
[python] view plain copy
import subprocess
import subprocess
child =subprocess.Popen(["ping","-c","5","www.google.com"])
print("parent process")
[python] view plain copy
out = subprocess.call("ls-l", shell=True)
out = subprocess.call("cd ..",shell=True)
使用了shell=True这个参数,这个时候,我们使用一整个字符串,而不是一个表来运行子进程。
Python将先运行一个shell,再用这个shell来解释这整个字符串。
shell命令中有一些是shell的内建命令,这些命令必须通过shell运行,$cd。shell=True允许我们运行这样一些命令。
Popen()
实际上,我们上面的三个函数都是基于Popen()的封装(wrapper)。这些封装的目的在于让我们容易使用子进程。
当我们想要更个性化我们的需求的时候,就要转向Popen类,该类生成的对象用来代表子进程。
与上面的封装不同,Popen对象创建后,主程序不会自动等待子进程完成。
我们必须调用对象的wait()方法,父进程才会等待 (也就是阻塞block):
从运行结果中看到,父进程在开启子进程之后并没有等待child的完成,而是直接运行print。
对比等待的情况:
[python] view plain copy
import subprocess
child =subprocess.Popen(["ping","-c","5","www.google.com"])
child.wait()
print("parent process")
此外,你还可以在父进程中对子进程进行其它操作,比如我们上面例子中的child对象:
child.poll() # 检查子进程状态
child.kill() # 终止子进程
child.send_signal() # 向子进程发送信号
child.terminate() # 终止子进程
子进程的PID存储在child.pid
子进程的文本流控制
子进程的标准输入,标准输出和标准错误也可以通过如下属性表示:
child.stdin
child.stdout
child.stderr
我们可以在Popen()建立子进程的时候改变标准输入、标准输出和标准错误,
并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe):
[python] view plain copy
import subprocess
child1 =subprocess.Popen(["ls","-l"], stdout=subprocess.PIPE)
child2 =subprocess.Popen(["wc"],stdin=child1.stdout,stdout=subprocess.PIPE)
out = child2.communicate()
print(out)
subprocess.PIPE实际上为文本流提供一个缓存区。
child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。
child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
要注意的是,communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成。
我们还可以利用communicate()方法来使用PIPE给子进程输入:
[python] view plain copy
importsubprocess
child =subprocess.Popen(["cat"], stdin=subprocess.PIPE)
child.communicate("vamei")
我们启动子进程之后,cat会等待输入,直到我们用communicate()输入”vamei”。
通过使用subprocess包,我们可以运行外部程序。这极大的拓展了Python的功能。
如果你已经了解了操作系统的某些应用,你可以从Python中直接调用该应用(而不是完全依赖Python),并将应用的结果输出给Python,并让Python继续处理。
shell的功能(比如利用文本流连接各个应用),就可以在Python中实现。
Python-signal信号
2017年5月22日
23:28
signal包负责在Python程序内部处理信号,典型的操作包括预设信号处理函数,暂停并等待信号,以及定时发出SIGALRM等。
要注意,signal包主要是针对UNIX平台(比如Linux,MAC OS),而Windows内核中由于对信号机制的支持不充分,
所以在Windows上的Python不能发挥信号系统的功能。
定义信号名
signal包定义了各个信号名及其对应的整数,比如
[python] view plain copy
import signal
print signal.SIGALRM
print signal.SIGCONT
Python所用的信号名和Linux一致。可以通过
$man 7 signal
查询
预设信号处理函数
signal包的核心是使用signal.signal()函数来预设(register)信号处理函数,如下所示:
singnal.signal(signalnum, handler)
signalnum为某个信号,handler为该信号的处理函数。
我们在信号基础里提到,进程可以无视信号,可以采取默认操作,还可以自定义操作。
当handler为signal.SIG_IGN时,信号被无视(ignore)。
当handler为singal.SIG_DFL,进程采取默认操作(default)。
当handler为一个函数名时,进程采取函数中定义的操作。
在主程序中,首先使用signal.signal()函数来预设信号处理函数。
然后我们执行signal.pause()来让该进程暂停以等待信号,以等待信号。
当信号SIGUSR1被传递给该进程时,进程从暂停中恢复,并根据预设,执行SIGTSTP的信号处理函数myHandler()。
myHandler的两个参数一个用来识别信号(signum),另一个用来获得信号发生时,进程栈的状况(stackframe)。
这两个参数都是由signal.singnal()函数来传递的。
importsubprocess
importsignal
defsigint_handler(signum,frame):#SIGINT信号处理函数
print("InsignalSIGINThandler")
signal.signal(signal.SIGINT,sigint_handler)#设置SIGINT信号处理函数
pingP=subprocess.Popen(args="pingwww.sina.com.cn",shell=True)
pingP.wait()#等待子进程完成,后面在这里会被中断
print(pingP.pid)
print(pingP.returncode)
上面的程序可以保存在一个文件中(比如test.py)。我们使用如下方法运行:
$python test.py
以便让进程运行。当程序运行到signal.pause()的时候,进程暂停并等待信号。
此时,通过按下CTRL+Z向该进程发送SIGTSTP信号。
可以看到,进程执行了myHandle()函数,随后返回主程序,继续执行。
当然,也可以用$ps查询processID, 再使用$kill来发出信号。
进程并不一定要使用signal.pause()暂停以等待信号,它也可以在进行工作中接受信号,
比如将上面的signal.pause()改为一个需要长时间工作的循环。
可以根据自己的需要更改myHandler()中的操作,以针对不同的信号实现个性化的处理。
import signal
# Define signalhandler function
def myHandler(signum,frame):
print("Now,it's time")
# Registersignal.SIGTSTP's handler
signal.signal(signal.SIGALRM,myHandler)
signal,alarm(5)
while True:
print("End")
这里用了一个无限循环以便让进程持续运行。
在signal.alarm()执行5秒之后,进程将向自己发出SIGALRM信号,随后,信号处理函数myHandler开始执行。
发送信号
signal包的核心是设置信号处理函数。
除了signal.alarm()向自身发送信号之外,并没有其他发送信号的功能。
但在os包中,有类似于linux的kill命令的函数,分别为
os.kill(pid, sid)
os.killpg(pgid, sid)
分别向进程和进程组(见Linux进程关系)发送信号。sid为信号所对应的整数或者singal.SIG*。
实际上signal, pause,kill和alarm都是Linux应用编程中常见的C库函数,在这里,我们只不过是用Python语言来实现了一下。
实际上,Python 的解释器是使用C语言来编写的,所以有此相似性也并不意外。
此外,在Python 3.4中,signal包被增强,信号阻塞等功能被加入到该包中。
multiprocessing
2017年5月18日
21:02
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
#coding=utf-8
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('子进程运行中,name= %s ,pid=%d...' % (name, os.getpid()))
if __name__=='__main__':
print('父进程 %d.' % os.getpid())
p = Process(target=run_proc,args=('test',))
print('子进程将要执行')
p.start()
p.join()
print('子进程已结束')
运行结果:
说明
· 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。
· join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
multiprocessing 强化
2017年5月22日
23:37
我们可以使用subprocess包来创建子进程,但这个包有两个很大的局限性:
1) 我们总是让subprocess运行外部的程序,而不是运行一个Python脚本内部编写的函数。
2) 进程间只通过管道进行文本交流。
以上限制了我们将subprocess包应用到更广泛的多进程任务。
这样的比较实际是不公平的,因为subprocessing本身就是设计成为一个shell,而不是一个多进程管理包。
一 threading和multiprocessing
multiprocessing包是Python中的多进程管理包。
与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。
该进程可以运行在Python程序内部编写的函数。
该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。
此外multiprocessing包中也有Lock/Event/Semaphore/Condition类(这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。
所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
但在使用这些共享API的时候,我们要注意以下几点:
1)在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。
所以,有必要对每个Process对象调用join()方法(实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。
2)multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。
应优先考虑Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式(因为它们占据的不是用户进程的资源)。
3)多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。
在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。
但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。Process.PID中保存有PID,如果进程还没有start(),则PID为None。
我们可以从下面的程序中看到Thread对象和Process对象在使用上的相似性与结果上的不同。各个线程和进程都做一件事:打印PID。
但问题是,所有的任务在打印的时候都会向同一个标准输出(stdout)输出。这样输出的字符会混合在一起,无法阅读。
使用Lock同步,在一个任务输出完成之后,再允许另一个任务输出,可以避免多个任务同时向终端输出。
[python] view plain copy
# Similarity and difference of multithread vs. multi process
import os
import threading
import multiprocessing
# worker function
def worker(sign, lock):
lock.acquire()
print(sign, os.getpid())
lock.release()
# Main
print('Main:',os.getpid())
# Multi-thread
record = []
lock = threading.Lock()
for i in range(5):
thread = threading.Thread(target=worker,args=('thread',lock))
thread.start()
record.append(thread)
for thread in record:
thread.join()
# Multi-process
record = []
lock = multiprocessing.Lock()
for i in range(5):
process =multiprocessing.Process(target=worker,args=('process',lock))
process.start()
record.append(process)
for process in record:
process.join()
所有Thread的PID都与主程序相同,而每个Process都有一个不同的PID。
二 Pipe和Queue
管道PIPE和消息队列messagequeue,multiprocessing包中有Pipe类和Queue类来分别支持这两种IPC机制。Pipe和Queue可以用来传送常见的对象。
1) Pipe可以是单向(half-duplex),也可以是双向(duplex)。
我们通过mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。
一个进程从PIPE一端输入对象,然后被PIPE另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。
下面的程序展示了Pipe的使用:
[python] view plain copy
# Multiprocessing with Pipe
import multiprocessing as mul
def proc1(pipe):
pipe.send('hello')
print('proc1 rec:',pipe.recv())
def proc2(pipe):
print('proc2 rec:',pipe.recv())
pipe.send('hello, too')
# Build a pipe
pipe = mul.Pipe()
# Pass an end of the pipe to process1
p1 = mul.Process(target=proc1, args=(pipe[0],))
# Pass the other end of the pipe toprocess 2
p2 = mul.Process(target=proc2, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
这里的Pipe是双向的。
Pipe对象建立的时候,返回一个含有两个元素的表,每个元素代表Pipe的一端(Connection对象)。
我们对Pipe的某一端调用send()方法来传送对象,在另一端使用recv()来接收。
2) Queue与Pipe相类似,都是先进先出的结构。但Queue允许多个进程放入,多个进程从队列取出对象。
Queue使用mutiprocessing.Queue(maxsize)创建,maxsize表示队列中可以存放对象的最大数量。
下面的程序展示了Queue的使用:
[python] view plain copy
import os
import multiprocessing
import time
#==================
# input worker
def inputQ(queue):
info = str(os.getpid()) + '(put):' + str(time.time())
queue.put(info)
# output worker
def outputQ(queue,lock):
info = queue.get()
lock.acquire()
print (str(os.getpid()) + '(get):' + info)
lock.release()
#===================
# Main
record1 = [] # store input processes
record2 = [] # store output processes
lock = multiprocessing.Lock() # Toprevent messy print
queue = multiprocessing.Queue(3)
# input processes
for i in range(10):
process = multiprocessing.Process(target=inputQ,args=(queue,))
process.start()
record1.append(process)
# output processes
for i in range(10):
process = multiprocessing.Process(target=outputQ,args=(queue,lock))
process.start()
record2.append(process)
for p in record1:
p.join()
queue.close() # No more object will come, close thequeue
for p in record2:
p.join()
一些进程使用put()在Queue中放入字符串,这个字符串中包含PID和时间。
另一些进程从Queue中取出,并打印自己的PID以及get()的字符串。
三 进程池
进程池 (ProcessPool)可以创建多个进程。这些进程就像是随时待命的士兵,准备执行任务(程序)。一个进程池中可以容纳多个待命的士兵。
比如下面的程序:
[python] view plain copy
import multiprocessing as mul
def f(x):
return x**2
pool = mul.Pool(5)
rel = pool.map(f,[1,2,3,4,5,6,7,8,9,10])
print(rel)
我们创建了一个容许5个进程的进程池 (Process Pool) 。Pool运行的每个进程都执行f()函数。
我们利用map()方法,将f()函数作用到表的每个元素上。这与built-in的map()函数类似,只是这里用5个进程并行处理。
如果进程运行结束后,还有需要处理的元素,那么的进程会被用于重新运行f()函数。除了map()方法外,Pool还有下面的常用方法。
1)apply_async(func,args)从进程池中取出一个进程执行func,args为func的参数。
它将返回一个AsyncResult的对象,你可以对该对象调用get()方法以获得结果。
2)close()进程池不再创建新的进程
3)join()wait进程池中的全部进程。必须对Pool先调用close()方法才能join。
四 共享资源
多进程共享资源必然会带来进程间相互竞争。而这种竞争又会造成racecondition,我们的结果有可能被竞争的不确定性所影响。
但如果需要,我们依然可以通过共享内存和Manager对象这么做。
1 共享内存
根据共享内存(sharedmemory)的原理,这里给出用Python实现的例子:
[python] view plain copy
import multiprocessing
def f(n, a):
n.value = 3.14
a[0] = 5
num = multiprocessing.Value('d', 0.0)
arr = multiprocessing.Array('i', range(10))
p = multiprocessing.Process(target=f,args=(num, arr))
p.start()
p.join()
print num.value
print arr[:]
这里我们实际上只有主进程和Process对象代表的进程。
我们在主进程的内存空间中创建共享的内存,也就是Value和Array两个对象。对象Value被设置成为双精度数(d), 并初始化为0.0。
而Array则类似于C中的数组,有固定的类型(i, 也就是整数)。在Process进程中,我们修改了Value和Array对象。
回到主程序,打印出结果,主程序也看到了两个对象的改变,说明资源确实在两个进程之间共享。
2 Manager
Manager对象类似于服务器与客户之间的通信(server-client),与我们在Internet上的活动很类似。
我们用一个进程作为服务器,建立Manager来真正存放资源。其它的进程可以通过参数传递或者根据地址来访问Manager,建立连接后,操作服务器上的资源。
在防火墙允许的情况下,我们完全可以将Manager运用于多计算机,从而模仿了一个真实的网络情境。
下面的例子中,我们对Manager的使用类似于shared memory,但可以共享更丰富的对象类型。
[python] view plain copy
import multiprocessing
def f(x, arr, l):
x.value = 3.14
arr[0] = 5
l.append('Hello')
server =multiprocessing.Manager()
x = server.Value('d', 0.0)
arr = server.Array('i', range(10))
l = server.list()
proc =multiprocessing.Process(target=f, args=(x, arr, l))
proc.start()
proc.join()
print(x.value)
print(arr)
print(l)
Manager利用list()方法提供了表的共享方式。
实际上你可以利用dict()来共享词典,Lock()来共享threading.Lock(注意,我们共享的是threading.Lock,而不是进程的mutiprocessing.Lock。后者本身已经实现了进程共享)等。
这样Manager就允许我们共享更多样的对象。
进程间通信
2017年5月18日
21:07
进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。
Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print 'Put %s to queue...' % value
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
while True:
if not q.empty():
value = q.get(True)
print 'Get %s from queue.' % value
time.sleep(random.random())
else:
break
if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write,args=(q,))
pr = Process(target=read,args=(q,))
# 启动子进程pw,写入:
pw.start()
# 等待pw结束:
pw.join()
# 启动子进程pr,读取:
pr.start()
pr.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
print
print '所有数据都写入并且读完'
总结
· 进程间通信是通过Queue、Pipes等实现的。
进程间通信-pipe
2017年5月18日
21:07
进程间通信-pipe
#coding:utf-8
import multiprocessing
import time
def proc1(pipe):
# 发送
for i in xrange(5):
print "proc1 发送 %s"%i
pipe.send(i)
time.sleep(1)
#for i in xrange(5):
# print 'proc1 接收:',pipe.recv()
def proc2(pipe):
# 接收
for i in xrange(5):
print 'proc2 接收:',pipe.recv()
#for i in xrange(10,15):
# print "proc2 发送 %s"%i
# pipe.send(i)
# time.sleep(1)
# 创建一个管道
pipe = multiprocessing.Pipe()
# 把管道的一端传递给一个线程
p1 =multiprocessing.Process(target=proc1, args=(pipe[0],))
# 把管道的另外一端传递给另外一个线程
p2 =multiprocessing.Process(target=proc2, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
进程与线程的区别
2017年5月23日
8:53
引入进程和线程的概念及区别
1、线程的基本概念
概念
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
************************
好处
· (1)易于调度。
· (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
· (3)开销少。创建线程比创建进程要快,所需开销很少
2、进程的基本状态及状态之间的关系
状态:运行、阻塞、挂起阻塞、就绪、挂起就绪
状态之间的转换:
· (1)准备就绪的进程,被CPU调度执行,变成运行态;
· (2)运行中的进程,进行I/O请求或者不能得到所请求的资源,变成阻塞态;
· (3)运行中的进程,进程执行完毕(或时间片已到),变成就绪态;
· (4)将阻塞态的进程挂起,变成挂起阻塞态,当导致进程阻塞的I/O操作在用户重启进程前完成(称之为唤醒),挂起阻塞态变成挂起就绪态,当用户在I/O操作结束之前重启进程,挂起阻塞态变成阻塞态;
· (5)将就绪(或运行)中的进程挂起,变成挂起就绪态,当该进程恢复之后,挂起就绪态变成就绪态;
3、线程和进程的关系以及区别?
**进程和线程的关系:**
· (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
· (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
· (3)处理机分给线程,即真正在处理机上运行的是线程
· (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.
进程与线程的区别:
· (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
· (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
· (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
· (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
4、进程间通信的方式?
· (1)管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
· (2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
· (3)消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
· (4)共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
· (5)信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
· (6)套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
5、同步和互斥的区别:
· 当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。
· 所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
· 所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
进程VS线程
2017年5月18日
21:08
进程VS线程
我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
进程线程区别
2017年5月23日
8:50
线程和进程的区别
· [x] 进程:对各种资源管理的集合
· [x] 线程:操作系统最小的调度单位,是一串指令的集合
进程不能单独执行,只是资源的集合
进程要操作CPU,必须要先创建一个线程。
所有在同一个进程里的线程,是同享同一块内存空间的
< 关系
进程中第一个线程是主线程,主线程创建其他线程,其他线程也可以创建线程,线程之间是平等的
进程有父进程、子进程,独立的内存空间,唯一的进程标识符、pid
< 速度
启动线程比启动进程快。运行进程和运行线程速度上是一样的,没有可比性。
线程共享内存空间,进程的内存是独立的
< 创建
父进程生成子进程,相当于克隆一份内存空间。进程直接不能直接访问
创建新线程很简单,创建新进程需要对其父进程进行一次克隆
一个线程可以控制和操作同一线程里的其他线程,但是进程只能操作子进程
< 交互
同一个进程之间的线程之间可以直接交流
两个进程想通信必须通过一个中间代理来实现。
Poll进程池
2017年5月18日
21:08
Poll
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name,os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' %(name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' %os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task,args=(i,))
print('Waiting for all subprocessesdone...')
p.close()
p.join()
print('All subprocesses done.')
运行结果:
Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.
代码解读:
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:
p = Pool(5)
就可以同时跑5个进程。
由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
多进程multiprocessing小结
2017年5月22日
23:41
多进程multiprocessing
multiprocessing is a package thatsupports spawning processes using an API similar to the threading module. Themultiprocessing package offers both local and remote concurrency, effectivelyside-stepping the Global Interpreter Lock by using subprocesses instead ofthreads. Due to this, the multiprocessing module allows the programmer to fullyleverage multiple processors on a given machine. It runs on both Unix andWindows.
from multiprocessing import Process
import time
def f(name):
time.sleep(2)
print('hello', name)
if __name__ == '__main__':
p = Process(target=f, args=('bob',))
p.start()
p.join()
注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。
To show the individual process IDsinvolved, here is an expanded example:
from multiprocessing import Process
import os
def info(title):
print(title)
print('module name:', __name__)
print('parent process:', os.getppid())
print('process id:', os.getpid())
print("nn")
def f(name):
info('