【DS with Python】 re模块与正则表达式


前言

正则表达式(Regular expression,简称Regex)在很多语言中都会用到,其内容几乎是独立于具体语言,有着非常广泛的应用,在python中,本周学习利用re模块进行匹配,合理利用re.search(), re.match(), re.findall(), re.finditer() 可以较好的匹配我们需要的关键词,重要程度值的专门开一页来记录


以下是本篇文章正文内容

零、思路概括

re模块中大部分的函数参数都是(pattern, text),本文想先介绍几个简单的函数,再介绍pattern的书写规则,再一些更广泛的函数和一些特殊用法


一、开头与全文查询(re.match() 和 re.search())

首先要说的是,在re模块中,大部分函数的第一个参数都是pattern,需要做什么样的匹配,就把规则放在第一个参数,接下来是需要匹配的文本,re.match(pattern,text) 和re.search(pattern,text) 多用在if statement中用作条件判断,他们的区别如下:

  • match() checks for a match that is at the beginning of the string.
  • search() checks for a match anyway in the string.

简而言之,match()是匹配开头,search()是匹配全文,他们返回的内容实际上是一个re.Match的object,他可以直接放在if statment中做boolean值,查看这个object,他会告诉你什么值被匹配了(可以用group()来查看,将在下文介绍),位置在哪里,下面我们用一个例子来看一下:

上次我们用np.genfromtxt()来阅读csv,这次我们考虑用.read()来读取txt文件,方法如下:

with open('../week1/datasets/ferpa.txt') as file:
    wiki=file.read()


--Outputs:
'Overview[edit]\nFERPA gives parents access to their child\'s education records,
 an opportunity to seek to have the records amended, and some control over the
  disclosure of information from the records. With several exceptions, schools 
  must have a student\'s consent prior to the disclosure of education records 
 	...#省略部分
counseling center) might be released to the school administration under certain
 triggering events, such as when a student sued his college or university.[7]
 [8]\n\nUsually, student medical treatment records will remain under the 
 protection of FERPA, not the Health Insurance Portability and Accountability Act 
 (HIPAA). This is due to the "FERPA Exception" written within HIPAA.[9]'

我们来看一下文章的开头是不是’Overview’,用re.match(),文本内有没有’FERPA’,用re.search():

if re.match('Overview',wiki):   #begining
    print('1')
else:
    print('0') 
    
if re.match('FERPA',wiki):   #begining
    print('1')
else:
    print('0') 

if re.search('FERPA',wiki):  
    print('1')
else:
    print('0')   #the result of re.search is re.Match object which can always be used in if statement.

--Outputs:
1
0
1

可以看到在开头match 'FERPA’就返回了0


二、全匹配与分割 (re.findall()和re.split())

re模块中的re.split(pattern,text)可以根据pattern对文本进行分割,将字符串分割为列表,并返回成功匹配的列表。但是pattern的部分并不会出现在结果中

text='Amy works diligently Amy gets good grades Our student Amy is successful'
print(re.split('Amy',text))

--Outputs:
['', ' works diligently ', ' gets good grades Our student ', ' is successful']

相比于re.match()和re.search()中返回的是re.Match的object,re.findall(pattern,text)可以找到所有符合规则的内容并输出:

print(re.findall('Amy',text))

--Outputs:
['Amy', 'Amy', 'Amy']

三、正则规则–pattern参数的书写规则

1. Anchors:位置符

Anchors本意为锚,在正则表达式中可以理解为确定位置的符号,主要用到两种:
^:匹配文本开头
$:匹配文本结尾

(注意:^不仅仅具有匹配文本开头的作用,他也具有取反的意思,在下文将会提到)

2. Character Classes:字符组

在正则表达式中,[]代表的是字符组,也可以叫set operator,字符组为一组字符,首先需要明确,无论里面的内容有多少,他在真正匹配时只占一个位置,待匹配文本中如果出现的第一个[]括号中的字符即会被匹配上,在[]的字符与字符之间的关系是或的关系,在re模块中可以在字符之间添加 | ,但在其他的Regex中可能不行,平常在用的时候用到了[]就不要在里面加 | 了,属于多此一举。

在上文讲到的 ^ 放在[]中的意思是取反,表示要匹配的内容是除了括号里面的所有字符,注意,这里可能包括空格、-、@等其他符号不仅仅是数字和字母

例如[ABC]可以匹配A或B或C,[B-D]可以B, C, D, [^ABC]则可以匹配除了ABC外的所有字符
我们用re.findall()将所有匹配的结果列出来看一下:

import re 
grades='ACAAAABCBCBAADDD'
print(re.findall('A',grades))
print(re.findall('[AB]',grades))
print(re.findall('[A][B-D]',grades))

grades2='ACAAAABCBCBAADDD!@#¥%&*()'
print(re.findall('[^ABC]',grades2))
--Outputs:
['A', 'A', 'A', 'A', 'A', 'A', 'A']
['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'A', 'A']
['AC', 'AB', 'AD']
['D', 'D', 'D', '!', '@', '#', '¥', '%', '&', '*', '(', ')']#可以看到,把其他的字符都匹配出来了

与字符组具有相似功能的是管道符 | 也表示或,|可以在多个模式中选择一个,字符组则多用于匹配单个字符:

grades='ACAAAABCBCBAADDD'
print(re.findall('AB|AD',grades))
print(re.findall('[A][BD]',grades))

--Outputs:
['AB', 'AD']
['AB', 'AD']

3. Quantifies:数量词

在学习数量词之前,我们首先了解一下贪婪模式和非贪婪模式:

  • 贪婪模式: Match as much text as possible, 尽可能多的匹配字符
  • 非贪婪模式:Match as few text as possible, 尽可能少的匹配字符

在贪婪模式下,如果数量词的规则是匹配0次到5次,有满足可以匹配5次的字符将会被放在一个字符串中,例如我要在findall()的前提下’AAAAA’中匹配A,规则是A在一个字符串中可以连续出现0次到5次,贪婪模式下就会将’AAAAA’放在一个字符串中输出即(‘AAAAA’),而非贪婪模式则相反,他一次只输出一个’A’,并输出五次即(‘A’,‘A’,‘A’,‘A’,‘A’),re模块默认开启贪婪模式,需要非贪婪模式之遥在数量词后面加’?'即可。

数量词往往加在匹配模块的后面,用来表示需要重复多少次,我们先来介绍一般情况下的集中常见的数量词

   (I). e{m,n} :
        e:需要匹配的表达式或者字符
        m:需要匹配的最小次数(inclusive,包含m)
        n:需要匹配的最大次数(inclusive,包含n)
   非贪婪模式e{m,n}?,代表只匹配m次(话说你直接写e{m}不就好了)
   对于e{m,n} ,只有连续出现m-n次范围内的模块会被匹配出来,如果一段字符达到了数量词的上限n,匹配将会结束,见代码第一个例子在这里,m可以等于n,m=n时简写成e{m}

   (注意:在re模块中,e{m,n}在{}中间不可以添加其他的符号,例如空格,这样会使返回结果为空)

grades='ACAAAABCBCBAADDD'
print(re.findall('[A]{2,10}',grades))
print(re.findall('[A]{2,2}',grades))
print(re.findall('[A]{2, 2}',grades))#在{}中添加了空格
--Outpus:
['AAAA', 'AA']
['AA', 'AA', 'AA']
[] #输出为空

   (II). Asterix,星号(*)
   星号匹配左边的表达式或字符0次或者多次,可以看到输出结果中有很多空值,因为0次也被匹配了。
   非贪婪模式*? ,代表只匹配0次

grades='ACAAAABCBCBAADDD'
print(re.findall('[A]*',grades))
print(re.findall('[A]*?',grades))
--Outputs:
['A', '', 'AAAA', '', '', '', '', '', 'AA', '', '', '', '']
['', 'A', '', '', 'A', '', 'A', '', 'A', '', 'A', '', '', '', '', '', '', 'A', '', 'A', '', '', '', '']

   (注:匹配0次也会把不匹配的以空值形式给出)

   (III). Question mark(?)
   问号匹配左边的表达式或字符0次或1次。
   非贪婪模式??,代表只匹配0次

grades='ACAAAABCBCBAADDD'
print(re.findall('[A]?',grades))
print(re.findall('[A]??',grades))
--Outputs:
['A', '', 'A', 'A', 'A', 'A', '', '', '', '', '', 'A', 'A', '', '', '', '']
['', 'A', '', '', 'A', '', 'A', '', 'A', '', 'A', '', '', '', '', '', '', 'A', '', 'A', '', '', '', '']

   (IV). Plus mark(+)
   +号匹配1次或多次
   非贪婪模式+?,代表只匹配1次

grades='ACAAAABCBCBAADDD'
print(re.findall('[A]+',grades))
print(re.findall('[A]+?',grades))
--Outputs:
['A', 'AAAA', 'AA']
['A', 'A', 'A', 'A', 'A', 'A', 'A']

4. 特殊字符集

在Regex中,常用一些字符集来代替具体的字符进行匹配,主要有以下几种:

  • \d: 匹配任何十进制数字,等价于[0-9]
  • \D: 匹配任何非十进制数字,等价于[^0-9]
  • \w: 匹配全部字母、数字、下划线,等价于[A-Za-z0-9_]
  • \W: 匹配全部非字母、数字、下划线,等价于[^A-Za-z0-9_]
  • \s: 匹配空白字符,(依次是空格、缩进tab、换行、回车、换页)等价于[ \t\n\r\f]
  • \S: 匹配非空白字符,等价于[^ \t\n\r\f]
  • \b: 匹配单词边界,比如 ‘er\b’ 可以匹配"never" 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
  • \B: 匹配非单词边界,‘er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
    很明显,所有大写符号都是小写符号的取非运算,记忆量直接减半。

通过以下几个例子来熟悉特殊字符集,这里提前用到了反斜号’',将在下文解释:

#注:wiki文本在上文已倒入
re.findall('[\w]*\[edit\]',wiki)
re.findall('[ \w]*\[edit\]',wiki)
re.findall('[\w ]*\[edit\]',wiki)
for title in re.findall("[\w ]*\[edit\]",wiki):
    print(re.split('\[',title)[0])
    
--Outputs:
['Overview[edit]', 'records[edit]', 'records[edit]']
['Overview[edit]',
 'Access to public records[edit]',
 'Student medical records[edit]']

Overview
Access to public records
Student medical records

5. 其他特殊符号及组合式
我们已经介绍了:管道符’|‘, caret’^‘, dollar’$‘, squared brackets’[]‘, 花括号’{}’
还有几个特殊符号值的关注:

  • 圆括号parenthesis’()’ :
    圆括号主要用来分组,我们对匹配成功的结果感兴趣时可以用圆括号提取出来,在下文group()和groupdict()中会展现
re.findall('[\w ]*\[edit\]',wiki)

--Outputs:
[('Overview', '[edit]'), ('records', '[edit]'), ('records', '[edit]')]
  • 反斜杠’\':
    反斜是转意符号,将特殊字符进行转义,使其成为普通字符,比如\? \. \*,就成了普通的 ?. *

  • 点号’.':
    在字符匹配中最万能的匹配符号,可以匹配所有的单个字符(.*所以就是全匹配了)

print(re.findall('.*',grades))

--Outputs:
['ACAAAABCBCBAADDD', '']

在匹配时我们可能并不需要输出或储存某些内容,这时候可以用到一些特殊符号的组合来进行筛选:

  • 问号冒号组合式’?:':
    (?:pattern)表示匹配pattern,但匹配结果并不会被储存
pattern='([\w]*)(?: )([\w]*)'
text='George Washington'
re.findall(pattern,text)

--Outputs:
[('George', 'Washington')]
  • 问号等号组合式’?=’
    (?=pattern)表示查找pattern,但匹配pattern左边的表达式
pattern='([\w]*)(?= )([\w]*)'
text='George Washington'
re.findall(pattern,text)

--Outputs:
[('George', ''), ('', '')]
  • 问号小于号等号组合式’?<=’
    (?=pattern)表示查找文本是不是pattern开头

  • 问号惊叹号组合’?!’
    (?!pattern)表示查询紧跟的内容是不是pattern

  • 问号小于号惊叹号等号组合’?<!=’
    (?<!=pattern)表示查询文本是不是不是pattern开头


四、替换(.sub())

语法为re.sub(pattern, repl, string, count, flags)
pattern为需要匹配的模块,repl是replace的缩写,代表替换的值,string为需要操作的文本对象,count为替换的最大次数,flags表示编译时使用的模式,默认为0,例句如下:

str1='*LONDON*2023*/.SPRING*'
str2=re.sub('[*]','#',str1)
str3=re.sub('[0-9/]','',str2)
str4=re.sub('[BMLS]','',str3)

--
Outputs:
'#ONDON##PRING#'

五、分组与迭代器(.gruop() .groups() 和 re.finditer())

通过前面的实验,我们看到,通过re.findall()和re.split()会返回string的list,而re.match()和re.search()会返回re.Match object注意与函数区分),接下来,我们将会介绍如何查看re.match()的内容,并介绍如何产生一个元素都是re.Match的list。

首先解决第一个问题,如何查看re.Match:

print(re.search('[ed]it',wiki)) #直接查看re.Match object

--Outputs:
<re.Match object; span=(10, 13), match='dit'>

我们可以在这里看到match=‘dit’,那么match的结果就是dit,span即位置区间就是10-13,我们回去检验一下wiki text,可以看到这三个字符确实在10、11、12三个位置第一次出现:
在这里插入图片描述
了解了这些,有没有简单的办法直接输出值呢,用.group()就可以输出re.Match object 中match的值,返回类型是字符
.group()作为一个函数,实际上是有参数的,记得上面讲过在re中圆括号()可以用来分组,在re.Match() object中也有体现,圆括号会把字段分成若干组,而.group()可以选定输出第几组的内容,默认值为0,代表输出所有组内容,如果是.group(1)则代表输出第一组,看下以下示例便能一目了然:

print(re.search('([\w]*)(\[edit\])',wiki).group())
print(re.search('([\w]*)(\[edit\])',wiki).group(1))
print(re.search('([\w]*)(\[edit\])',wiki).group(2))

--Outputs:
Overview[edit]
Overview
[edit]

当然也可以用.groups(),他会把所有的分组的内容以tuple类型返回且仅返回分组的内容,这是与group最大的区别!一定要注意圆括号的分组groups()和group()的选择。

print(re.search('([\w]*)(\[edit\])',wiki).groups())
print(re.search('[\w]*\[edit\]',wiki).groups())

--Outputs:
('Overview', '[edit]')
()

接下来解决第二个问题,如何产生一个re.Match object的list:

为什么要产生一个这样的list?因为我们对全文搜索的时候希望把所有匹配规则的文本都找出来,re.search()又只能找第一个,找到所有的re.Match object并生成迭代器可以使我们高效的分析文本,这里我们介绍re.finditer()功能来生成一个re.Match object的list或者说是迭代器,并来查看一下迭代器里面的内容吧:

print(re.finditer("([\w]*)(\[edit\])",wiki))
for i in re.finditer("([\w]*)(\[edit\])",wiki):
    print(i)
    
--Outputs:
<callable_iterator object at 0x7fe0eeb7e9b0>
<re.Match object; span=(0, 14), match='Overview[edit]'>
<re.Match object; span=(2732, 2745), match='records[edit]'>
<re.Match object; span=(3708, 3721), match='records[edit]'>

我们可以很清楚的看到,这个迭代器就是由三个re.Match object所形成的,记录了所有匹配规则的文本的值和位置,再利用.group()的技巧,就可以查看所有值了:

for item in re.finditer("([\w]*)(\[edit\])",wiki):
    print(item.group())
print('\n')

for item in re.finditer("([\w]*)(\[edit\])",wiki):
    print(item.group(1))
print('\n')

for item in re.finditer("([\w]*)(\[edit\])",wiki):
    print(item.groups()[0])
--Outputs:
Overview[edit]
records[edit]
records[edit]

Overview
records
records

Overview
records
records

六、 用re.finditer()创建dict

re.finditer()的另一个用途也很强大就是创建字典,我们先来了解一下一个语句:
(?P<name>pattern): 在这个语句中圆括号表示分组,?P表示这是一个基础Regex的拓展,在这里可以理解成我们要创建dict的key值,后面pattern就是匹配规则了,看一下以下示例:

re.finditer("(?P<title>[\w]*)(?P<edit_link>\[edit\])",wiki)

这样,我们就生成了一个re.Match object 的迭代器,用圆括号进行了分组,为创建字典设置了两个key:title和edit_link,对应的值就是匹配上的内容。接下来就是用.groupdict()查看对应的值就可以了:

for item in re.finditer("(?P<title>[\w]*)(?P<edit_link>\[edit\])",wiki):
    print(item.groupdict())
--Outputs:
{'title': 'Overview', 'edit_link': '[edit]'}
{'title': 'records', 'edit_link': '[edit]'}
{'title': 'records', 'edit_link': '[edit]'}

如果我们想要匹配字段但不想要将他们展现出来,可以用(?=pattern)的方法取pattern前的内容:

for item in re.finditer("(?P<title>[\w ]+)(?=\[edit\])",wiki):
    print(item.groups())
--Outputs:
('Overview',)
('Access to public records',)
('Student medical records',)


七、 详细模式 VERBOSE MODE

本周笔记最后一张讲到re.VERBOSE匹配,在这里你可以制定详细的规则,用来匹配你想要的字段:
先定一个pattern,用pattern=“”" “”" 来定义,可以换行,需要分组,可以添加#注释,例如我们想获得美国某地的大学名名称,大学所在的城市和大学所在的州,我们先看看文本是什么样的:

with open("datasets/buddhist.txt","r") as file:
    # we'll read that into a variable called wiki
    wiki=file.read()

在这里插入图片描述
比如我们要匹配红字的内容,可以发现,先是学校名,再是地区名,再是州名,那么我们用以下的pattern规则:

pattern="""
(?P<title>.*)        #the university title
(–\ located\ in\ )   #an indicator of the location
(?P<city>\w*)        #city the university is in
(,\ )                #separator for the state
(?P<state>\w*)       #the state the city is located in"""

并在re.finditer()中添加参数re.VERBOSE:

for item in re.finditer(pattern,wiki,re.VERBOSE):
    # We can get the dictionary returned for the item with .groupdict()
    print(item.groupdict())
--Outputs:
{'title': 'Dhammakaya Open University ', 'city': 'Azusa', 'state': 'California'}
{'title': 'Dharmakirti College ', 'city': 'Tucson', 'state': 'Arizona'}
{'title': 'Dharma Realm Buddhist University ', 'city': 'Ukiah', 'state': 'California'}
{'title': 'Ewam Buddhist Institute ', 'city': 'Arlee', 'state': 'Montana'}
{'title': 'Institute of Buddhist Studies ', 'city': 'Berkeley', 'state': 'California'}
{'title': 'Maitripa College ', 'city': 'Portland', 'state': 'Oregon'}
{'title': 'University of the West ', 'city': 'Rosemead', 'state': 'California'}
{'title': 'Won Institute of Graduate Studies ', 'city': 'Glenside', 'state': 'Pennsylvania'}

下周预告

Pandas Toolkits(Series+Pandas)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值