Python爬虫:正则表达式?就这

435 阅读5分钟

前言

当你点开文章的时候,我就知道这次的标题有点装逼了,哈哈,不过不要紧,还好我写的都是干货。

正则表达式是处理字符串的强大工具,它有自己特定的语法结构,可以实现字符串的检索、替换、匹配验证。

案例引入

打开开源中国提供的正则表达式测试工具https://tool.oschina.net/regex/,输入带匹配的文本,然后选择常用的正则表达式,就可以得到相应的匹配结果。

运行界面

其实,这里就是使用的正则表达式匹配,也就是用一定的规则将特定的文本提取出来。

对于电子邮件来说可以使用

[\w!#%&'*+/=?^_`{|}~-]+(?:\.[\w!#%&'+/=?^_`{|}~-]+)@(?:\w?.)+\w?

将它匹配出来。

上面的一大串字符看起来是不是一团糟,现在我就将常用的匹配规则列举出来。

模式描述
\w匹配字母、数字及下划线
\W匹配不是字母、数字及下划线的字符
\s匹配任意空白字符,相当于{\t\n\r\f}
\S匹配任意非空白字符
\d匹配任意数字,等价于[0-9]
\D匹配任意非数字字符
\A匹配字符串开头
\Z匹配字符串的结尾,如果存在换行,只匹配到换行前的字符串
\z匹配字符串的结尾,如果存在换行,同时还会匹配换行符
\G匹配最后完成匹配的位置
\n匹配换行符
\t匹配制表符
匹配一行字符串的开头
$匹配一行字符串的结尾
.匹配除换行符外的任意字符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]用来表示一组字符单独列出,比如[amk]匹配a,m,k
[^...]不在[]中的字符,比如^abc,表示匹配除了a,b,c之外的字符
*匹配0个或多个表达式
+匹配1个或多个表达式
匹配0个或1个前面正则表达式定义的片段(非贪婪匹配)
{n}精确匹配n个前面的表达式
{n,m}匹配n到m次,由前面正则表达式匹配的片段(贪婪匹配)
a|b匹配a或b
( )匹配括号内的表达式,也表示一个组

看完之后会不会有点晕呢?

不用担心,接下来我在这边会详细的说明这个规则的用法。

其实正则表达式不是Python独有的,它也可以在其他编程语言使用。在Python中使用re这个库,提供了正则表达式的实现,利用这个库,可以在Python中使用正则表达式。

match( )

这里先介绍一个常用的匹配方法——match(),向它传入需要匹配的字符串及正则表达式,就可以检测这个正则表达式是否匹配字符串。

match( )方法是从字符串起始位置匹配正则表达式,如果匹配就返回匹配成功的结果,如果不匹配就返回None。

示例如下

import re


content = 'Hello 123 456 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{3}\s\w{10}', content)
print(result)
print(type(result))
print(result.group())
print(result.span())

这里声明了一个字符串,其中包含英文字符、空白符、数字等。

那么现在就对刚刚写出的正则表达式进行简单分析。

开头的^表示匹配字符串的开头,也就是说以Hello开头;然后\s匹配空白字符;\d表示匹配数字;\d{3}代表前面的规则匹配3次;\w表示匹配数字、字母及下划线;{10}表示前面的规则匹配10次。

你可以试着运行上面的这段代码,你会发现我们并没有将字符串匹配完全,不过依然可以进行匹配,只不过是匹配的结果短一点。

在match( )方法中,第一个参数是正则表达式,第二个参数是传入要匹配的字符串。

打印输出结果可以看到结果是SRE_Match对象,这证明成功匹配。该对象有两个方法:group( )方法可以输出打印内容;span()方法可以输出匹配的范围。

匹配目标

刚刚使用match( )方法可以匹配到字符串的内容,如果想要从字符串中提取一部分内容,可以使用( )括号,将想要提取的子字符串括起来,( )实际上标记了一个子表达式开始和结束的位置,被标记的每个子表达式会依次对应每一个分组,调用group( )方法传入分组的索引可以获取提取的结果。

示例如下

import re


content = 'Hello 123456 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result)
print(type(result))
print(result.group())
print(result.group(1))
print(result.span())

你可以试着编写并运行上面的示例代码你会发现,我们成功的获取到了123456。这里用的是group(1),与group()不同的是,后者获取完整的匹配结果,而前者会输出被( )包围的匹配结果,以后还会使用group(2)、group(3)获取匹配结果。

通用匹配

刚才我们写的正则表达式其实还是比较复杂的,出现空白符就用\s,出现数字就用\d匹配,这样的工作量还是比较大的。其实根本没有必要,可以使用万能匹配,那就是.*,其中 .(点) 可以匹配任意字符(换行符除外),(星号)表示匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。

示例如下

import re


content = 'Hello 123 456 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello.*Demo$', content)
print(result)
print(result.group())
print(result.span())

贪婪与非贪婪

使用上面的通用匹配.*时,可能有时候匹配到的并不是我们想要的结果。看下面的例子:

import re


content = 'Hello 123456 World_This is a Regex Demo'
print(len(content))
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
print(result.span())

通过上面的代码,你会发现匹配的结果是7,但是这个不是不是我们想要的结果。

这里就涉及一个贪婪匹配与与非贪婪匹配。在贪婪模式下会尽可能的匹配多的字符。正则表达式中.*后面是\d+,也就是至少一个数字,并没有指定具体多少个数字。

因此,.*就匹配尽可能多的字符串,把12345都匹配了,留下满足\d的数字。

其实这里只需要使用非贪婪匹配就好了,非贪婪匹配的写法是.*? ,多了个?,可以来看看有什么样的效果。

import re


content = 'Hello 123456 World_This is a Regex Demo'
print(len(content))
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))
print(result.span())

运行上面的代码,你会清楚的看到已经获取到了123456。

非贪婪匹配是尽量匹配少的字符,当匹配到数字的时候就不往下匹配了,那么\d+便刚好可以匹配下去。

但是要注意,如果匹配的结果在字符串的结尾,那么.*?就匹配不到任何结果,因为非贪婪匹配尽可能少的内容。

修饰符

正则表达式可以使用包含可选标志修饰符来控制匹配的模式,修饰符被指定为一个可选的标志。

示例如下:

import re


content = '''Hello 123456 
World_This is a Regex Demo'''
print(len(content))
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))
print(result.span())

运行结果

None
Traceback (most recent call last):
  File "D:/github/Python_scrapy/爬虫笔记+源码/第四章 提取数据/code/demo5.py", line 9, in <module>
    print(result.group(1))
AttributeError: 'NoneType' object has no attribute 'group'

返回值为None,因此导致出现AttributeError这个错误。原因是 .(点) 只能匹配除换行符号外的任意字符。在上面的程序中,你会发现,中间多了个换行符,因此匹配失败。

在这里只需要添加修饰符re.S,即可修正这个错误。

result = re.match('^He.*?(\d+).*Demo$', content, re.S)

这个re.S经常在网页匹配中用到,在HTML中经常会有节点的换行。

下面列举一些常见的修饰符

修饰符描述
re.I使匹配对大小写不敏感
re.L做本地化识别(local-aware)匹配
re.M多行匹配影响^和$
re.U根据Unicode字符集解析字符,这个标志影响\w、\W、\b和\B
re.X该标志通过给予你更灵活的格式使正则表达式写的得更易于理解
re.S匹配包括换行在内的所有字符

转义匹配

我们知道正则表达式中定义了不少的匹配模式,如:匹配换行符以外的其他字符,但是如果目标字符串了面包含 .(点),那该怎么办呢?

这里就需要转义匹配了。

代码示例:

import re

content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com', content)
print(result.group())

运行上面的代码,你会发现成功匹配源字符串。

search( )

前面提过,match( )方法是从字符串的开头处开始匹配的,一旦字符串的开头不匹配,那么整个字符串就失效了。

因为match( )方法进行匹配时需要考虑是否符合从开头位置匹配,这样对我们来说不是特别的方便。

这里有另外一种方法,那就是search( )方法,它在匹配的时候会扫描整个字符串,直到找到符合匹配规则的第一个字符串。

search( )与match( )使用方法相似。

findall( )

前面提到过search( )的使用方法,它可以匹配符合规则的第一个字符串,但是想要匹配符合规则的全部字符串就需要借助findall( )方法。该方法会搜索整个字符串,然后匹配所有符合规则的字符串,用法与search( ) 和match( )相同。

sub( )

除了使用正则表达式匹配字符串之外,还可以使用正则表达式来修改文本,比如想要把一个字符串中的所有数字全部去除,如果使用字符串的replace()方法就会显得很繁琐,这里可以借助sub( )方法,具体代码如下所示:

import re

content = 'sdsd55wee66err33'
result = re.sub('\d+', '', content)
print(result)

运行上面的代码,你会发现,已经将字符串中的所有数字都去除掉了。

compile( )

前面所提过的方法都是用来处理字符串的,现在介绍一下compile( )方法,这个方法可以将正则字符串编译成正则表达式对象。以便在后面的匹配中复用,具体代码如下所示:

import re

content1 = '2020-12-29 02:35'
content2 = '2020-12-30 03:35'
content3 = '2020-12-31 01:35'

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)

运行上面的代码,你会发现时与分都被去掉了,并且匹配规则只写了一次。

同样的compile( )方法也可以传入修饰符re.S、re.I等等,这样在match( )、search( )、findall( )都不需要额外在传。

最后

本次对正则表达式的分享到这里就结束了,你学废了吗?可以在评论区告诉我。

如果你阅读到了这里,那说明本文对你还是有些许帮助的,这也是我写文章的初衷。

路漫漫其修远兮,吾将上下而求索。

我是啃书君,一个专注于学习的人,你懂的越多,你不懂的越多。更多精彩内容,我们下期再见!