正则表达式与Python中re模块的使用

爬虫中正则表达式使用的非常多,用Python做的话会用到re模块。 本文总结一下正则表达式re模块的基础与使用。

另外,给大家介绍一个在线测试正则表达式的神器网站http://tool.chinaz.com/regex

正则表达式

使用场景

关于正则表达式的基本概念这里就不赘述了,大家可以去各种百科里查找它的定义。正则的使用场景主要分为两个:

一是:检测某一段字符串是否符合规则,也就是我们常说的"校验"
二是:从一大段字符串中找到符合规则的字符串,可以理解为"检索"

对于第一种使用场景,我们在登陆或者注册时填写邮箱、手机号等内容的时候经常会见到——底层的实现思路就是利用正则“校验”我们输入的内容是否“规范”。 另外,正则的“检索”功能大量使用在爬虫里,简单的说,爬虫能从一个网站大量的数据中得到用户想要的内容等等...... 需要特别注意,正则表达式只是用来处理字符串的

元字符

一:可以灵活使用的元字符:[] 与 [^]
(1)数字  [0-9]
(2)小写字母  [a-z]
(3)大写字母  [A-Z]
(4)大小写字母  [A-Za-z]
(5)大小写字母+数字  [0-9A-Za-z]
(6)注意一个字符组不限制个数:匹配三个的话:[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
(7)大小写字母+ _与%   [A-Za-z_%]
(8-1)匹配1-5   [1\-5]  用转义符,- 有特殊含义
(8-2)字符组中 - 是有特殊意义的,需要使用\作为转义符!
(9)[^123]——除了123都匹配,包括换行
二:匹配字符
(1)\d——所有数字
(2)\w——字母数字下划线
(3)\s——空白
(4)空格匹配空格
(5)\t——制表符
(6)\n——换行符
(7)\b——单词的边界   o\b——hello的'o'(o是单词的最后一个)    \bo——ok的'o'(o是单词的第一个)
(8)\W——除了字母数字下划线
(9)\D——除了数字
(10)\S——除了空白符
(11).——匹配除了换行符所有的
(12)[\D\d]、[\W\w]、[\s\S]——匹配所有的
(13)[^123]——除了123都匹配,包括换行
(14)^——开始字符
(15)$——结束字符
(16)^.....$——开始+结尾,中间是5个除了换行的符号
(17)|——或   长的放在前面!
(18)()——分组   www\.(baidu|oldboy)\.com  匹配www.baidu.com或者www.oldboy.com

量词

量词用来限制匹配的次数

(1){n}——匹配n次
(2){n,}——匹配至少n次
(3){n,m}——匹配n到m次
(4)+——一次或者多次  匹配小数点的前后必须有数      \d+\.\d+
(5)*——0次或者多次    匹配整数:第一位是1-9不要是0:     [1-9]\d*|0
(6)?——0次或者一次    匹配一个整数或者小数: \d+(\.\d+)?

常用的正则表达式小结

  • '.' 默认匹配除\n之外任意一个字符,若指定flag DOTALL,则匹配任意字符,包括换行
  • '^' 匹配字符开头,若指定flags MULTILINE,这种也可以匹配上(r"^a","\nabc\neee",flags=re.MULTILINE)
  • '$' 匹配字符结尾,若指定:flags MULTILINE ,re.search('foo.$','foo1\nfoo2\n',re.MULTILINE).group() 会匹配到foo1
  • '*' 匹配号前的字符0次或多次, re.search('a','aaaabac') 结果'aaaa'
  • '+' 匹配前一个字符1次或多次,re.findall("ab+","ab+cd+abb+bba") 结果['ab', 'abb']
  • '?' 匹配前一个字符1次或0次 ,re.search('b?','alex').group() 匹配b 0次
  • '{m}' 匹配前一个字符m次 ,re.search('b{3}','alexbbbs').group() 匹配到'bbb'
  • '{n,m}' 匹配前一个字符n到m次,re.findall("ab{1,3}","abb abc abbcbbb") 结果'abb', 'ab', 'abb']
  • '|' 匹配|左或|右的字符,re.search("abc|ABC","ABCBabcCD").group() 结果'ABC'
  • '(...)' 分组匹配, re.search("(abc){2}a(123|45)", "abcabca456c").group() 结果为'abcabca45'
  • '\A' 只从字符开头匹配,re.search("\Aabc","alexabc") 是匹配不到的,相当于re.match('abc',"alexabc") 或^
  • '\Z' 匹配字符结尾,同$
  • '\d' 匹配数字0-9
  • '\D' 匹配非数字
  • '\w' 匹配[A-Za-z0-9]
  • '\W' 匹配非[A-Za-z0-9]
  • '\s' 匹配空白字符、\t、\n、\r , re.search("\s+","ab\tc1\n3").group() 结果 '\t'
  • (?P<name>...) 分组匹配 re.search("(?P<province>[0-9]{4})(?P<city>[0-9]{2})(?P<birthday>[0-9]{4})","371481199306143242").groupdict("city") 结果{'province': '3714', 'city': '81', 'birthday': '1993'}

贪婪匹配与惰性匹配

在正则中,默认的匹配模式是“贪婪匹配”,也就是说,在符合匹配规则的前提下尽可能多的去匹配字符,但是有些时候,我们不需要匹配太多的内容,只要得到需要的“片段”内容就好了。 而量词是匹配多次的,这时我们可以在量词的后面加上?就可以让匹配“适可而止”了,这样的匹配规则就是惰性匹配 这里举一个贪婪匹配惰性匹配对比的例子:

有字符串"aasdasdasdxxxxxxasdasd",现在想匹配到x结束
(1)用"惰性匹配"的话:
    语法:a.*?x
    说明:遇见一个先检测是不是x,如果是的话就停止,不是的话就匹配
    结果:aasdasdasdx
(2)默认的"贪婪匹配":
    语法:a.*x
    说明:这里会匹配后面所有的x
    结果:aasdasdasdxxxxxx

当然,什么时候用贪婪匹配什么时候用惰性匹配需要我们具体情况具体分析。

re模块

re模块是Python的内置模块,是专门用来处理与正则表达式相关需求的模块。 使用方法:直接在Python程序中import即可:

import re

根据正则的规则从一段内容中查找结果

findall

找到所有符合正则规则的字符串,并以列表的形式返回结果:

ret = re.findall('\d+','whw123w1233')
print(ret)
#结果:
['123', '1233']

找到第一个,返回结果集,需要通过group方法取值。没取到的话返回None,此时用group方法会报错!因此需要提前判断下:

ret = re.search('\d+','wanghw123ww')
print(ret)
if ret:
    print(ret.group())
#结果:
<_sre.SRE_Match object; span=(6, 9), match='123'>
123

match

从头开始找第一个,返回一个结果集,需要通过group取值。没取到的话返回None,此时用group方法会报错!因此需要提前判断下:

下面是匹配到结果的:

ret = re.match('\d+','23whw22a')
print(ret)
if ret:
    print(ret.group())
#结果:
<_sre.SRE_Match object; span=(0, 2), match='23'>
23

没有匹配到的话可以利用异常处理或者像上面一样的条件判断:

ret = re.match('\d+','whw22a')
print(ret)
try:
    print(ret.group())
except AttributeError as e:
    print(e,':','没有匹配到!')
#结果:
None
'NoneType' object has no attribute 'group' : 没有匹配到!

我们可以看到,match的返回结果与search是一样的;而且,match的匹配语法其实就是相当于search在其匹配规则上加上^,因此:match可以被search代替,实际中match用的比较少。

re.search('^\d+','123asd') 
等同于:
re.match('\d+','123asd')

替换与切割

sub

我们可以利用re模块与正则表达式结合进行字符串的批量替换: 有如下需求: 字符串:s1 = 'wanghw123whw456',将s1中所有的数字替换成字符串"HERO"。 我们可以利用re模块的sub方法来做:

s1 = 'wanghw123whw456'
ss = re.sub('\d+','HERO',s1)
print(ss)
#结果:
wanghwHEROwhwHERO

sub方法的第一个参数是正则的规则,第二个参数是要被替换成的字符串,第三个参数是需要被操作的字符串。当然,聪明的你肯定想到一个问题:这默认替换的是所有符合规则的字符串呀!我如果想限定替换的次数怎么做呢?我们可以指定第四个参数,来达到限定替换次数的作用:

s1 = 'wanghw123whw456'
ss = re.sub('\d+','HERO',s1,1)
print(ss)
#结果:
wanghwHEROwhw456

subn

subn的用法跟sub一模一样,只不过返回值是有区别的,我们可以执行一下看看:

s1 = 'wanghw123whw456'
ss = re.subn('\d+','HERO',s1)
print(ss)
#结果:
('wanghwHEROwhwHERO', 2)

当然也可以指定替换的次数:

s1 = 'wanghw123whw456'
ss = re.subn('\d+','HERO',s1,1)
print(ss)
#结果:
('wanghwHEROwhw456', 1)

大家可以看到,subn方法返回一个元组:第一个元素是得到的结果,第二个元素是替换的次数。

split

split方法与字符串的切割方法一样,返回的也是一个列表,只不过,这次我们用的是正则匹配的结果进行切割的!

s1 = 'wanghw123whw456qwe'
sq = re.split('\d+',s1)
print(sq)
#结果:
['wanghw', 'whw', 'qwe']

split的第一个参数放正则规则,第二个参数放被操作的字符串。

预编译

我们都知道,Python是一种解释型的编程语言,Python的代码需要先在解释器中转换为机器码,最终转换为计算机能识别的编码才能运行。 对于正则匹配来说,如果一个同样的正则表达式需要用到几十次甚至上百次去匹配一段长文字的话,用普通的方法,我们需要将这个正则表达式运行几十次甚至上百次来进行每一次的匹配。这样大大的降低了我们程序的效率,是我们最不愿意看到的! 而在Python中为我们提供了预编译的方法去解决这个难题。 所谓的预编译,在正则中我们实际用在下面这样的场景中:对于一个经常被重复使用的正则表达式,我们可以先进行一次编译,将这个正则表达式需要做的事预先编译,这样,之后只要用到了这个表达式我们可以将编译好的结果直接拿来用就行了,酱紫大大的省了代码的执行时间!

预编译的使用举例:

比如说,我们想匹配三段(实际中可能是上百个)用到相同规则匹配的字符串中的内容。首先,我们可以将这个规则进行预编译,然后利用这个预编译的结果去匹配每一段需要匹配的字符串:

ss = 'wangh123qwe3123'
ss1 = 'wangh123qwe3123312'
ss2 = 'wangh123qwe3123qwqasd5412'

par = re.compile('\d+')
print(par)
rets = par.findall(ss)
rets1 = par.findall(ss1)
rets2 = par.findall(ss2)
print(rets)
print(rets1)
print(rets2)

看到这里,相信聪明的你又发现问题了!这样固然降低了时间复杂度,但是,如果实际中有几万个大字符串,你这样一下子读取出来,内存不是直接爆炸了么! 对!没错,针对降低空间复杂度的问题,Python为我们提供了另外一个方法————finditer

finditer

需要注意的是,finditer方法得到的是一个迭代器。下面我们模拟匹配10个相同字符串的例子:

re=  re.finditer('\d+','23adq9009qwe'*10)
print(re)
for i in re:
    print(i)
    print(i.group())
#结果(只拿一个结果举例):
#第一个拿到的是re——迭代器对象
<callable_iterator object at 0x000001157E942940>
#遍历re中拿到的是"结果集"
<_sre.SRE_Match object; span=(0, 2), match='23'>
#最终的结果需要用group()方法取到:
23

下面就来一发compile与finditer结合同时降低时间复杂度与空间复杂度的方法

par = re.compile('\d+')
ret = par.finditer('wanghe123asdh2312asd33'*10)
print(ret)
for i in ret:
    print(i.group())
#结果(只拿前1组):
<callable_iterator object at 0x000001DA20362940>
123
2312
33

“分组”与re模块的结合使用

在实际中,正则表达式的“分组”思想与re模块的方法结合使用的情况非常多

findall与分组

有如下需求:在标签字符串中提取出标签中的内容: 一般的方法是这样的:

s = '<title>whwHERO</title>'
print(ret)
print(ret[0].split('>'))
print(ret[0].split('>')[1].split('<')[0])
#结果:
['>whwHERO<']
['', 'whwHERO<']
whwHERO

这样的方法看起来比较麻烦,正则匹配到以后我们还得用Python的方法去处理。 我们可以利用正则的分组与re模块的方法结合区实现:

ret = re.findall('>(\w+)<',r'<title>whwHERO<\\title>')
print(ret)
#结果:
['whwHERO']

我们可以看到:findall优先显示分组中的内容!简直不要太棒! 这样的效果在本例中效果很棒,但是,在一下情况下我们是不想酱紫的: 如果我们想匹配www.baidu.com或者www.taobao.com。正则表达式利用分组我们可以酱紫写:www\.(baidu|taobao)\.com 但是,与findall方法结合会出现问题:

ret = re.findall('www\.(baidu|taobao)\.com','www.baidu.com')
print(ret)
#结果:
['baidu']

再比如,我们想获取一串字符串中的数字(包括小数):

digit = re.findall('\d+(\.\d+)?',r'1.23+2.33')
print(digit)
#结果:['.23', '.33']

很显然,酱紫的分组优先是我们不想要的。那么如何取消findall下的分组优先呢? 在分组里的最前面加上?:就可以了,上面两个例子我们可以酱紫修改:

ret = re.findall('www\.(?:baidu|taobao)\.com','www.baidu.com')
print(ret)
#结果:['www.baidu.com']
digit = re.findall('\d+(?:\.\d+)?','1.23+2.33')
print(digit)
#结果:
['1.23', '2.33']

split与分组

split与分组结合跟普通的split方法的区别是:与分组结合的话会保留在分组中“切掉”的内容,用一个例子来讲就很直观了:

ret = re.split('\d(\d)','wanghw211whw222www233ee')
print(ret)
#结果:
['wanghw', '1', '1whw', '2', '2www', '3', '3ee']

我们可以看到:匹配的第二个数字在分组中,我们利用匹配到的两个数字进行切割,但是由于只有第二个数字在分组中,因此第一个数字被“干掉”了,切割后的列表只保留了第二个数字!

search与分组

在讲search与分组的关系时,请大家记住下面做数据处理的思想:

在爬虫\数据清洗的过程中最常用的正则表达式的操作
而大多数时候我们并不是把我们要的内容都用正则写出来
而是把整个页面都用正则描述下来,然后把我需要的内容放在分组里
这样就能够通过分组取到我想要的内容了

(心中默念一遍后)看下面这个例子: 比如说我们从网页中爬取了下面字符串(当然实际中要大得多,这里只做模拟)

s="<title>whwisahero<\title>"

我们想用search方法获取里面的内容,根据上面介绍的处理数据的思路,我们先利用search拿到结果集,然后在这个结果集中取数据:

ret = re.search(r'<(\w+)>(\w+)<\\(\w+)>',r'<title>whwisahero<\title>')
print(ret.group())
print(ret.group(0))
print(ret.group(1))
print(ret.group(2))
print(ret.group(3))
#结果:
<title>whwhwhw<\title>
<title>whwhwhw<\title>
title
whwisahero (这是我们想要的内容)
title

酱紫我们就拿到了结果。 但是,相信聪明的你叕叕叕叕叕发现问题了!我怎么知道我想要的内容在哪个索引里!没错!这里就需要我们利用分组命名了!

ret = re.search('<(?P<title>\w+)>(?P<content>\w+)</(?P<title2>\w+)>',r'<title>whwisahero</title>')
print(ret.group('title'))
print(ret.group('title2'))
print(ret.group('content'))
#结果:
title
title
whwisahero

可以看到,我们在分组内部的前面利用?P<分组名>的语法为分组起了个名字,在最后取值的时候将分组名加在group方法的参数里就OK了! 另外,分组名还可以这么玩: 如果两个分组名一样可以酱紫写:

ret1 = re.search('<(?P<title>\w+)>(?P<content>\w+)</(?P=title)>',r'<title>whwisahero</title>')
print(ret1.group('title'))
print(ret1.group('content'))
#结果:
title
whwisahero

也可以酱紫写:

ret2 = re.search(r'<(?P<title>\w+)>(?P<content>\w+)</\1>',r'<title>whwisahero</title>')
print(ret2.group('title'))
print(ret2.group('content'))
#结果:
title
whwisahero

筛选数据的思想

接着跟大家下筛选数据的思路:

当我们要匹配的内容混在不想匹配的内容中
只能把不想要的也匹配出来,然后去掉不想要的就是想要的

看下面的例子: 我想拿到一串字符串中的正整数,利用上面的思路可以酱紫做:先将不需要的数据也匹配出来,然后进行进一步的加工。因为实际中正则表达式并不是万能或者一劳永逸的,得到的数据还需要我们进行进一步的处理:

ret = re.findall('-*\d+\.\d+|(-*\d+)','2*(60+(-40.35/5)-(-4*3))')
print(ret)#['2', '60', '', '5', '-4', '3']
for i in ret:
    if not i or i.startswith('-'):
        ret.remove(i)
print(ret)#['2', '60', '5', '3']

常见的几个正则表达式例子:

最后给大家分享常见的几个正则表达式:

#邮箱的规则:
    (1)@之前必须有内容且只能是字母(大小写)、数字、下划线(_)、减号(-)、点(.)
    (2)@和最后一个点(.)之间必须有内容且只能是字母(大小写)、数字、点(.)、减号(-),且两个点不能挨着
    (3)最后一个点(.)之后必须有内容且内容只能是字母(大小写)、数字且长度为大于等于2个字节,小于等于6个字节
正则表达式:[\w\-\.]+@([a-zA-Z\d\-]+\.)+[a-zA-Z\d]{2,6}
1.整数或者小数 包括正数和负数
-?\d+(\.\d+)?

2.年月日
2018-9-20
\d{1,4}-\d{1,2}-\d{1,2}
\d{1,4}-(1[0-2]|0?[1-9])-(3[01]|[12]\d|0?[1-9])

3.匹配qq号
4位 11位
[1-9]\d{4,11}

4.8-10位的密码 数字字母下划线
\w{8,10}

5.验证码
 [\da-zA-Z]{4}

re有关的爬虫练习

简单版本

import re
import json
from urllib.request import urlopen

def getPage(url):
    response = urlopen(url)
    return response.read().decode('utf-8')

def parsePage(s):
    com = re.compile(
        '<div class="item">.*?<div class="pic">.*?<em .*?>(?P<id>\d+).*?<span class="title">(?P<title>.*?)</span>'
        '.*?<span class="rating_num" .*?>(?P<rating_num>.*?)</span>.*?<span>(?P<comment_num>.*?)评价</span>', re.S)

    ret = com.finditer(s)
    for i in ret:
        yield {
            "id": i.group("id"),
            "title": i.group("title"),
            "rating_num": i.group("rating_num"),
            "comment_num": i.group("comment_num"),
        }


def main(num):
    url = 'https://movie.douban.com/top250?start=%s&filter=' % num
    response_html = getPage(url)
    ret = parsePage(response_html)
    print(ret)
    f = open("move_info7", "a", encoding="utf8")

    for obj in ret:
        print(obj)
        data = str(obj)
        f.write(data + "\n")

count = 0
for i in range(10):
    main(count)
    count += 25

优化版

import requests

import re
import json


def getPage(url):
    response = requests.get(url)
    return response.text


def parsePage(s):
    com = re.compile(
        '<div class="item">.*?<div class="pic">.*?<em .*?>(?P<id>\d+).*?<span class="title">(?P<title>.*?)</span>'
        '.*?<span class="rating_num" .*?>(?P<rating_num>.*?)</span>.*?<span>(?P<comment_num>.*?)评价</span>', re.S)

    ret = com.finditer(s)
    for i in ret:
        yield {
            "id": i.group("id"),
            "title": i.group("title"),
            "rating_num": i.group("rating_num"),
            "comment_num": i.group("comment_num"),
        }


def main(num):
    url = 'https://movie.douban.com/top250?start=%s&filter=' % num
    response_html = getPage(url)
    ret = parsePage(response_html)
    print(ret)
    f = open("move_info7", "a", encoding="utf8")

    for obj in ret:
        print(obj)
        data = json.dumps(obj, ensure_ascii=False)
        f.write(data + "\n")


if __name__ == '__main__':
    count = 0
    for i in range(10):
        main(count)
        count += 25

实际校验的一个例子

校验密码:长度再8~24之间并且包含大小写字母数字特殊字符中的至少三种

自己练习的例子

re模块小结