正则表达式
正则表达式的基本知识
正则表达式是一种描述字符串结果的语法规则,是一个特定的格式化模式,可以匹配、替换、截取匹配的字符串。常用的语言基本上都有正则表达式,如JavaScript、java等。其实,只有了解一种语言的正则使用,其他语言的正则使用起来,就相对简单些。
行定位符(^与$)
行定位符是用来描述字符串的边界。“$”
表示行结尾“^”
表示行开始如"^de"
,表示以de开头的字符串 "de$"
,表示以de结尾的字符串。
单词定界符
我们在查找的一个单词的时候,如an是否在一个字符串”gril and body”中存在,很明显如果匹配的话,an肯定是可以匹配字符串“gril and body”匹配到,怎样才能让其匹配单词,而不是单词的一部分呢?这时候,我们可以是哟个单词定界符\b。
\ban\b 去匹配”gril and body”的话,就会提示匹配不到。
当然还有一个大写的\B,它的意思,和\b正好相反,它匹配的字符串不能使一个完整的单词,而是其他单词或字符串中的一部分。如\Ban\B。
选择字符(|) ,表示或
选择字符表示或的意思。如Aa|aA,表示Aa或者是aA的意思。注意使用”[]”与”|”的区别,在于”[]”只能匹配单个字符,而”|”可以匹配任意长度的字符串。在使用”[]”的时候,往往配合连接字符”-“一起使用,如[a-d],代表a或b或c或d。
排除字符,排除操作
正则表达式提供了””来表示排除不符合的字符,一般放在[]中。如[^1-5],该字符不是1~5之间的数字。
限定符(?*+{n,m})
限定符主要是用来限定每个字符串出现的次数。
限定字符 | 含义 |
---|---|
? | 零次或一次 |
* | 零次或多次 |
+ | 一次或多次 |
n次 | |
至少n次 | |
n到m次 |
如(D+)表示一个或多个D
点号操作符
匹配任意一个字符(不包含换行符)
表达式中的反斜杠(\
)
表达式中的反斜杠有多重意义,如转义、指定预定义的字符集、定义断言、显示不打印的字符。
转义字符
转义字符主要是将一些特殊字符转为普通字符。而这些常用特殊字符有”.”,”?”、”\”等。
字符 | 含义 |
---|---|
\d | 任意一个十进制数字[0-9] |
\D | 任意一个非十进制数字 |
\s | 任意一个空白字符(空格、换行符、换页符、回车符、字表符) |
\S | 任意一个非空白字符 |
\w | 任意一个单词字符 |
\W | 任意个非单词字符 |
###显示不可打印的字符
字符 | 含义 |
---|---|
\a | 报警 |
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车 |
\t | 字表符 |
括号字符()
在正则表达式中小括号的作用主要有:
- 改变限定符如(|、* 、^)的作用范围
如(my|your)baby,如果没有”()”,|将匹配的是要么是my,要么是yourbaby,有了小括号,匹配的就是mybaby或yourbaby。 - 进行分组,便于反向引用
反向引用
反向引用,就是依靠子表达式的”记忆”功能,匹配连续出现的字串或是字符。如(dqs)(pps)\1\2,表示匹配字符串dqsppsdqspps。在下面详细展开学习反向引用。
模式修饰符
模式修饰符的作用是设定模式,也就是正则表达式如何解释。php中主要模式如下表:
修饰符 | 说明 |
---|---|
i | 忽略大小写 |
m | 多行文本模式 |
s | 单行文本模式 |
x | 忽略空白字符 |
正则表达式在php中应用
php中字符串匹配
所谓的字符串匹配,言外之意就是判断一个字符串中,是否包含或是等于另一个字符串。如果不使用正则,我们可以使用php中提供了很多方法进行这样的判断。
不使用正则匹配
-
strstr函数
string strstr ( string haystack,mixedneedle [, bool $before_needle = false ])
- 注1:haystack是当事字符串,needle是被查找的字符串。该函数区分大小写。
- 注2:返回值是从needle开始到最后。
- 注3:关于$needle,如果不是字符串,被当作整形来作为字符的序号来使用。
- 注4:before_needle若为true,则返回前东西。
-
stristr函数与strstr函数相同,只是它不区分大小写
-
strpo函数
int strpos ( string haystack,mixedneedle [, int $offset = 0 ] )
注1:可选的 offset 参数可以用来指定从 haystack 中的哪一个字符开始查找。返回的数字位置是相对于 haystack 的起始位置而言的。 -
stripos -查找字符串首次出现的位置(不区分大小定)
-
strrpos -计算指定字符串在目标字符串中最后一次出现的位置
-
strripos -计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)
使用正则进行匹配
在php中,提供了preg_math()和preg_match_all函数进行正则匹配。关于这两个函数原型如下:
int preg_match|preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
搜索subject与pattern给定的正则表达式的一个匹配.
pattern:要搜索的模式,字符串类型。
subject :输入字符串。
matches:如果提供了参数matches,它将被填充为搜索结果。 matches[0]将包含完整模式匹配到的文本,matches[1]将包含第一个捕获子组匹配到的文本,以此类推。
flags:flags可以被设置为以下标记值:PREG_OFFSET_CAPTURE 如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。 注意:这会改变填充到matches参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject中的偏移量。
offset:通常,搜索从目标字符串的开始位置开始。可选参数 offset 用于 指定从目标字符串的某个未知开始搜索(单位是字节)。
返回值:preg_match()返回 pattern 的匹配次数。 它的值将是0次(不匹配)或1次,因为 preg_match()在第一次匹配后 将会停止搜索。 preg_match_all()不同于此,它会一直搜索subject直到到达结尾。 如果发生错误 preg_match()返回 FALSE。
实例
- 实例1
判断字符串”http://blog.csdn.net/“中是否包含csdn?
解法一(不适用正则):
如果不适用正则,我们使用strstr或者strpos中任意一个都可以,在此,我将使用strstr函数,代码如下:
$str='http://blog.csdn.net/';
function checkStr1($str,$str2)
{
return strstr1($str,$str2)?true:false;
}
echo checkStr($str,'csdn');
解法二:使用正则
因为我们只需要判断是否存在即可,所以选择preg_match。
$str='http://blog.csdn.net/';
$pattern='/csdn/';
function checkStr2($str,$str2)
{
return preg_match($str2,$str)?true:false;
}
echo checkStr2($str,$pattern);
- 实例2(考察单词定界符)
判断字符串”I am a good boy”中是否包含单词go
首先判断是单词,而不是字符串,因此比较的时候,需要比较是否包含’ go ‘,即在字符串go前后有一个空格。
解析:如果使用非正则比较,只需要调用上面的checkStr1()函数即可,注意,第二个参数前后要加一个空格,即’ go ‘。如果使用正则,
我们可以考虑使用单词定界符\b,那么$pattern=’/\bgo\b/’;然后调用checkStr2函数即可. - 例3(考察反向引用)
判断字符串”I am a good boy”中是否包含3个相同的字母
解析:此时,如果我们不使用正则,将会很难判断,因为字母太多了,我们不可能去将所有字母分别与该字符串比较,那样工作量也比较大。这时候涉及到了正在的反向引用。在php正则表达式中,通过\n,来表示第n次匹配到的结果。如\5代表第五次匹配到的结果。那么本题的$pattern='/(\w).*\1.*\1/';
主要注意的是,在使用反向匹配的时候都需要使用(),反向匹配时,匹配()里面出现的字符或字符串。
php中字符串替换
不使用正则
php中当替换字符串的时候,如果不适用正则,我们通常使用substr、mb_substr、str_replace、substr_replace关于这几个函数区别如下表。
函数符 | 功能 | 描述 |
---|---|---|
str_replace(find,replace,string,count) | 使用一个字符串替换字符串中的另一些字符。 | find 必需。规定要查找的值。replace 必需。规定替换 find 中的值的值。string 必需。规定被搜索的字符串。count 可选。一个变量,对替换数进行计数。 |
substr_replace(string,replacement,start,length) | 把字符串的一部分替换为另一个字符串。适合用于替换自定位置的字符串。 | string 必需。规定要检查的字符串。replacement 必需。规定要插入的字符串。start 必需。规定在字符串的何处开始替换。 |
使用正则
如果使用正则替换,php中提供了preg_replace _callback和preg_replace 函数,preg_replace 原型如下:
mixed preg_replace ( mixed pattern,mixedreplacement , mixed subject[,intlimit = -1 [, int &count]])函数功能描述:在字符串subject中,查找pattern,然后使用replacement 去替换,如果有limit则代表限制替换limit次。pregreplacecallback与pregreplace功能相识,不同的是pregreplaceback使用一个回调函数callback来代替replacement.−例1将字符串”hello,中国”中的hello替换为′你好′;如果不是用正则:str=’hello,中国’;
str=strreplace(′hello′,′你好′,str)
或是使用str=substrreplace(str,’你好’,0,5)
使用正则
pattern=′/hello/′;str=preg_replace (pattern,′你好′,str);
- 例2
去除字符串”gawwenngeeojjgegop”中连续相同的字母
$str='gawwenngeeojjgegop';
$pattern='/(.)\1/';
$str=preg_replace($pattern,'',$str);
解析:当然这样可能会遇到,当第一次去除了重复了字符串后,又出来重复的字符串。如字符串味’gewwenngeeojjgegop’,针对这中问题,当然,这样的话,通过判断,继续替换下去。
- 例3
将字符串中”age13gegep3iorji65k65k”;中出现的连续两个数字改为第二个数字,如字符串中13被改为3
$str='age13gegep3iorji65k65k';
$pattern='/(\d)(\d)/';
$str=preg_replace($pattern,'$2', $str);
解析:$n在正则表达式外使用反向引用。n代表第几次匹配到的结果。
php中字符串分割
不用正则
php提供了explode函数去分割字符串,与其对应的是implode。关于explode原型如下:
array explode ( string delimiter,stringstring [, int $limit ] )
delimiter:边界上的分隔字符。
string:输入的字符串。
limit:如果设置了 limit 参数并且是正数,则返回的数组包含最多 limit 个元素,而最后那个元素将包含 string 的剩余部分。如果 limit 参数是负数,则返回除了最后的 -limit 个元素外的所有元素。如果 limit 是 0,则会被当做 1。
使用正则
关于通过正则表达式进行字符串分割,php提供了split、preg_split 函数。preg_split() 函数,通常是比 split() 更快的替代方案。
array preg_split ( string pattern,stringsubject [, int limit=−1[,intflags = 0 ]] )
- 例题
将字符串 ‘http://blog.csdn.net/123/456‘按照’/’进行分割
解法一:
$str='http://blog.csdn.net/123/456';
$str=explode('/', $str);
解法二:
$str='http://blog.csdn.net/123/456';
$pattern='/\//'; /*因为/为特殊字符,需要转移*/
$str=preg_split ($pattern, $str);
php中贪婪匹配与惰性匹配
- 贪婪匹配:就是匹配尽可能多的字符。
比如,正则表达式中m.*n,它将匹配最长以m开始,n结尾的字符串。如果用它来搜索manmpndegenc的话,它将匹配到的字符串是manmpndegen而非man。可以这样想,当匹配到m的时候,它将从后面往前匹配字符n。 - 懒惰匹配:就是匹配尽可能少的字符。
有的时候,我们需要并不是去贪婪匹配,而是尽可能少的去匹配。这时候,就需要将其转为惰性匹配。怎样将一个贪婪匹配转为惰性匹配呢?只需要在其后面添加一个”?”即可。如m.*?n将匹配manmpndegenc,匹配到的字符串是man。
函数符 | 描述 |
---|---|
*? | 零次或多次,但尽可能少的匹配 |
+? | 一次或多次,但尽可能少的匹配 |
?? | 0次或1次,但尽可能少的匹配 |
{n,}? | 至少n次,但尽可能少的匹配 |
{n,m}? | n到m次 ,但尽可能少的匹配 |
php正则表达式之回溯与固态分组
回溯
首先我们需要清楚什么是回溯,回溯就像是在走岔路口,当遇到岔路的时候就先在每个路口做一个标记。如果走了死路,就可以照原路返回,直到遇见之前所做过的标记,标记着还未尝试过的道路。如果那条路也走不能,可以继续返回,找到下一个标记,如此重复,直到找到出路,或者直到完成所有没有尝试过的路。首先我们看例题
$str='aageacwgewcaw';
$pattern='/a\w*c/i';
$str=preg_match($pattern, $str);
看到上面的程序,可能都清楚是什么意思,就是匹配$str是否包含这样一个由”a+0个或多个字母+c”不区分大小写的字符串。但是至于程序怎样去匹配的呢?匹配的过程中,回溯了多少次呢?
匹配过程 | 接下来操作描述 |
---|---|
‘a\w*c’中a匹配到’aageacwgewcaw’中第一个字符a | \w进行下一个字符匹配 |
因为\w是贪婪匹配,会一直匹配到’aageacwgewcaw’中最后一个字符w | c进行下一个字符匹配时 |
‘a\w*c’中c发现没有可以匹配的 | 于是\w匹配进行第一次回溯,匹配到倒数第二个字符a |
‘a\w*c’中c发现还是没有可以匹配的 | 于是\w匹配进行第二次回溯,匹配到倒数第三个字符c |
‘a\w*c’中c匹配成功 | 匹配结束返回结果 |
现在,如果我们将pattern改为pattern=’/a\w*?c/i’;又会回溯多少次呢?正确答案是回溯四次。
固态分组
固态分组,目的就是减少回溯次数, 使用(?>…)括号中的匹配时如果产生了备选状态,那么一旦离开括号便会被立即 引擎抛弃掉。举个典型的例子如: ‘\w+:’这个表达式在进行匹配时的流程是这样的,会优先去匹配所有的符合\w的字符,假如字符串的末尾没有’:’,即匹配没有找到冒号,此时触发回溯机制,他会迫使前面的\w+释放字符,并且在交还的字符中重新尝试与’:’作比对。但是问题出现在这里: \w是不包含冒号的,显然无论如何都不会匹配成功,可是依照回溯机制,引擎还是得硬着头皮往前找,这就是对资源的浪费。所以我们就需要避免这种回溯,对此的方法就是将前面匹配到的内容固化,不令其存储备用状态!,那么引擎就会因为没有备用状态可用而只得结束匹配过程。大大减少回溯的次数。
如下代码,就不会进行回溯:
$str='nihaoaheloo';
$pattern='/(?>\w+):/';
$rs=preg_match($pattern, $str);
当然有的时候,又需慎用固态分组,如下,我要检查$str中是否包含以a结尾的字符串,很明显是包含的,但是因为使用了固态分组,反而达不到我们想要的效果
$str='nihaoahelaa';
$pattern1='/(?>\w+)a/';
$pattern2='/\w+a/';
$rs=preg_match($pattern1, $str);//0
$rs=preg_match($pattern2, $str);//1
php中其他常用字符串操作函数
- 字符串截取截取
string substr ( string string,intstart [, int length])stringmbsubstr(stringstr , int start[,intlength = NULL [, string $encoding = mb_internal_encoding() ]] ) - 字符串中大小写转换
strtoupper
strtolower
ucfirst
ucwords - 字符串比较
-strcmp、strcasecmp、strnatcmp - 字符串过滤
- 字符串翻转
strrev($str); - 字符串随机排序
string str_shuffle ( string $str )
补充
怎样进行邮箱匹配,url匹配,手机匹配
使用preg_match函数进行匹配,以下内容从TP中复制而来。
邮箱验证
pattern=′/\w+([−+.]\w+)∗@\w+([−.]\w+)∗.\w+([−.]\w+)∗/’;
url匹配
pattern='/^http(s?)😕/(?:[A-za-z0-9-]+.)+[A-za-z]{2,4}(:\d+)?(?:[/?#][/=?%-&~`@[]':+!.#\w]*)?/’;
手机验证
pattern=′/1[3458]\d10/’;
php中正则的优缺点
php中正则在某些时候,能帮我们解决php函数很多困难的匹配或是替换。然后php中正则的效率,往往是我们需要考虑的,所以在某些时候,能不用正则还是尽量不去用它,除非,某些场合必须用到,或是我们能够有效减少其回溯次数。
python中的正则方法
Python的re库提供了整个正则表达式的实现,利用re库我们就可以在Python中使用正则表达式来,在Python中写正则表达式几乎都是用的这个库。
match()
在这里首先介绍第一个常用的匹配方法,match()方法,我们向这个方法传入要匹配的字符串以及正则表达式,就可以来检测这个正则表达式是否匹配字符串了。
match()方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果,如果不匹配,那就返回None。
我们用一个实例来感受一下:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
print(len(content))
result =re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}',content)
print(result)
print(result.group())
print(result.span())
运行结果:
41<_sre.SRE_Match object; span=(0, 25), match='Hello 123 4567 World_This'>Hello 123 4567 World_This(0, 25)
在这里我们首先声明了一个字符串,包含英文字母、空白字符、数字等等内容,接下来我们写了一个正则表达式^Hello\s\d\d\d\s\d{4}\s\w{10}
来匹配这个长字符串。
开头的^
是匹配字符串的开头,也就是以Hello
开头,然后\s
匹配空白字符,用来匹配目标字符串的空格,\d
匹配数字,三个\d
匹配123
,然后再写一个\s
匹配空格,后面还有4567
,我们其实可以依然用四个\d
来匹配,但是这么写起来比较繁琐,所以在后面可以跟{4}
代表匹配前面的字符四次,也就是匹配四个数字,这样也可以完成匹配,然后后面再紧接一个空白字符,然后\w{10}
匹配10个字母及下划线,正则表达式到此为止就结束了,我们注意到其实并没有把目标字符串匹配完,不过这样依然可以进行匹配,只不过匹配结果短一点而已。
我们调用match()方法,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
打印输出一下结果,可以看到结果是SRE_Match对象,证明成功匹配,它有两个方法,group()方法可以输出匹配到的内容,结果是Hello 123 4567 World_This
,这恰好是我们正则表达式规则所匹配的内容,span()方法可以输出匹配的范围,结果是(0, 25)
,这个就是匹配到的结果字符串在原字符串中的位置范围。
通过上面的例子我们可以基本了解怎样在Python中怎样使用正则表达式来匹配一段文字。
匹配目标
刚才我们用了match()方法可以得到匹配到的字符串内容,但是如果我们想从字符串中提取一部分内容怎么办呢?就像最前面的实例一样,从一段文本中提取出邮件或电话号等内容。
在这里可以使用()
括号来将我们想提取的子字符串括起来,()
实际上就是标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,我们可以调用group()方法传入分组的索引即可获取提取的结果。
下面我们用一个实例感受一下:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
依然是前面的字符串,在这里我们想匹配这个字符串并且把其中的1234567
提取出来,在这里我们将数字部分的正则表达式用()
括起来,然后接下来调用了group(1)获取匹配结果。
运行结果如下:
<_sre.SRE_Match object; span=(0, 19), match='Hello 1234567 World'>Hello 1234567 World1234567(0, 19)
可以看到在结果中成功得到了1234567
,我们获取用的是group(1),与group()有所不同,group()会输出完整的匹配结果,而group(1)会输出第2个被()
包围的匹配结果,假如正则表达式后面还有()
包括的内容,那么我们可以依次用group(2)、group(3)等来依次获取。
通用匹配
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写\s
匹配空白字符,出现数字我们就写\d
匹配数字,工作量非常大,其实完全没必要这么做,还有一个万能匹配可以用,也就是.*
,.
可以匹配任意字符(除换行符),*
又代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意的字符了,有了它我们就不用挨个字符地匹配了。
所以接着上面的例子,我们可以改写一下正则表达式。
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$', content)
print(result)
print(result.group())
print(result.span())
在这里我们将中间的部分直接省略,全部用.*
来代替,最后加一个结尾字符串就好了,运行结果如下:
<_sre.SRE_Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'>Hello 123 4567 World_This is a Regex Demo(0, 41)
可以看到group()方法输出了匹配的全部字符串,也就是说我们写的正则表达式匹配到了目标字符串的全部内容,span()方法输出(0, 41)
,是整个字符串的长度。
因此,我们可以在使用.*
来简化正则表达式的书写。
贪婪匹配与非贪婪匹配
在使用上面的通用匹配.*
的时候可能我们有时候匹配到的并不是想要的结果,我们看下面的例子:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
在这里我们依然是想获取中间的数字,所以中间我们依然写的是(\d+)
,数字两侧由于内容比较杂乱,所以两侧我们想省略来写,都写.*
,最后组成^He.*(\d+).*Demo$
,看样子并没有什么问题,我们看下运行结果:
<_sre.SRE_Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>7
奇怪的事情发生了,我们只得到了7这个数字,这是怎么回事?
这里就涉及一个贪婪匹配与非贪婪匹配的原因了,贪婪匹配下,.*
会匹配尽可能多的字符,我们的正则表达式中.*
后面是\d+
,也就是至少一个数字,并没有指定具体多少个数字,所以.*
就尽可能匹配多的字符,所以它把123456
也匹配了,给\d+
留下一个可满足条件的数字7
,所以\d+
得到的内容就只有数字7了。
但这样很明显会给我们的匹配带来很大的不便,有时候匹配结果会莫名其妙少了一部分内容。其实这里我们只需要使用非贪婪匹配匹配就好了,非贪婪匹配的写法是.*?
,多了一个?
,那么它可以达到怎样的效果?我们再用一个实例感受一下:
import recontent = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))
在这里我们只是将第一个.*
改成了.*?
,转变为非贪婪匹配匹配。结果如下:
<_sre.SRE_Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>1234567
很好,这下我们就可以成功获取1234567
了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符,.*?
之后是\d+
用来匹配数字,当.*?
匹配到Hello
后面的空白字符的时候,再往后的字符就是数字了,而\d+
恰好可以匹配,那么这里.*?
就不再进行匹配,交给\d+
去匹配后面的数字。所以这样,.*?
匹配了尽可能少的字符,\d+
的结果就是1234567
了。
所以说,在做匹配的时候,字符串中间我们可以尽量使用非贪婪匹配来匹配,也就是用.*?
来代替.*
,以免出现匹配结果缺失的情况。
但这里注意,如果匹配的结果在字符串结尾,.*?
就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符,例如:
import re
content = 'http://weibo.com/comment/kEraCN'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content)
print('result1', result1.group(1))
print('result2', result2.group(1))
运行结果:
result1 result2 kEraCN
观察到.*?
没有匹配到任何结果,而.*
则尽量匹配多的内容,成功得到了匹配结果。
所以在这里好好体会一下贪婪匹配和非贪婪匹配的原理,对后面写正则表达式非常有帮助。
修饰符
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。
我们用一个实例先来感受一下:
import re
content = '''Hello 1234567 World_Thisis a Regex Demo'''
result = re.match('^He.*?(\d+).*?Demo$', content)
print(result.group(1))
和上面的例子相仿,我们在字符串中加了个换行符,正则表达式也是一样的来匹配其中的数字,看一下运行结果:
AttributeError Traceback (most recent call last)<ipython-input-18-c7d232b39645> in <module>() 5 ''' 6 result = re.match('^He.*?(\d+).*?Demo$', content)----> 7 print(result.group(1))AttributeError: 'NoneType' object has no attribute 'group'
运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为None,而我们又调用了group()方法所以导致AttributeError。
那我们加了一个换行符为什么就匹配不到了呢?是因为.
匹配的是除换行符之外的任意字符,当遇到换行符时,.*?
就不能匹配了,所以导致匹配失败。
那么在这里我们只需要加一个修饰符re.S
,即可修正这个错误。
result = re.match('^He.*?(\d+).*?Demo$', content, re.S)
在match()方法的第三个参数传入re.S
,它的作用是使.
匹配包括换行符在内的所有字符。
运行结果:
1234567
这个re.S
在网页匹配中会经常用到,因为HTML节点经常会有换行,加上它我们就可以匹配节点与节点之间的换行了。
另外还有一些修饰符,在必要的情况下也可以使用:
修饰符描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。
在网页匹配中较为常用的为re.S、re.I。
#### 转义匹配
我们知道正则表达式定义了许多匹配模式,如.
匹配除换行符以外的任意字符,但是如果目标字符串里面它就包含.
我们改怎么匹配?
那么这里就需要用到转义匹配了,我们用一个实例来感受一下:
import re
content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com', content)
print(result)
当遇到用于正则匹配模式的特殊字符时,我们在前面加反斜线来转义一下就可以匹配了。例如.
我们就可以用\.
来匹配,运行结果:
<_sre.SRE_Match object; span=(0, 17), match='(百度)www.baidu.com'>
可以看到成功匹配到了原字符串。
以上是写正则表达式常用的几个知识点,熟练掌握上面的知识点对后面我们写正则表达式匹配非常有帮助。
search()
我们在前面提到过match()方法是从字符串的开头开始匹配,一旦开头不匹配,那么整个匹配就失败了。
我们看下面的例子:
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.match('Hello.*?(\d+).*?Demo', content)
print(result)
在这里我们有一个字符串,它是以Extra
开头的,但是正则表达式我们是以Hello
开头的,整个正则表达式是字符串的一部分,但是这样匹配是失败的,也就是说只要第一个字符不匹配整个匹配就不能成功,运行结果如下:
None
所以match()方法在我们在使用的时候需要考虑到开头的内容,所以在做匹配的时候并不那么方便,它适合来检测某个字符串是否符合某个正则表达式的规则。
所以在这里就有另外一个方法search(),它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果,也就是说,正则表达式可以是字符串的一部分,在匹配时,search()方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,那就返回None。
我们把上面的代码中的match()方法修改成search(),再看下运行结果:
<_sre.SRE_Match object; span=(13, 53), match='Hello 1234567 World_This is a Regex Demo'>1234567
这样就得到了匹配结果。
所以说,为了匹配方便,我们可以尽量使用search()方法。
下面我们再用几个实例来感受一下search()方法的用法。
首先这里有一段待匹配的HTML文本,我们接下来写几个正则表达式实例来实现相应信息的提取。
html =
'''<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction"> 经典老歌列表 </p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐">沧海一声笑</a> </li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦">往事随风</a> </li>
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li> <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li> <li data-view="5">
<a href="/6.mp3" singer="邓丽君"><i class="fa fa-user"></i>但愿人长久</a> </li> </ul></div>'''
观察到<ul>
节点里面有许多<li>
节点,其中<li>
节点有的包含<a>
节点,有的不包含<a>
节点,<a>
节点还有一些相应的属性,超链接和歌手名。
首先我们尝试提取class为active的<li>
节点内部的超链接包含的歌手名和歌名。
所以我们需要提取第三个<li>
节点下的<a>
节点的singer属性和文本。
所以正则表达式可以以<li>
开头,然后接下来寻找一个标志符active
,中间的部分可以用.*?
来匹配,然后接下来我们要提取singer这个属性值,所以还需要写入singer="(.*?)"
,我们需要提取的部分用小括号括起来,以便于用group()方法提取出来,它的两侧边界是双引号,然后接下来还需要匹配<a>
节点的文本,那么它的左边界是>
,右边界是</a>
,所以我们指定一下左右边界,然后目标内容依然用(.*?)
来匹配,所以最后的正则表达式就变成了<li.*?active.*?singer="(.*?)">(.*?)</a>'
,然后我们再调用search()方法,它便会搜索整个HTML文本,找到符合正则表达式的第一个内容返回。
另外由于代码有换行,所以这里第三个参数需要传入re.S
所以整个匹配代码如下:
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)if result: print(result.group(1), result.group(2))
由于我们需要获取的歌手和歌名都已经用了小括号包围,所以可以用group()方法获取,序号依次对应group()的参数。
运行结果:
齐秦 往事随风
可以看到这个正是我们想提取的class为active的<li>
节点内部的超链接包含的歌手名和歌名。
那么正则表达式不加active会怎样呢?也就是匹配不带class为active的节点内容,我们将正则表达式中的active去掉,代码改写如下:
result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html, re.S)if result: print(result.group(1), result.group(2))
由于search()方法会返回第一个符合条件的匹配目标,那在这里结果就变了。
运行结果如下:
任贤齐 沧海一声笑
因为我们把active标签去掉之后,从字符串开头开始搜索,符合条件的节点就变成了第二个<li>
节点,后面的就不再进行匹配,所以运行结果自然就变成了第二个<li>
节点中的内容。
注意在上面两次匹配中,search()方法的第三个参数我们都加了re.S
,使得.*?
可以匹配换行,所以含有换行的<li>
节点被匹配到了,如果我们将其去掉,结果会是什么?
result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html)if result: print(result.group(1), result.group(2))
运行结果:
beyond 光辉岁月
可以看到结果就变成了第四个<li>
节点的内容,这是因为第二个和第三个<li>
标签都包含了换行符,去掉re.S
之后,.*?
已经不能匹配换行符,所以正则表达式不会匹配到第二个和第三个<li>
节点,而第四个<li>
节点中不包含换行符,所以成功匹配。
由于绝大部分的HTML文本都包含了换行符,所以通过上面的例子,我们尽量都需要加上re.S
修饰符,以免出现匹配不到的问题。
findall()
在前面我们说了search()方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果我们想要获取满足匹配正则表达式的所有内容的话怎么办?这时就需要借助于findall()方法了。
findall()方法会搜索整个字符串然后返回匹配正则表达式的所有内容。
还是上面的HTML文本,如果我们想获取所有<a>
节点的超链接、歌手和歌名,就可以将search()方法换成findall()方法。如果有返回结果的话就是list类型,所以我们需要遍历一下list来获依次获取每组内容。
import re
results = re.findall('<li.*?href="https://ask.hellobi.com/(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
print(results)
print(type(results))
for result in results:
print(result)
print(result[0], result[1], result[2])
运行结果:
[('/2.mp3', '任贤齐', '沧海一声笑'), ('/3.mp3', '齐秦', '往事随风'), ('/4.mp3', 'beyond', '光辉岁月'), ('/5.mp3', '陈慧琳', '记事本'), ('/6.mp3', '邓丽君', '但愿人长久')]<class 'list'>('/2.mp3', '任贤齐', '沧海一声笑')/2.mp3 任贤齐 沧海一声笑('/3.mp3', '齐秦', '往事随风')/3.mp3 齐秦 往事随风('/4.mp3', 'beyond', '光辉岁月')/4.mp3 beyond 光辉岁月('/5.mp3', '陈慧琳', '记事本')/5.mp3 陈慧琳 记事本('/6.mp3', '邓丽君', '但愿人长久')/6.mp3 邓丽君 但愿人长久
可以看到,返回的list的每个元素都是tuple类型,我们用对应的索引依次取出即可。
所以,如果只是获取第一个内容,可以用search()方法,当需要提取多个内容时,就可以用findall()方法。
sub()
正则表达式除了提取信息,我们有时候还需要借助于它来修改文本,比如我们想要把一串文本中的所有数字都去掉,如果我们只用字符串的replace()方法那就太繁琐了,在这里我们就可以借助于sub()方法。
我们用一个实例来感受一下:
import re
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)
运行结果:
aKyroiRixLg
在这里我们只需要在第一个参数传入\d+
来匹配所有的数字,然后第二个参数是替换成的字符串,要去掉的话就可以赋值为空,第三个参数就是原字符串。
得到的结果就是替换修改之后的内容。
那么在上面的HTML文本中,如果我们想正则获取所有<li>
节点的歌名,如果直接用正则表达式来提取可能比较繁琐,比如可以写成这样子:
results = re.findall('<li.*?>\s*?(<a.*?>)?(\w+)(</a>)?\s*?</li>', html, re.S)
for result in results:
print(result[1])
运行结果:
一路上有你沧海一声笑往事随风光辉岁月记事本但愿人长久
但如果我们借助于sub()函数就比较简单了,我们可以先用sub()函数将<a>
节点去掉,只留下文本,然后再利用findall()提取就好了。
html = re.sub('<a.*?>|</a>', '', html)
print(html)
results = re.findall('<li.*?>(.*?)</li>', html, re.S)
for result in results:
print(result.strip())
运行结果:
<div id="songs-list"> <h2 class="title">经典老歌</h2> <p class="introduction"> 经典老歌列表 </p> <ul id="list" class="list-group"> <li data-view="2">一路上有你</li> <li data-view="7"> 沧海一声笑 </li> <li data-view="4" class="active"> 往事随风 </li> <li data-view="6">光辉岁月</li> <li data-view="5">记事本</li> <li data-view="5"> 但愿人长久 </li> </ul></div>一路上有你沧海一声笑往事随风光辉岁月记事本但愿人长久
可以到<a>
标签在经过sub()函数处理后都没有了,然后再findall()直接提取即可。所以在适当的时候我们可以借助于sub()方法做一些相应处理可以事半功倍。
compile()
前面我们所讲的方法都是用来处理字符串的方法,最后再介绍一个compile()方法,这个方法可以将正则字符串编译成正则表达式对象,以便于在后面的匹配中复用。
import re
content1 = '2016-12-15 12:00'
content2 = '2016-12-17 12:55'
content3 = '2016-12-22 13:21'
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern, '', content1)
result2 = re.sub(pattern, '', content2)
result3 = re.sub(pattern, '', content3)
print(result1, result2, result3)
例如这里有三个日期,我们想分别将三个日期中的时间去掉,所以在这里我们可以借助于sub()方法,sub()方法的第一个参数是正则表达式,但是这里我们没有必要重复写三个同样的正则表达式,所以可以借助于compile()函数将正则表达式编译成一个正则表达式对象,以便复用。
运行结果:
2016-12-15 2016-12-17 2016-12-22
另外compile()还可以传入修饰符,例如re.S
等修饰符,这样在search()、findall()等方法中就不需要额外传了。所以compile()方法可以说是给正则表达式做了一层封装,以便于我们更好地复用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)