Python爬虫第8节-学爬虫必会的正则表达式

目录

前言

一、实例引入

二、match()

三、search()

四、findall()

五、sub()

六、compile()


前言

        这一节,我们来了解正则表达式的用法。正则表达式是处理字符串的好帮手,它有着自己特定的语法。依靠正则表达式,不管是检索字符串、替换字符串,还是做匹配验证,都能轻松完成。尤其对爬虫来说,用它从HTML里提取想要的信息,能省不少事 。 

一、实例引入

        讲了这么多,估计大家对正则表达式具体是啥还不太明白。没关系,下面我们通过几个例子来看看它怎么用。大家打开开源中国的这个正则表达式测试工具,网址是http://tool.oschina.net/regex/ 。打开后,在里面输入要匹配的文本,再选个常用的正则表达式,就能看到对应的匹配结果了。比如说,咱们输入这么一段文本:“Hello, my phone number is 010-86432100 and email is cqc@cuigingcai.com, and my website is https://cuiqingcai.com.”这段文字里有一个电话号码和一个邮箱地址。现在,我们就试试用正则表达式把它们给提取出来。 

在网页右侧选择“匹配国内电话号码”,下方就会显示出文本中的电话号码;

在网页右侧选择“匹配Email地址”,下方就会显示出文本中的E - mail;

选择“匹配网址URL”,下方则会显示出文本中的URL。是不是很神奇?

        实际上,这里用到的是正则表达式匹配,简单来说,就是按照特定的规则把想要的文本找出来。就拿电子邮件举例,它的格式是开头得有一串字符,然后是@符号,最后跟着一个域名。再看URL,它开头是协议类型,接着是冒号和双斜线,结尾是域名加上路径。

        要是想匹配URL,可以用这个正则表达式:[a-zA - z]+://[^\s]* 。用它去匹配字符串的时候,只要字符串里有像URL的内容,就能被筛选出来。别看这个正则表达式看起来乱糟糟的,它其实是有语法规则的。像a - z,意思是能匹配任意小写字母;\s表示可以匹配任意空白字符;*则代表能匹配前面的字符很多次,甚至可以不匹配。这一长串正则表达式就是把这些规则组合在一起形成的。

        等写好了正则表达式,就能用它在长字符串里找符合规则的内容。不管字符串里都有些啥,只要和写的规则一致,都能被找出来。要是想知道网页源代码里有多少个URL,用匹配URL的正则表达式去匹配就可以了。前面讲了一些匹配规则,更多常用的匹配规则在下表,大家可以去看看。

常用的匹配规则

        看了这些规则,可能大家脑袋有点懵,这很正常,不用着急。后面我们会详细讲讲一些常见规则具体怎么用。正则表达式不是Python特有的,其他编程语言也能用。不过在Python里,有个re库,它把正则表达式的功能都实现了。依靠这个re库,我们就能在Python编程中使用正则表达式。基本上,只要在Python里写正则表达式,都会用到这个re库。下面,我们就来认识一下re库的一些常用方法 。 

二、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())

运行结果如下:

        我们先弄了一个字符串,里面有英文字母、空白的地方,还有数字啥的。然后,写了这么一个正则表达式:^Hello\s\d\d\d\s\d{4}\s\w{10} 。用它来跟这个长字符串对比看看能不能配上。这个正则表达式开头的^,意思是得从字符串开头匹配,也就是开头得是Hello;\s是用来找空白字符的,就对应目标字符串里的空格;\d能匹配数字,这里3个\d就是为了匹配123;再写个\s,又是匹配空格;后面的4567,本来可以写4个\d来匹配,不过太麻烦,就用{4},表示按照前面匹配数字的规则,连着匹配4次,也就是匹配4个数字;接着再匹配1个空白字符;最后\w{10},就是匹配10个字母或者下划线。要注意,我们这个正则表达式没有把目标字符串全部覆盖,但还是能匹配,就是得到的匹配结果短一些。

        在使用match()方法的时候,第一个参数放我们写的正则表达式,第二个参数放要匹配的字符串。把结果打印出来,得到一个SRE_Match对象,这就说明匹配成功了。这个对象有两个好用的方法:group()方法能把匹配到的内容显示出来,结果是Hello 123 4567 World This,刚好就是按照正则表达式规则匹配到的那部分;span()方法能显示匹配的范围,结果是(0, 25),也就是匹配到的这串字符,在原来那个长字符串里从第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),就能得到我们想要的1234567这个匹配结果了。 

运行结果如下:

        从结果能明显看出,我们成功提取到了1234567 。这里用到的group(1)和group()作用不一样。group()会把整个符合正则表达式的匹配结果输出,而group(1)专门输出第一个被小括号()包围的那部分匹配结果。要是正则表达式里还有其他被()括起来的内容,那就按顺序,用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())

        这里将中间部分直接省略,全部用.*代替,最后加上结尾字符串。运行结果如下:

        从输出结果能发现,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$ ,乍一看,好像没啥毛病。 

看看运行结果:

        怪了,结果只得到了数字7。这到底咋回事呢?这就和贪婪匹配、非贪婪匹配有关了。在贪婪匹配模式下,.*会一股脑地尽可能多匹配字符。你看这个正则表达式,.*后面跟着\d+,\d+的意思是至少得有一个数字,但没说具体要几个数字。这样一来,.*就可劲儿匹配,把123456都匹配走了,只给\d+剩下一个数字7,所以最后我们得到的就只有7了。

        这明显不太好,有时候会导致匹配结果莫名其妙地少了一部分。其实,遇到这种情况,用非贪婪匹配就行。非贪婪匹配写法也简单,就是在.*后面加个?,写成.*?。那这么一改,能有啥不一样的效果呢?下面再看个例子。

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

这里只是把第一个.*改成了.*?,变成非贪婪匹配。结果如下:

        这次,我们成功把1234567提取出来了。这其中的原因不难理解,贪婪匹配一门心思要匹配最多的字符,而非贪婪匹配却恰恰相反,它只匹配最少的字符。当正则表达式中的.*?匹配到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))

运行结果如下:

可以看到,.*?没有匹配到任何结果,而.*则尽量匹配多的内容,成功得到了匹配结果。

修饰符
        正则表达式里能带上一些可选的标志修饰符,用它们来调整匹配的模式。这些修饰符作为一种可选的标志存在。下面通过具体例子给大家讲讲: 

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

        和上面的例子类似,在字符串中加了换行符,正则表达式不变,用来匹配其中的数字。看看运行结果:

        程序一运行就报错了,这表明正则表达式没能和这个字符串匹配上,返回的结果是None 。可我们还接着调用了group()方法,这就引发了AttributeError错误。

        那为啥加了一个换行符就匹配不了呢?这是因为在默认情况下,.只能匹配除换行符以外的任何字符。当碰到换行符的时候,.*?就没办法继续匹配了,所以整个匹配就失败了。其实解决这个问题很简单,只要加上一个修饰符re.s就行:

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

        这个修饰符的作用是使.匹配包括换行符在内的所有字符。此时运行结果如下:

        在进行网页匹配时,re.s这个修饰符常常会用到。我们知道,HTML代码里节点和节点之间经常会有换行,而加上re.s修饰符之后,就可以让正则表达式匹配这些换行部分了。除了re.s,还有其他一些修饰符,在某些特定情况下也能派上用场,具体看下表: 

修饰符

在网页匹配中,比较常用的有re.s和re.I。

转义匹配

        咱们都知道正则表达式有好多匹配模式,像“.”这个符号,它的作用是匹配除了换行符之外的任意字符。但要是我们要匹配的目标字符串里,本身就有“.”这个符号,那该怎么处理呢?这时候,就得用到转义匹配了。下面给大家举个例子: 

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

        要是碰到那些在正则表达式里有特殊用途、用来表示匹配模式的字符,在它前面加个反斜线,就能把它当普通字符来匹配了。比如说,要匹配字符“.”,就写成“\. ”。下面看看运行结果:

        从结果能看出,成功匹配到了原来的字符串。上面讲的这些,都是写正则表达式时常用的知识点,大家把它们掌握好,对后面写正则表达式做匹配操作,会有很大帮助 。 

三、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。尽管从整体上看,这个正则表达式所描述的内容确实包含在这个字符串里,但由于开头不相符,按照match()方法从开头匹配的规则,这次匹配就没法成功。下面就是实际运行之后得到的结果: 

None

        用match()方法的时候,得特别留意字符串的开头部分,这在匹配操作时不太方便。所以,match()方法更适合用来判断某个字符串是否完全符合特定的正则表达式规则。

        这里还有个方法叫search(),它在匹配时会对整个字符串进行扫描,一旦找到第一个匹配成功的结果就返回。这意味着,正则表达式描述的内容可以只是字符串的一部分。search()方法会逐个位置地扫描字符串,直到找到第一个符合正则表达式规则的部分,然后返回匹配到的内容。要是把整个字符串都搜完了还没找到符合规则的部分,就会返回None。

        咱们把上面代码里的match()方法换成search()方法,再看看运行结果:

import re
content = 'Extra stings Hello 1234567 World This is a Regex Demo Extra stings'
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)
print(result.group(1))

        这下子,成功得到了匹配的结果。所以说,为了让匹配过程更简便,我们最好选用search()方法。下面,我们再通过几个具体的例子,深入了解一下search()方法该怎么用。首先,这里有一段HTML文本,接下来我们要写几个正则表达式,用search()方法来提取文本里相应的信息。 

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" 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="邓丽君">但愿人长久</a>
        </li>
    </ul>
</div>
'''

        仔细瞧瞧这段HTML代码,能看到在`ul`节点里头,有好多`li`节点。这些`li`节点情况还不一样,有的里面有`a`节点,有的则没有。那些有`a`节点的,`a`节点还带着超链接、歌手名这些属性。

        咱们先试试从这段代码里,把`class`属性值为`active`的`li`节点内部,超链接所关联的歌手名和歌名提取出来。具体到这段代码,就是要提取第三个`li`节点下,`a`节点的`singer`属性值,以及`a`节点里面的文本。

        写这个正则表达式的时候,开头得先写上`li`,接着得找到`active`这个关键标志。这中间因为内容不太确定,就用`.*?`来匹配最少的字符。然后,因为要提取`singer`这个属性的值,所以得写上`singer="(.*?)"`,把要提取的内容用小括号括起来,方便后面用`group()`方法把它揪出来,注意,两边得用双引号作为边界。再往后,还要匹配`a`节点里面的文本,左边边界是`>`,右边边界是`</a>`,中间要提取的文本部分,还是用`(.*?)`来匹配。这样一来,最终的正则表达式就写成了:

<li.*?active.*?singer="(.*?)">(.*?)</a>

        接下来调用search()方法,这个方法会把整个HTML文本从头到尾扫描一遍,一旦发现有符合咱们刚才写的正则表达式的内容,就把找到的第一个匹配项返回出来。因为这段HTML代码里有换行,要是不处理一下,可能会影响匹配效果,所以在调用search()方法时,得把第三个参数设置成re.S 。完整的匹配代码就像下面这样: 

result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
    print(result.group(1), result.group(2))

        因为我们要获取的歌手名和歌名,在正则表达式里都已经用小括号给括起来了,这样就可以利用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`节点里有换行,也能被正常匹配到。那要是我们把这个`re.s`去掉,会出现什么结果呢?下面是修改后的代码:

result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html)
if result:
    print(result.group(1), result.group(2))

运行结果如下:

        从运行结果能明显看出,匹配到的内容变成了第四个`li`节点里的东西。为啥会这样呢?原因就在于第二个和第三个`li`节点里都有换行符。之前加上`re.s`的时候,`.*?`能够匹配换行符,所以能匹配到这些带换行的节点。可一旦把`re.s`去掉,`.*?`就没办法匹配换行符了,这样一来,正则表达式就匹配不到第二个和第三个`li`节点。而第四个`li`节点里面没有换行符,正好符合去掉`re.s`之后的匹配条件,所以就成功被匹配到了。

        大家要知道,大部分的HTML文本里面都会有换行符。所以,为了保险起见,在使用正则表达式匹配HTML文本的时候,最好都加上`re.s`这个修饰符,不然很容易出现该匹配的内容没匹配到的问题。

四、findall()

        前面给大家讲了search()方法怎么用,它只能返回符合正则表达式的第一个匹配内容。可要是我们想把所有符合正则表达式的内容都拿到手,该怎么做呢?这就得靠findall()方法了。这个方法会把整个字符串全面搜索一遍,然后把所有匹配正则表达式的内容都给返回出来。

        就拿刚才那个HTML文本来说,如果我们打算把所有a节点的超链接、歌手名以及歌名都提取出来,那就把之前代码里的search()方法换成findall()方法。因为findall()方法返回的结果是一个列表类型,所以我们得通过遍历这个列表,才能把里面一组一组的内容依次取出来。

代码如下:

results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
print(results)
print(type(results))
for result in results:
    print(result)
    print(result[0], result[1], result[2])

运行结果如下:

        从结果能看到,返回的列表里,每个元素都是元组类型,咱们直接用对应的索引就能把元素依次取出来。

        要是只需要获取第一个匹配的内容,用`search()`方法就行。但要是需要提取多个匹配的内容,那就得用`findall()`方法了。

五、sub()

        除了用正则表达式来提取信息,有时候我们还得靠它来修改文本。就拿去掉一串文本里所有数字来说,如果只用字符串的`replace()`方法,操作起来会特别麻烦。这种情况下,用`sub()`方法就方便多了。下面给你举个例子: 

import re
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)

运行结果如下:

aKyroiRixLg

        用`sub()`方法的时候,第一个参数填`\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())

运行结果如下:

        从结果能看出,用`sub()`方法处理后,HTML文本里的`a`节点就消失了。这样一来,剩下的文本就变得更“干净”,更方便我们后续操作。之后,直接用`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)

        比如说,这儿有3个日期,我们想把这3个日期里的时间部分都去掉,这时候就能用`sub()`方法。用`sub()`方法时,第一个参数得是正则表达式。但要是直接用,就得把同一个正则表达式写3遍,挺麻烦的。这时候就可以用`compile()`方法,把这个正则表达式编译成一个正则表达式对象,之后就能重复用这个对象了。 

运行结果如下:

        另外,用`compile()`方法的时候,还能传入像`re.s`这样的修饰符。这样一来,在使用`search()`、`findall()`这些方法时,就不用再额外传修饰符了。可以说,`compile()`方法就像是给正则表达式加了个“包装”,让我们能更方便地重复使用。

        到这儿,正则表达式的基本用法就讲完了。接下来,我会结合具体的例子,给你详细说说正则表达式该怎么用。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值