0、前言
我们知道每个语言都有自己的内建函数来对字符串进行处理。通过这些内建函数我们可以对字符串进行一些简单的处理,从而达到数据清洗等目的。在Python中有index()——定位、 find()——查找、split()——分隔、 count()——计数、 replace()——替换等。但这些方法都只是最简单的字符串处理。
从我们处理流程来看,能用简单方法来处理的一定不要把问题复杂化,而简单方法无法处理的字符串内容提取则需要正则表达式来处理。
1、什么是正则表达式
(——来源于百度百科)
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。正则表达式的特点是:
1. 灵活性、逻辑性和功能性非常强;
2. 可以迅速地用极简单的方式达到字符串的复杂控制。
3. 对于刚接触的人来说,比较晦涩难懂。
由于正则表达式主要应用对象是文本,因此它在各种文本编辑器场合都有应用,小到著名编辑器EditPlus,大到Microsoft Word、Visual Studio等大型编辑器,都可以使用正则表达式来处理文本内容。
2、正则表达式语法、字符、特殊构造等
见下图,图片资料本来自己整理了一
份有关于法的,后来在网上发现一张更详细的图。基本核心用法都在图上。
图片材料1:
图片材料2:
3、Python中正则表达式的转义
与大多数编程语言相同,正则表达式里使用”\”作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符”\”,那么使用编程语言表示的正则表达式里将需要4个反斜杠”\\\\”:前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。
Python里的原生字符串很好地解决了这个问题,这个例子中的正则表达式可以使用r”\\”表示。同样,匹配一个数字的”\\d”可以写成r”\d”。有了原生字符串,妈妈也不用担心是不是漏写了反斜杠,写出来的表达式也更直观勒。
4、贪婪模式与非贪婪模式
“贪婪模式”总是尝试匹配尽可能多的字符;“非贪婪模式”则相反,总是匹配尽可能少的字符。例如,用“ab*”如果用于查找”abbbc”,将找到”abbb”。而如果使用非贪婪的数量词”ab*?”,将找到”a”。
5、Python正则表达式模块Re使用
Python的re模块提供了正则表达式支持,使用前首先需要导入该模块:
import re
在Python中正则表达式搜索的典型语法为:
match = re.search(pat, str)
re.search()方法接收一个正则表达式模式和一个字符串作为参数,并在给定字符串中搜索给定模式。
如果搜索成功,search()返回一个匹配对象,否则返回None。
因此,通常搜索会紧跟一个if语句来测试搜索是否成功,如下例所示,搜索word:后紧跟三个字母的模式:
mystr = 'an example word:cat!!' match = re.search(r'word:\w\w\w', mystr) # If-statement after search() tests if it succeeded if match: # '查找实际内容 word:cat,其实就是word:后面跟着三个小写字母就符合' print('命中!', match.group()) else: print('未命中!')
代码match =re.search(pat,str)将搜索结果存储在match变量中,然后if语句对match变量进行测试。
- 如果为真,则match.group()为匹配文本(e.g. ‘word:cat’)。
- 否则match为假(更具体地说是None),即匹配不成功,不存在匹配文本。
字符串开头的r指定一个raw字符串,即对转义字符\不进行解析,这对正则表达式是很便利的。
因此建议养成习惯在使用正则表达式时始终采用r指定模式。
5.1、基本示例
正则表达式匹配字符串中的模式的基本规则为:
- 匹配从左到右,在完成第一次匹配后停止
- 必须匹配所有给定模式才算一次成功匹配
- 如果match = re.search(pat, str)匹配成功,则match变量非空,match.group()将返回匹配文本
# 在字符串'piiig' 中搜索 'iii' . # 所有的模式都必须匹配,但它可能出现在任何地方。 # 在成功时,match.group()是匹配的文本。 match = re.search(r'iii', 'piiig') # 命中, match.group() == "iii" match = re.search(r'igs', 'piiig') # 未命中 match == None # . = any char but \n match = re.search(r'..g', 'piiig') # 命中, match.group() == "iig" # \d = digit char, \w = word char match = re.search(r'\d\d\d', 'p123g') # 命中, match.group() == "123" match = re.search(r'\w\w\w', '@@abcd!!') # 命中, match.group() == "abc"
5.2、重复模式
可以采用+和*来指定重复出现的模式
- + – 匹配1个或更多次出现左边的模式(一定出现),如’i+’表示一个或多个i
- * – 匹配0个或更多次出现左边的模式(可能出现)
- ? – 匹配0个或1个左边的模式(最多出现一次)
“Leftmost” & “Largest”
首先找到最先出现(Leftmost)的匹配,再尽可能长的(Largest)匹配字符串。
#%% 重复模式 # i+ 表示一个或者多个, match = re.search(r'pi+', 'piiig') # 将命中, match.group() == "piii" # 从第一个或者最左边起,尽可能的贪婪的去获取更长的数据。记住:是第一个 match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii" # \s* 表示0个或者多个空白字符 # 下面这个例子中是寻找3个数字,数字中可能以空白符分隔或者不分割。 # 需要注意到,'xx1 2 3xx'或者'xx123xx'都会查找到123 match = re.search(r'\d\s*\d\s*\d', 'xx1 2 3xx') # found, match.group() == "1 2 3" match = re.search(r'\d\s*\d\s*\d', 'xx12 3xx') # found, match.group() == "12 3" match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123" # ^ 表示将必须从字符串开头开始匹配b,将会失败: match = re.search(r'^b\w+', 'foobar') # not found, match == None # 去掉^后,将会成功: match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"
5.3 方括号([])
方括号可以用于指定一个字符集合,如[abc]匹配‘a’或‘b’或‘c’。
\w和\s同样可以放入方括号中,但是.只表示实际上的‘.’6。
对于邮件地址,使用方括号将‘.’和‘-‘加入需要匹配的字符集合,模式r'[\w.-]+@[\w.-]+’可以获得整个邮件地址。
下面的例子中,我将匹配模式赋值给了一个变量,在调用时,直接调用变量名即可。这样方便我们后续的修改。
#%% 匹配邮箱 # 这是我帮人写爬虫时的王牌匹配规则 emailRegex = r"\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,34}" # 随便在网上找了一个网页找到一个带邮箱的代码,隐藏 mystr = ''' <td width="193" height="154" background="images/index1_30.jpg"> <table border="0" cellpadding="0" cellspacing="0"> <tbody><tr> <td width="20"></td> <td style="line-height:20px">电话:011-22228888<br> 传真:0101-66666666<br> 徐先生:15666666666<br> 陈小姐:13998888898<br> E-mail:88888888aaaaaaaaaa1@qq.com </td> </tr> </tbody></table> </td> ''' match = re.search(emailRegex, mystr) if match: print(match.group()) # '88888888aaaaaaaaaa1@qq.com'
5.4[]范围
可以使用-来表示范围,即[a-z]匹配所有小写字母。
若要使用-而不表示范围(通常情况),将-放在模式集合的最后,如[abc-]。
^放在方括号内开头位置表示取反集,即[^ab]表示除’a’和’b’之外的任意字符。
5.5提取Group
正则表达式的group方法允许程序选取匹配文本的一部分。
假设在邮件地址问题中我们需要提取用户名和主机名。
为了这样做,将用户名模式和主机名模式用()包围起来,例如r'([\w.-]+)@([\w.-]+)’。
在这种情况下,()并不影响匹配规则,而是在匹配文本中建立一个分组(group)。对于成功的匹配:
- match.group(1)表示第一个括号内的模式匹配的文本
- match.group(2)表示第二个括号内的模式匹配的文本。
- match.group()仍然表示整个匹配文本7
- match.groups()返回包括所有分组匹配结果的元组。
mystr = 'purple alice-b@google.com monkey dishwasher' match = re.search('([\w.-]+)@([\w.-]+)', mystr) if match: print(match.group()) # 'alice-b@google.com' (the whole match) print(match.group(1)) # 'alice-b' (the username, group 1) print(match.group(2)) # 'google.com' (the host, group 2)
通常使用正则表达式的工作流程是首先为需要查找的东西写出相应的模式,然后再添加括号分组以便提取感兴趣的部分。
5.6 findall
findall()可能是re模块中最有用的函数。之前我们采用re.search()来查找关于给定模式的Leftmost匹配。
不同的是,findall()将找到所有的匹配并以字符串列表(list)的形式返回,每一个字符串表示一次匹配。
#%% 5.6 # 假设我们有一个包含许多电子邮件地址的文本 mystr = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher' # 这里re.findall()返回所有找到的电子邮件字符串的列表 emails = re.findall(r'[\w\.-]+@[\w\.-]+', mystr) ## ['alice@google.com', 'bob@abc.com'] for email in emails: # 当找到后,打印出来 print(email)
5.7 对文件findall
对于文件进行模式匹配,你可能会习惯于写一个循环来在文件的行间迭代,在每一次迭代中调用findall()。
与其这样,不如让findall()替你完成迭代过程–这样当然更好!
只需将文件内的文本传递给findall()让其一次性以列表的形式返回所有匹配:
# 打开文件 f = open('test.txt', 'r') #将文件文本输入findall();它返回所有找到的字符串的列表 strings = re.findall(r'some pattern', f.read())
5.8 findall和分组
分组括号()同样可以用于和findall()组合使用。
如果模式包含两个或多个分组括号,则findall()将返回元组(tuple)列表而不是字符串(str)列表。
每个元组表示一次匹配,在元组内则是group(1),group(2),…数据9。
所以,如果在邮件地址模式中加入两个分组括号,则findall()将返回一个元组列表,每一个元组包含用户名和主机名。
#%% 5.8 mystr = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher' tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', mystr) print(tuples) # [('alice', 'google.com'), ('bob', 'abc.com')] for tuple in tuples: print(tuple[0]) # username print(tuple[1]) # host
一旦获得了元组列表,则可以在元组间循环以对每一个元组进行一些计算。
- 如果模式不包含分组括号,则findall()返回一个字符串列表。
- 如果模式包含单个分组括号,则findall()返回对应于单个分组的字符串列表。
一条较晦涩的可选特性:
有时模式中包含分组括号(),且并不希望提取它,在括号内以?:开始, e.g.(?:),则不会将匹配文本归入分组结果中。
6 其他例子
6.1 提取手机号码 / 提取数字
phoneRegex = r"0?(13|14|15|17|18|19)[0-9]{9}" # 用之前的代码例子 mystr = ''' <td width="193" height="154" background="images/index1_30.jpg"> <table border="0" cellpadding="0" cellspacing="0"> <tbody><tr> <td width="20"></td> <td style="line-height:20px">电话:011-22228888<br> 传真:0101-66666666<br> 徐先生:15666666666<br> 陈小姐:13998888898<br> E-mail:88888888aaaaaaaaaa1@qq.com </td> </tr> </tbody></table> </td> ''' # 只找第一个 match = re.search(phoneRegex, mystr) if match: print(match.group()) # 15666666666 # 找出全部 这里的规则设计就很有意思, # 我只需要找11个数字连续版本即可。 tuples = re.findall(r'\d{11}', mystr) print(tuples) for one in tuples: print(one)