这一部分主要介绍sys、os、hashlib和re模块。其中的re模块介绍得非常详细,是本部分的重点!
均为python3.5.1环境。
一、sys模块
sys模块涉及的主要是与python解释器相关的操作。这里的system应当理解为python的系统,而不是主机的系统。os模块才是主机操作系统相关。在sys模块中,毫无疑问,最重要的是sys.path,它决定了你的模块搜索路径,任何一个python程序员都必须搞清楚它的所有问题。
sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) sys.version 获取Python解释程序的版本信息 sys.maxint 最大的Int值 sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值 sys.platform 返回操作系统平台名称 sys.stdin 输入相关 sys.stdout 输出相关 sys.stderror 错误相关
二、os模块
os模块主要是和操作系统相关的功能。由于python是跨平台的语言,所以在涉及操作系统相关的操作时,请尽量使用本模块提供的方法,而不要使用当前平台特定的用法或格式,否则一旦移植到其他平台,可能会造成难以解决的困扰。
os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname") 改变当前脚本工作目录;相当于shell下cd os.curdir 返回当前目录: ('.') os.pardir 获取当前目录的父目录字符串名:('..') os.makedirs('dir1/dir2') 可生成多层递归目录 os.removedirs('dirname1') 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推 os.mkdir('dirname') 生成单级目录;相当于shell中mkdir dirname os.rmdir('dirname') 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirname os.listdir('dirname') 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印 os.remove() 删除一个文件 os.rename("oldname","new") 重命名文件/目录 os.stat('path/filename') 获取文件/目录信息 os.sep 操作系统特定的路径分隔符,win下为"\\",Linux下为"/" os.linesep 当前平台使用的行终止符,win下为"\t\n",Linux下为"\n" os.pathsep 用于分割文件路径的字符串 os.name 字符串指示当前使用平台。win->'nt'; Linux->'posix' os.system("bash command") 运行shell命令,直接显示.但返回值是0或-1,不能获得显示在屏幕上的数据。与之相对的是popen
os.popen("bash command") 运行shell命令,通过read方法将命令的结果返回,不像system只能看不能存,这个能存! os.environ 获取系统环境变量 os.path.abspath(path) 返回path规范化的绝对路径 os.path.split(path) 将path分割成目录和文件名二元组返回 os.path.dirname(path) 返回path的目录。其实就是os.path.split(path)的第一个元素 os.path.basename(path) 返回path最后的文件名。如何path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素 os.path.exists(path) 如果path存在,返回True;如果path不存在,返回False os.path.isabs(path) 如果path是绝对路径,返回True os.path.isfile(path) 如果path是一个存在的文件,返回True。否则返回False os.path.isdir(path) 如果path是一个存在的目录,则返回True。否则返回False os.path.join(path1[, path2[, ...]]) 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略 os.path.getatime(path) 返回path所指向的文件或者目录的最后存取时间 os.path.getmtime(path) 返回path所指向的文件或者目录的最后修改时间
三、hashlib模块
用于加密相关的操作,代替了md5模块和sha模块,主要提供 SHA1, SHA224, SHA256, SHA384, SHA512 ,MD5 算法。需要注意的是在python3中,加密模块操作的对象是byte类型,而不是string类型,因此要通过内置函数bytes将字符串先转化为byte类型。其基本使用如下:
import hashlib # ######## md5 ######## hash = hashlib.md5() # help(hash.update) hash.update(bytes('admin', encoding='utf-8')) print(hash.hexdigest()) print(hash.digest()) ######## sha1 ######## hash = hashlib.sha1() hash.update(bytes('admin', encoding='utf-8')) print(hash.hexdigest()) # ######## sha256 ######## hash = hashlib.sha256() hash.update(bytes('admin', encoding='utf-8')) print(hash.hexdigest()) # ######## sha384 ######## hash = hashlib.sha384() hash.update(bytes('admin', encoding='utf-8')) print(hash.hexdigest()) # ######## sha512 ######## hash = hashlib.sha512() hash.update(bytes('admin', encoding='utf-8')) print(hash.hexdigest())
加密算法虽然非常厉害,但也存在缺陷,通过撞库可以反解。所以,必须在加密算法中添加自定义key来做进一层的加密。
import hashlib # ######## md5 ######## hash = hashlib.md5(bytes('898oaFs09f',encoding="utf-8")) hash.update(bytes('admin',encoding="utf-8")) print(hash.hexdigest())
由于常用口令的MD5值很容易被计算出来,这被称为“撞库”。为了让存储的用户口令不是那些能被计算出来的常用口令的MD5,通常对原始密码加一个复杂字符串,俗称“加盐”:
def cal_md5(password):
return get_md5(password + 'Salt')
经过“Salt”(此处代表任意字符串)处理的MD5口令,只要Salt不被黑客知道,即使用户的口令比较简单,也很难通过MD5反推明文口令。
但是如果有两个用户都使用了相同的简单口令比如123456
,他们将生成两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。
另外,python还有一个内置模块hmac ,它在内部会对我们创建的key和内容进行进一步的处理然后再加密。
import hmac h = hmac.new(bytes('898oaFs09f',encoding="utf-8")) h.update(bytes('admin',encoding="utf-8")) print(h.hexdigest())
四、re模块
其实这篇博文的重点是re模块。python的re模块主要用于处理与正则表达式相关的操作,它基于C语言开发的正则引擎。
关于如何书写正则表达式,不是本文的重点,有专门的书籍介绍相关的内容,个人强烈推荐Ben Forta编写的《正则表达式必知必会》,书很薄,但是内容凝练,例子经典,是不可多得的快速入门好书!
python在使用正则表达式时需要注意的是“\”字符!由于在python中,“\”也被定义为转义字符,因此两个python中的“\”才能代表一个正则中的“\”,这就导致了大量的“\”重复。为了解决这一问题,python提供了原生字符的办法。也就是在字符串前面加上一个“r”,代表此字符串中的“\”可直接用于正则表达式,而不用再次转义。因此,请养成在python的正则表达式字符串的前面添加一个“r“的好习惯。
在re模块中,有这么几个重要的方法,compile、match、search、findall、split、sub。它们比字符串对象方法具有更快的速度和更强大的能力。下面逐一进行介绍。
函数 | 描述 | 返回值 |
compile(pattern[, flags]) | 根据包含正则表达式的字符串创建模式对象 | re模块的对象 |
search(pattern, string[, flags]) | 在字符串中寻找模式 |
第一个匹配到 的对象或者None |
match(pattern, string[, flags]) | 在字符串的开始处匹配模式 |
在字符串开头匹配到 的对象或者None |
split(pattern, string[, maxsplit=0,flags]) | 根据模式的匹配项来分割字符串 | 分割后的字符串列表 |
findall(pattern, string,flags) | 列出字符串中模式的所有匹配项 | 所有匹配到的字符串列表 |
sub(pat,repl, string[,count=0,flags]) | 将字符串中所有的pat的匹配项用repl替换 | 完成替换后的新字符串 |
finditer(pattern, string,flags) | 将所有匹配到的项生成一个迭代器 |
所有匹配到的字符串 组合成的迭代器 |
subn(pat,repl, string[,count=0,flags]) | 在替换字符串后,同时报告替换的次数 |
完成替换后的新字符串 及替换次数 |
escape(string) | 将字符串中所有特殊正则表达式字符串转义 | 转义后的字符串 |
purge(pattern) | 清空正则表达式 | |
template(pattern[,flags]) | 编译一个匹配模板 | 模式对象 |
fullmatch(pattern, string[, flags]) | match函数的全字符串匹配版本 | 类似match的返回值 |
1. compile
该方法用于将正则表达式转换为模式对象,可以实现更高效率的匹配。使用compile完成一次转换后,在每次使用模式的时候就不用再进行转换了。经过compile转换的正则表达式对象也能使用普通的re函数。其用法如下:
>>> import re >>> pat = re.compile(r"abc") >>> pat.match("abc123") <_sre.SRE_Match object; span=(0, 3), match='abc'> >>> pat.match("abc123").group <built-in method group of _sre.SRE_Match object at 0x00000000035104A8> >>> pat.match("abc123").group() 'abc'
经过compile方法编译过后的返回值是个re模块的对象,它可以调用match、search、findall等其他方法。但其他方法不能调用compile方法,这么是不行的:
>>> re.match(r"abc","abc123").compile() Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> re.match(r"abc","abc123").compile() AttributeError: '_sre.SRE_Match' object has no attribute 'compile'
我们必须分清楚这里的差别!实际上,在match和search等方法在使用前,python内部就帮你进行了compile这么个步骤。类似re.match,re.split的用法是python给我们提供的“语法糖”,归根结底,它们还是需要用compile方法在内部先编译一下。
那么是使用compile还是直接使用re.match呢?看场景!如果你只是简单的匹配一下后就不用了,那么re.match这种方式无疑来得更简单快捷。如果你有个模式需要进行大量次数的匹配,那么先编译,再匹配的运行效率会高很多。
2. match
match方法会在给定字符串的开头进行匹配,如果匹配不成功则返回None,匹配成功返回一个匹配对象,这个对象有个group方法,可以将匹配到的字符串给出。
>>> ret = re.match(r"abc","ab1c123") >>> print(ret) None >>> re.match(r"abc","abc123") <_sre.SRE_Match object; span=(0, 3), match='abc'> >>> obj = re.match(r"abc","abc123") >>> obj.group() 'abc'
对于一个<_sre.SRE_Match object; span=(0, 3), match='abc'>对象,span指的是匹配到的字符在字符串中的位置下标,分别对应start和end。需要注意的是不包括end位置的下标,它是右开口的。具体如下:
>>> obj = re.match(r"abc","abc123") >>> obj.start() 0 >>> obj.end() 3 >>> obj.span() (0, 3) >>> obj.group() 'abc'
3. search
search方法是在字符串内查找模式,返回第一个匹配到的字符串。它的返回值类型和使用方法与match是一样的。
>>> obj = re.search(r"abc","123abc456abc789") >>> obj <_sre.SRE_Match object; span=(3, 6), match='abc'> >>> obj.group() 'abc' >>> obj.start() 3 >>> obj.end() 6 >>> obj.span() (3, 6)
4. findall
作为re的三大搜索函数之一,findall和match、search的不同之处在于,前两者都是单值匹配,而它是全部匹配,它的返回值是一个匹配到的字符串的列表。这个列表没有group的方法,没有start、end、span,更不是一个匹配对象,它仅仅是个列表!如果一项都没有匹配到那么返回一个空列表。
>>> obj = re.findall(r"abc","123abc456abc789") >>> obj ['abc', 'abc'] >>> obj.group() Traceback (most recent call last): File "<pyshell#37>", line 1, in <module> obj.group() AttributeError: 'list' object has no attribute 'group' >>> obj = re.findall(r"ABC","123abc456abc789") >>> print(obj) []
5. split
split函数和字符串类型的split方法很相似,都是利用特定的字符去分割字符串。但是re模块的split可以使用正则表达式,因此更灵活,更强大,而且还有“杀手锏”。看下面这个例子,匹配模式是加减乘除四个运算符中的任何一种,通过split将字符串分割成一个一个的数字:
>>> s = "8+7*5+6/3" >>> import re >>> a_list = re.split(r"[\+\-\*\/]",s) >>> a_list ['8', '7', '5', '6', '3']
split有个参数maxsplit,用于指定分割的次数:
>>> a_list = re.split(r"[\+\-\*\/]",s,maxsplit= 2) >>> a_list ['8', '7', '5+6/3']
利用分组的概念,split函数还可以保存被匹配到的分隔符,这个功能非常重要!为什么呢?比如,你要计算8+7,是不是要同时获得8,+,7三个字符?就如同下面的例子,字符串s = "8+7*5+6/3",想要计算字符串内的表达式的值,你必须获得其中加减乘除的符号。
>>> a_list = re.split(r"([\+\-\*\/])",s) >>> a_list ['8', '+', '7', '*', '5', '+', '6', '/', '3']
6. sub
sub函数类似字符串的replace功能,用指定的内容替换匹配到的字符,可以指定替换次数。
>>> s = "i am jack! i am nine years old ! i like swiming!" >>> import re >>> s = re.sub(r"i","I",s) >>> s 'I am jack! I am nIne years old ! I lIke swImIng!'
sub函数有一个高级用法,有人称之为“分组引用”,这是一个非常强大的功能,运用好了能发挥无穷的效率!举例如下:
import re origin = "Hello,world!" r = re.sub(r"(world)", r"<em>\1<em>", origin) print(r) 运行结果: Hello,<em>world<em>!
分析一下,首先在正则表达式里用括号建立了一个分组,然后在要替换进去的字符串里用“\1”引用了这个分组匹配到的内容。例子里面将匹配规则写死了,但你完全可以使用更加灵活的正则表达式,更加灵活的引用(比如\2,\3),实现更灵活的替换。
re模块其它的几个函数,使用较少,用法也比较简单。就不一一介绍了。
7. flag编译标志
需要注意的是,python的re模块为其主要的函数都设计了个flag参数,被称为编译标志,比如I、M、S等。编译标志可以修改正则表达式的一些运行方式。可以使用全名如 IGNORECASE,或缩写如 I来引用它们。多个标志可以组合,如 re.I | re.M 被设置成 I 和 M 标志:
I (忽略大小写)
IGNORECASE:使匹配对大小写不敏感。
L (本地模式)
LOCALE:使得\w, \W, \b, \B, \d, \D依赖于本地设置。
M (多行模式)
MULTILINE:对于这个编译标识,网上很多解释都是错误的,真正的解释应该是让正则表达式的^和$符号可以适应多行模式的字符串。
例如:字符串 s = "\nabc\n" 实际上它有三行。
>>> import re >>> s = "\nabc\n" >>> re.search(r"^abc$",s) >>> >>> re.search(r"abc$",s) <_sre.SRE_Match object; span=(1, 4), match='abc'> >>> re.search(r"^abc",s) >>> >>> re.search(r"abc",s) <_sre.SRE_Match object; span=(1, 4), match='abc'> >>> >>> re.search(r"^abc$",s,re.M) <_sre.SRE_Match object; span=(1, 4), match='abc'> >>> >>> re.search(r"^abc",s,re.M) <_sre.SRE_Match object; span=(1, 4), match='abc'> >>> >>> re.search(r"abc$",s,re.M) <_sre.SRE_Match object; span=(1, 4), match='abc'>
S (匹配换行符)
DOTALL:使 "." 特殊字符完全匹配任何字符,包括换行;没有这个标志, "." 匹配除了换行外的任何字符。
X (注释模式)
VERBOSE:该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。当该标志被指定时,在正则表达式字符串中的空白、tab、换行符被忽略,除非该空白符在字符类中或在反斜杠之后;这可以让你更清晰地组织和缩进 表达式。它也允许你将注释写入表达式,这些注释会被引擎忽略;注释用 "#"号 来标识,不过该符号不能在字符串或反斜杠之后。巴拉巴拉一堆,你只需要记住它的两个作用:一是让冗长难懂的表达式更易读;二是给表达式加注释。
下面的pat等同于 r"\*([^\*]+)\*":
>>> pat = re.compile(r''' \* # 转义一个星号 ( #左括号代表一个 组的开始 [^\*]+ #捕获任何非星号的字符 ) #右括号代表组的结束 \* #转义一个星号 ''',re.VERBOSE) >>> obj = pat.search("hi ,this is a *something* !") >>> obj.group() '*something*'
U
UNICODE:兼容模式。在字符串模式下被忽略(默认模式),在字节模式下被禁止。我们知道,在python3之后,string和bytes被独立成了两种不同的数据类型。在re模块中,不能用bytes去匹配string或者用string去匹配bytes,只能用string匹配string,bytes匹配bytes。下面是一个bytes匹配bytes的例子:
import re s = "halo" b = bytes(s, encoding="utf-8") pat = bytes('ha', encoding="utf-8") print("字符串s:%s" % s) print("字节b:%s" % b) print("字节类型正则表达式pat:%s" % pat) obj_s = re.search(pat, b) print("匹配结果: %s" % obj_s.group()) 运行结果: 字符串s:halo 字节b:b'halo' 字节类型正则表达式pat:b'ha' 匹配结果: b'ha
当使用UNICODE模式时,将强制禁止使用bytes类型,一但使用将报错。在string类型中,UNICODE是默认设置。
obj_s = re.search(pat, b, re.U) #只贴出了修改了的部分 运行结果: 字符串s:halo 字节b:b'halo' 字节类型正则表达式pat:b'ha' Traceback (most recent call last): File "F:/Python/pycharm/s13/reflect/test.py", line 14, in <module> obj_s = re.search(pat, b, re.U) File "C:\Python35\lib\re.py", line 173, in search return _compile(pattern, flags).search(string) File "C:\Python35\lib\re.py", line 293, in _compile p = sre_compile.compile(pattern, flags) File "C:\Python35\lib\sre_compile.py", line 536, in compile p = sre_parse.parse(p, flags) File "C:\Python35\lib\sre_parse.py", line 830, in parse p.pattern.flags = fix_flags(str, p.pattern.flags) File "C:\Python35\lib\sre_parse.py", line 811, in fix_flags raise ValueError("cannot use UNICODE flag with a bytes pattern") ValueError: cannot use UNICODE flag with a bytes pattern
A
ASCII:对于string模式,使得\w, \W, \b, \B, \d, \D只匹配ACSII码字符集,而不是整个Unicode字符集(默认)。对于bytes模式,这个编译标志是默认设置,不需要特别指定。通常我们不太关心这些,但是对于那些频繁在各种语言或字符集中打交道的代码就得小心了。
8. re的高级用法:分组
python的re模块有一个高级的用法,叫做分组。所谓的分组就是去已经匹配到的内容里面再筛选出需要的内容,相当于二次过滤。实现分组靠的是小括号,而获取分组的内容靠得是group、groups和groupdict。re模块里的几个重要函数在分组这一块,有几种不同的表现形式,需要区别对待。
例一:match函数,无分组
import re origin = "hasdfi123123safd" # 无分组 r = re.match("h\w+", origin) print(r.group()) # 获取匹配到的所有结果 print(r.groups()) # 获取模型中匹配到的分组结果 print(r.groupdict()) # 获取模型中匹配到的分组结果 结果: hasdfi123123safd () {}
例二:match函数,有分组(注意小括号!)
import re origin = "hasdfi123123safd123" # 有分组 r = re.match("h(\w+).*(?P<name>\d)$", origin) print(r.group()) # 获取匹配到的所有结果 print(r.group(1)) # 获取匹配到的所有结果 print(r.group(2)) # 获取匹配到的所有结果 print(r.groups()) # 获取模型中匹配到的分组结果 print(r.groupdict()) # 获取模型中匹配到的分组中所有执行了key的组 运行结果: hasdfi123123safd123 asdfi123123safd12 3 ('asdfi123123safd12', '3') {'name': '3'}
分析一下上面的代码,正则表达式"h(\w+).*(?P<name>\d)$"中有2个小括号,表示它分了2个小组,在匹配的时候是拿整体的表达式去匹配的,而不是拿小组去匹配的。(\w+)表示这个小组内是1到多个字母数字字符,(?P<name>\d)中?P<name>是个正则表达式的特殊语法,表示给这个小组取了个叫“name”的名字,?P<xxxx>是固定语法。在获取分组值得时候,group()和group(0)是对等的,都表示整个匹配到的字符串,从group(1)开始,分别是从左往右的小组序号,按位置顺序来。
有时候括号会存在嵌套情况,那怎么确定1,2,3?要么用取名字的方法,要么就数左括号,第几个左括号就是第几个分组。例如(1(2,(3)),(4)),0表示表达式本身,不参加数左括号的动作。
例子三:search函数,有分组
import re origin = "sdfi1ha23123safd123" # 有分组 r = re.search("h(\w+).*(?P<name>\d)$", origin) print(r.group()) # 获取匹配到的所有结果 print(r.group(0)) # 获取匹配到的所有结果 print(r.group(1)) # 获取匹配到的所有结果 print(r.group(2)) # 获取匹配到的所有结果 print(r.groups()) # 获取模型中匹配到的分组结果 print(r.groupdict()) # 获取模型中匹配到的分组中所有执行了key的组 运行结果: ha23123safd123 ha23123safd123 a23123safd12 3 ('a23123safd12', '3') {'name': '3'}
实际上search和match在分组的机制上基本一样,两者仅仅在搜索策略上不一样。
例子四:findall函数,无分组
import re origin = "has something have do" # 无分组 r = re.findall("h\w+", origin) print(r) 运行结果: ['has', 'hing', 'have'] # 一切看起来没什么不一样
例子五:findall函数,有一个分组
import re origin = "has something have do" # 一个分组 r = re.findall("h(\w+)", origin) print(r) 运行结果: ['as', 'ing', 'ave']
从上面我们就能看出re模块分组的核心本质:匹配仍然是整体去匹配,但是返回的结果却是分组后的部分,也就是二层过滤。
例子六:findall函数,有两个以上分组
import re origin = "hasabcd something haveabcd do" # 原字符串调整了一下 # 两个分组 r = re.findall("h(\w+)a(bc)d", origin) print(r) 运行结果: [('as', 'bc'), ('ave', 'bc')]
注意到了返回值是什么了吗?元组组成的列表!(另外,要强调的是findall的返回值是个列表,没有group、groups、groupdict的概念!)
例子七:sub函数,有分组
import re origin = "hasabcd something haveabcd do" # 有分组 r = re.sub("h(\w+)", "haha",origin) print(r) 运行结果: haha somethaha haha do
看到没有?sub没有分组的概念!这是因为sub函数是用整体的正则表达式去匹配,然后又整体的去替换,分不分组对它没有意义。所以,这里要注意了!
例子八:split函数,有一个分组
import re origin = "has abcd something abcd do" # 有一个分组 r = re.split("(abcd)", origin) print(r) 运行结果: ['has ', 'abcd', ' something ', 'abcd', ' do']
事实上,在前面我们已经展示过这个例子了,通过分组,我们可以拿到split匹配到的分隔符。
例子九:split函数,有两个分组,并且嵌套
import re origin = "has abcd something abcd do" # 有一个分组 r = re.split("(a(bc)d)", origin) print(r) 运行结果: ['has ', 'abcd', 'bc', ' something ', 'abcd', 'bc', ' do']
最后一个例子:split函数,有多个分组,并且嵌套
import re origin = "has abcd something abcd do" # 有一个分组 r = re.split("(a(b)c(d))", origin) print(r) 运行结果: ['has ', 'abcd', 'b', 'd', ' something ', 'abcd', 'b', 'd', ' do']
至此,re模块中的分组概念已经介绍得比较清楚了。re模块的主体内容也就差不多了。