前言

在学编程的过程中,我们可能听过正则表达式,但是不知道它是什么,我一开始听到正则表达式时,我在想正则表达式是啥?它用来干嘛的?学起来难不难的?可能很多人和我想的一样。学完之后,我很认真负责地告诉你们,正则表达式不难!!!

正则表达式

百度百科里写到:

正则表达式又称规则表达式,计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本,是对字符串操作的一种逻辑公式,是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

我们来打开开源中国提供的正则表达式测试工具https://tool.oschina.net/regex#,输入待匹配的文本,然后选择要匹配的表达式,这样就可以得出结果了,如下图所示:

简单来说,正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证、在HTML里提取想要的信息都是简简单单的事。

有人可能会说下面这个是啥东西,看不懂。

[a-zA-z]+://[^\s]*

那个就是正则表达式的特定语法规则组合,通过这些组合,我们就可以得到我们想要的字符,例如,\s表示匹配任意的空白字符,*代表匹配前面的字符任意多个等等。常用的匹配字符规则如下表:

模式描述
\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
( )匹配括号内的表达式,也表示一个组

看到上面的表是不是有点害怕了,在爬虫中,我们用得最多的匹配字符有下面几个:

  • .:匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符;

  • *:匹配0个或多个表达式;

  • ?:匹配0个或1个前面的正则表达式定义的片段;

  • ():匹配括号内的表达式 ,也表示一个组;

  • \d:匹配任意数字,等价于[0-9];

  • {n}:精确匹配n个前面的表达式;

  • $:匹配一行字符串的结尾。

大家先记住上面最常用的匹配字符,在接下来的学习中,我们遇到其他匹配字符的话,会做简单的讲解。

re库——常用方法

查找一个匹配项

match():从字符串起始位置开始匹配,如果匹配不成功就返回None。

search():匹配整个字符串并返回第一个匹配成功的值,否则返回None;

fullmatch():匹配整个字符串是否与正则完全相同,否则返回None。

其语法格式为:

re.match(pattern,string,flags=0)
re.search(pattern,string,flags=0)
re.fullmatch(pattern,string,flags=0)

其中:

  • pattern表示匹配的正则表达式或字符串;

  • string表示匹配的字符串;

  • flags表示标准位,用于控制正则表达式的匹配方式也可以忽略不写,如:是否区分大小写。

具体代码如下所示:

import re
print(re.match('Hello,Word','Hello,WOrd'))
print(re.search('e.*?$','hello Word'))
print(re.fullmatch('e.*?$','Hello Word'))

运行结果为:

None
<re.Match object; span=(1, 10), match='ello Word'>
None

re.match()方法中,第一个参数是字符串,第二个参数是要匹配的字符串,由于两个字符串中的字母o不同,所以匹配不成功,返回的值为None;

re.search()方法中,第一个参数是正则表达式,该表达式表示从字母e开始匹配0个或多个任意字符前面正则表达式定义的片段匹配到字符串末尾,第二个参数是要匹配的字符串,在输出结果中,object是输出对象类型,span=(1,10)表示该匹配的范围是1到9,match='ello Word’表示匹配的内容;

re.fullmach()方法与re.match()方法差不多,第一个参数是正则表达式,第二个参数是要匹配的字符串,由于要匹配的字符串与正则表达式不匹配,所以返回的值为None。

注意:查找一个匹配项,返回的都是一个匹配对象。

查找多个匹配项

re.findall:在字符串任意位置中找到正则表达式所匹配字符,并返回一个列表,如果没有找到匹配的,则返回空列表;

re.finditer:在字符串任意位置中找到正则表达式所匹配字符,并返回一个迭代器;

语法格式为:

re.findall(pattern, string, flags=0)   或   pattern.findall(string , n,m)
re.finditer(pattern, string, flags=0)

其中:

  • string表示待匹配字符串;
  • n,m是可选参数,指定字符串的起始位置n(默认值为0)和结束位置m(默认为字符串的长度);
  • pattern表示匹配的正则表达式或字符串;
  • flags表示标志位,用于控制正则表达式的匹配方式,如:是否区分大小写。

具体代码如下所示:

import re
fd1 = re.findall('\D+','11a33c4word34f63')
pattern = re.compile('\D+')
fd2 = pattern.findall('11a33c4word34f63', 0, 10)
fi=re.finditer('\D+','11a33c4word34f63')
print(fd1)
print(fd2)
print(fi)

运行结果为:

['a', 'c', 'word', 'f']
['a', 'c', 'wor']
<callable_iterator object at 0x00000193BD2C5F10>

在第二行代码中,我们使用了re.findall(pattern, string, flags=0) 语法,第一个参数是由\D+简单匹配字符组合起来的正则表达式,其意思是匹配1个或多个任意非数字的字符,第二个参数是要匹配的字符串,所以匹配的内容是acwordf,没有被匹配的字符作为列表的分割点,所以返回的内容是[‘a’, ‘c’, ‘word’, ‘f’];

在第三行代码中,我们使用了re.compile()方法将正则字符串编译成正则表达式对象(这个re.compile()方法后面会介绍),第四行代码中,在pattern对象中调用了findall()方法,第一个参数是要匹配的字符串,后面两个数字是匹配字符串的始末位置,所以返回的内容是[‘a’, ‘c’, ‘wor’];

在第五行代码中,我们使用了re.finditer()方法,第一个参数是正则表达式,第二个参数是要匹配的字符串,返回的内容中的callable_iterator代表是迭代器。我们用个for循环打印输出匹配的成功的字符串有哪些?

fi=re.finditer('\D+','11a33c4word34f63')
for i in fi:
    print(i)

运行结果为:

<re.Match object; span=(2, 3), match='a'>
<re.Match object; span=(5, 6), match='c'>
<re.Match object; span=(7, 11), match='word'>
<re.Match object; span=(13, 14), match='f'>

匹配的内容和findall方法的一样,在可能存在大量的匹配项的情况下,我们推荐使用finditer方法,因为findall方法是返回列表,列表是一次性生成在内存中,而finditer方法是返回迭代器,迭代器是需要使用时一点一点生成出来的,内存使用更优。

分割

re.split():按照能够匹配的子串将字符串分割后返回列表。

语法格式为:

re.split(pattern, string ,maxsplit=0, flags=0)

其中:

  • pattern表示匹配的正则表达式或字符串;
  • string表示要匹配的字符串;
  • maxsplit表示分隔次数,maxsplit=1分隔一次,默认为 0,不限制次数;
  • flags表示标志位,用于控制正则表达式的匹配方式,如:是否区分大小写。

具体代码如下所示:

sp1=re.split('\d+','I5am5Superman',maxsplit=0)
sp2=re.split('\d+','I4am5Superman',maxsplit=1)
sp3=re.split('5','I4am5Superman',maxsplit=0)
print(sp1)
print(sp2)
print(sp3)

运行结果为:

['I', 'am', 'Superman']
['I', 'am5Superman']
['I4am', 'Superman']

在第二、三、四行代码中,我们使用了re.split()方法,我们使用了正则表达式或者数字作为分割点,并设置了分割次数。

注意:str模块也有个split方法,主要区别是str.split不支持正则分割,re.split支持正则;

替换

re.sub():用于替换字符串中的匹配项;

re.subn():用于替换字符串中的匹配项,返回一个元组。

语法格式为:

re.sub(pattern, repl, string, count=0, flags=0)
re.subn(pattern, repl, string, count=0, flags=0)

其中:

  • pattern表示匹配的正则表达式或字符串;
  • repl表示字符串或函数,当repl表示字符串时,则处理其中的任何反斜杠转义,当repl表示函数时,只能有一个入参:Match匹配对象。
  • string表示要被查找替换的原始字符串;
  • count表示匹配后替换的最大次数,默认为0表示替换所有的匹配;
  • flags表示表示时用到的匹配模式。

具体代码如下所示:

import re
def loo(matchobj):
    if matchobj.group(0)=='1':
        return '*'
    else:
        return '+'
print(re.sub('\w+',loo,'1-23-123-ds-fas23221'))
print(re.sub('\d+','1','1-23-123-ds-fas23221',count=2))
print(re.subn('\w+','1','1-23-123-ds-fas23221adsa2'))

运行结果为:

*-+-+-+-+
1-1-123-ds-fas23221
('1-1-1-1-1', 5)

首先我们定义了一个loo函数其作用是根据字符来返回特定的字符,第七、八行使用了re.sub(),其中repl传入的参数分别为函数和字符,第一个sub()没有传入count数据,所以替换所有的匹配,第二个sub()传入count数据为2,所以替换了匹配前两个数据;

第九行代码,我们使用了re.subn()方法其返回的是一个元组,传入的第一个参数为正则表达式,其作用是匹配字母、数字及下划线,第二个参数为替换成的字符,第三个参数为要被查找替换的原始字符串,由于我们替换了5次,所以返回的是(‘1-1-1-1-1’, 5)。

正则表达式对象

re.compile():将正则字符串编译成正则表达式对象。

其语法格式为:

re.compile(pattern,flags)

其中:

  • pattern表示正则表达式;
  • flags表示匹配模式,比如忽略大小写,多行模式等。

具体代码实例如下所示:

import re
pattern=re.compile('\w+')
content='I-am-superman'
result=re.findall(pattern,content)
print(result)

运行结果为:

['I', 'am', 'superman']

首先我们调用compile方法来创建一个正则表达式对象,定义一个content变量来存放字符串,再调用findall()方法,将匹配的字符串以列表的形式输出。compile()方法是给正则表达式做了一层封装,以便我们更好地复用,这样我们在调用search()方法、findall()等方法中就不需要重新在写正则表达式了。

re库——修饰符

正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。多个标志可以通过按位 OR(|) 它们来指定。如 re.I 和 re.M 被设置成 I 和 M 标志,还有一些修饰符,在必要的情况下也可以使用,如下表所示:

修饰符描述
re.I或re.IGNORECASE使匹配对大小写不敏感
re.L或re.LOCALE做本地化识别(locale-aware)匹配
re.M或re.MULTILINE多行匹配,影响 ^ 和 $
re.S或re.DOTALL使 . 匹配包括换行在内的所有字符
re.A或re.ASCII只匹配ASCII,而不是Unicode
re.X或re.VERBOSE该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

我们网页匹配比较常用的re.I和re.S来举例说明。

具体代码实例一如下:

import re
#修饰符re.I
print(re.findall('SUPERMAN','i am superman'))
print(re.findall('SUPERMAN','i am superman',re.I))
#修饰符re.S
print(re.findall('.*?','\nman',))
print(re.findall('.*?','\nman',re.S))

运行结果为:

[]
['superman']
['', '', 'm', '', 'a', '', 'n', '']
['', '\n', '', 'm', '', 'a', '', 'n', '']

没有添加re.I和re.S修饰符时,

由于大小写的原因,第一个输出为空;

由于匹配时自动忽略特殊符的匹配,所以第三个输入没有换行符(\n)。

小技巧

匹配目标

如何中一段文本中提取一部分内容呢,我们可以使用()括号将想提取的子符串括起来,它标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每个分组,调用group()方法传入分组的索引即可获得提取的结果。

具体实例代码如下所示:

import re
content='I am superman,It is cool123'
result=re.match('^I\s(\w+)\ssup.*?is\scool(\d+)',content)
print(result.group(0))
print(result.group(1))
print(result.group(2))

运行结果为:

I am superman,It is cool123
am
123

首先我们我们调用了re.match()方法进行查找匹配,然后调用group()方法传入分组的索引,这里我们使用了两个(),所以最大索引为2。

  • 当索引为0时,输出的是整个匹配的结果;
  • 当索引为1时,输出的是第一个目标匹配;
  • 当索引为2时,输出的是第二个目标匹配;

假如正则表达式后面还有()括号的内容,那么可以依次使用group(3)、group(4)来获取。

贪婪与非贪婪匹配

.*:贪婪匹配是尽可能匹配多的字符;

.*?:非贪婪匹配是尽可能匹配少的字符。

它们之间的区别是多了一个问号,我们通过具体代码来感受一下它们之间的差别。

具体代码如下:

import re
content='I am 123superman,It is cool'

#贪婪匹配.*
result=re.match('^I .*(\d+).*cool',content)
print(result)
print(result.group(1))

#非贪婪匹配.*?
result1=re.match('^I .*?(\d+).*?cool',content)
print(result1)
print(result1.group(1))

运行结果为:

<re.Match object; span=(0, 27), match='I am 123superman,It is cool'>
3
<re.Match object; span=(0, 27), match='I am 123superman,It is cool'>
123

匹配内容中,贪婪匹配与非贪婪匹配的整个匹配内容一样,但贪婪匹配的目标匹配内容会缺少一部分,这是因为贪婪匹配会尽可能匹配多的内容。

  • 当贪婪匹配.*后面是\d+,也就是至少匹配一个数字,但没具体几个数字,所以贪婪匹配就把12匹配了,只给\d+留了一个数字,导致最后得到的内容只有数字3了;
  • 当非贪婪匹配.*?匹配到I 时,在往后就是数字了,与\d+刚好匹配,非贪婪匹配就不再进行匹配,交给\d+去匹配后面的数字,所以最后得到的内容是123了。

在匹配的时候,尽量使用非贪婪匹配,以免出现匹配结果缺失的情况。

实战演练

现在我们来点实战,尝试爬取QQ音乐中热歌榜的排名、图片链接、歌名、歌手和播放时间等信息,并将信息存放在csv文件中。

本次爬取的基本思路:

  1. 页面分析;
  2. 抓取页面源代码;
  3. 正则提取我们想要的信息;
  4. 保存信息到csv文件中。

页面分析

首先我们在chrome浏览器打开QQ音乐中热歌榜,并打开开发者工具,如下图所示:

我们观察发现,存放排行榜信息的容器是在上图中的div中,展开这个div,观察里面的信息,如下图所示:

展开后,我们发现里面有两个ul,其中第二个ul里面有很多个li,每个li对应着一首歌的信息,这些信息正是我们待会构造正则表达式的依据。

抓取页面源代码

首先我们定义了一个get_page()方法用来获取页面源代码,具体代码如下所示:

import re
import requests
import csv

#获取页面源代码
def get_page():
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36'
    }
    response=requests.get('https://y.qq.com/n/ryqq/toplist/26',headers=headers)
    if response.status_code==200:
        html=response.text
        parse_page(html)
    return None

在上述代码中,首先导入re、requests、csv库,然后我们构造了一个请求头headers,再调用requests.get()方法,并给它传入了我们所需要获取的源代码的网址,如果网页的响应状态码为200时,就调用parse_page()方法,其方法我们给它传入的参数为Unicode型的数据。这样我们就成功把QQ音乐热歌榜源代码抓取下来并将源代码传输出去了。

正则提取

在上一步中,我们已经成功提取了源代码,接下来就要构造正则表达式把我们想要的内容提取出来存放在字典中,这里我们使用了非贪婪匹配。

首先我们先获取排名,打开开发者工具,寻找排名信息源代码所在的地方,如下图所示:

在上图中,我们可以看到排名代码的信息,我们根据这个信息来构造排名的正则表达式,由于一个li对应一首歌,所以我们正则表达式开头是

  • ,如下所示:
  • <li>.*?songlist__item.*?songlist__number.*?">(.*?)
    

    成功匹配排名后,我们根据下图的代码构造图片链接和歌名的正则表达式,

    经过查找后,我们图片链接存放在上图的img节点中,我们提取src属性,由于歌名也在img节点中,我们顺便把歌名提取出来,我们在排名的正则表达式基础上添加图片链接和图片的正则表达式:

    <li>.*?songlist__item.*?songlist__number.*?">(.*?)</div>.*?<img.*?src="(.*?)".*?alt="(.*?)".*?</li>
    

    成功匹配图片链接和歌名后,我们开始匹配歌手和播放时间了,我们根据下图的代码构造歌手和播放时间的正则表达式:

    经过查找后,我们歌手存放在上图的a节点中,我们提取title属性,歌名在div节点中,我们提取该节点的文本,我们在原有的正则表达式的基础上添加歌手和播放时间的正则表达式:

    <li>.*?songlist__item.*?songlist__number.*?">(.*?)</div>.*?<img.*?src="(.*?)".*?alt="(.*?)".*?<a.*?playlist__author.*?title="(.*?)".*?<div.*?songlist__time">(.*?)</div>.*?</li>
    

    好了,正则表达式已经写好了,我们调用re.compile()方法构造正则表达式对象,以便复用,具体代码如下:

    pattern=re.compile(
            '<li>.*?songlist__item.*?songlist__number.*?">(.*?)</div>.*?<img.*?src="(.*?)".*?alt="(.*?)".*?<a.*?playlist__author.*?title="(.*?)".*?<div.*?songlist__time">(.*?)</div>.*?</li>',re.S
        )
    

    在re.compile()方法中,我们传入了re.S参数,其作用是使 . 匹配包括换行在内的所有字符。

    提取我们想要的信息内容后,我们接下来把信息存放在字典中,并调用saving_song()方法并给其传入字典参数,具体代码如下:

    items=re.findall(pattern,html)
        for item in items:
            song= {
                '排名':item[0],
                '图片链接':'https:'+item[1],
                '歌名':item[2],
                '歌手':item[3],
                '播放时间':item[4]
            }
            saving_song(song)
    

    保存信息

    提取信息后,我们接下来开始保存信息,首先我们先创建一个csv文件,并写入表头,具体代码如下:

    with open('Song.csv','a',newline='')as csvfile:
        writer=csv.writer(csvfile)
        writer.writerow(['排名','图片链接','歌名','歌手','播放时间'])
    

    然后定义saving_song()方法,将字典写入csv文件中,具体代码如下:

    def saving_song(song):
        with open('Song.csv','a',newline='')as csvfile:
            writer=csv.writer(csvfile)
            writer.writerow([song.get('排名'),song.get('图片链接'),song.get('歌名'),song.get('歌手'),song.get('播放时间')])
    

    这样就把数据存放在csv文件中了。

    结果展示

    最后

    好了,正则表达式就讲解到这里了,需要源码的小伙伴可以私信博主。
    感谢观看!!!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐