<居然讲爬虫>3-数据解析之正则表达式

235 阅读9分钟

我们前面说到如何用requests模块来发送请求,获取响应。那获取到的响应数据,所有的都是我们想要的吗? 肯定是有一些数据是我们不想要的,那这个时候我们就要提取出我们想要的数据。

简单的来说,数据解析就是从响应中获取我们想要的数据的过程

数据的分类

如果我们想要的数据可以直接在网页的源代码中可以找到,这个时候我们可以用正则表达式,xpath、bs4等方式来提取到数据,如果想要的数据并非在网页源码中,这个时候我们可以找到数据接口,一般数据接口返回的数据都是json类型,我们只需要转换成Python中的数据类型就可以了。

数据提取的方式当然不止我上面提到的,这里只给大家说一些常用的。

正则表达式

我们先来看数据提取中的正则表达式,假设我们现在获取到的网页有很多的数据,但是我只想要以hello开头的语句,或者我想要含有hello的语句,还有我想要其中的电话和邮箱。

面对这种需求,我们可以借助正则表达式来完成。

正则表达式(regular expression)是一种工具,一种广泛用于匹配字符串的工具。它用一个"字符串"来描述一个特征,然后去验证另一个"字符串"是否符合这个特征。

正则表达式语法

正则表达式本质上只做一件事,那就是编写一个表达式“字符串”,然后用这个字符串去匹配目标文本。核心的核心,都在编写这个“字符串”表达式上面。

在Python中需要通过正则表达式对字符串进行匹配的时候,可以使用一个模块,名字为re

re模块的使用过程

# 导入re模块
import re

# 使用match方法进行匹配操作
result = re.match(正则表达式,要匹配的字符串)

# 如果上一步匹配到数据的话,可以使用group方法来提取数据
result.group()

关于正则表达式我们可以写普通字符、元字符、 预定义匹配字符集,我们先来看下普通字符

普通字符

字母、数字、汉字、下划线、以及没有特殊定义的符号,都是"普通字符"。正则表达式中的普通字符,在匹配的时候,只匹配与自身相同的一个字符。

u=1684198633,110338868&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto.webp

In [2]: import re

In [3]: # re.match('正则表达式', '要匹配的字符串')

In [4]: r = re.match(r'居然', '居然')

In [6]: r.group
Out[6]: <function SRE_Match.group>

In [7]: r.group()
Out[7]: '居然'

In [8]: r = re.match(r'居然1', '居然')
# 由于没有匹配到结果是None,所以group()是取不到结果的
In [9]: r.group()
AttributeError                            Traceback (most recent call last)
<ipython-input-4-5f54aece06a4> in <module>
----> 1 r.group()

AttributeError: 'NoneType' object has no attribute 'group'

元字符

正则表达式中使用了很多元字符,用来表示一些特殊的含义或功能。

表达式匹配
.小数点可以匹配除了换行符\n以外的任意一个字符
|逻辑或操作符
[]匹配字符集中的一个字符
[^]对字符集求反,也就是上面的反操作。尖号必须在方括号里的最前面
-定义[]里的一个字符区间,例如[a-z]
\对紧跟其后的一个字符进行转义
()对表达式进行分组,将圆括号内的内容当做一个整体,并获得匹配的值

例如:

  • a.c匹配abc
  • (a|b)c匹配ac与bc
  • [abc]1匹配a1或者b1或者c1使用方括号[]包含一系列字符,能够匹配其中任意一个字符。
  • 用[^]包含一系列字符,则能够匹配其中字符之外的任意一个字符。
  • [ab5@]匹配a或b或5或@
  • [^abc]匹配a,b,c之外的任意一个字符
  • [f-k]匹配f~k 之间的任意一个字母
  • [^A-F0-3]匹配A-F以及0~3之外的任意一个字符
  • a\.c匹配a.c

预定义匹配字符集

正则表达式中的一些表示方法,可以同时匹配某个预定义字符集中的任意一个字符。比如,表达式\d可以匹配任意一个数字。虽然可以匹配其中任意字符,但是只能是一个,不是多个。

表达式匹配
\d任意一个数字,0~9 中的任意一个
\w任意一个字母或数字或下划线,也就是 A-Z,a-z,0-9,_ 中的任意一个
\s空格、制表符、换页符等空白字符的其中任意一个
\D\d的反集,也就是非数字的任意一个字符,等同于[^\d]
\W\w的反集,也就是[^\w]
\S\s的反集,也就是[^\s]

例如表达式\d\d,在匹配abc123时,匹配的结果是:成功;匹配到的内容是12;匹配到的位置开始于3,结束于5。 (前提是用search来匹配,用match是匹配不到的)

重复匹配

前面的表达式,无论是只能匹配一种字符的表达式,还是可以匹配多种字符其中任意一个的表达式,都只能匹配一次。但是有时候我们需要对某个片段进行重复匹配,例如手机号码13666666666,一般的新手可能会写成\d\d\d\d\d\d\d\d\d\d\d(注意,这不是一个恰当的表达式),不但写着费劲,看着也累,还不一定准确恰当。

这种情况可以使用表达式再加上修饰匹配次数的特殊符号{},不用重复书写表达式就可以重复匹配。比如[abcd][abcd]可以写成[abcd]{2}

表达式匹配
{n}表达式重复n次,比如\d{2}相当于\d\d,a{3}相当于aaa
{m,n}表达式至少重复m次,最多重复n次。比如ab{1,3}可以匹配ababbabbb
{m,}表达式至少重复m次,比如\w\d{2,}可以匹配a12,_1111,M123等等
?匹配表达式0次或者1次,相当于{0,1},比如a[cd]?可以匹配a,ac,ad
+表达式至少出现1次,相当于{1,},比如a+b可以匹配ab,aab,aaab等等
*表达式出现0次到任意次,相当于{0,},比如\^*b可以匹配b,^^^b等等

位置匹配

有时候,我们对匹配出现的位置有要求,比如开头、结尾、单词之间等等。

表达式匹配
^在字符串开始的地方匹配,符号本身不匹配任何字符
$在字符串结束的地方匹配,符号本身不匹配任何字符
\b匹配一个单词边界,也就是单词和空格之间的位置,符号本身不匹配任何字符
\B匹配非单词边界,即左右两边都是\w范围或者左右两边都不是\w范围时的字符缝隙

例如表达式^aaa在匹配xxx aaa xxx时,匹配结果是:失败。因为^要求在字符串开始的地方匹配。

表达式aaa$在匹配xxx aaa xxx时,匹配结果是:失败。因为$要求在字符串结束的地方匹配。

表达式.\b.在匹配@@@abc时,匹配结果是:成功;匹配到的内容是@a;匹配到的位置开始于2,结束于4。

表达式\bend\b在匹配weekend,endfor,end时,匹配结果是:成功;匹配到的内容是end;匹配到的位置开始于15,结束于18。

贪婪与非贪婪模式

在重复匹配时,正则表达式默认总是尽可能多的匹配,这被称为贪婪模式。比如,针对文本dxxxdxxxd,表达式(d)(\w+)(d)中的\w+将匹配第一个d和最后一个d之间的所有字符xxxdxxx。可见,\w+在匹配的时候,总是尽可能多的匹配符合它规则的字符。同理,带有?*{m,n}的重复匹配表达式都是尽可能地多匹配。

但是有时候,这种模式不是我们想要的结果,比如最常见的HTML标签匹配。假设有如下的字符串:

<table>
    <tr>
        <td>苹果</td>
        <td>桃子</td>
        <td>香蕉</td>
    </tr>
</table>

我们的意图是获取每个<td></td>标签中的元素内容,那么如果你将正则表达式写成<td>(.*)</td>的话,你得到的是<td>苹果</td><td>桃子</td><td>香蕉</td>这么个东西,而不是“苹果”、“桃子”、“香蕉”。

在修饰匹配次数的特殊符号后再加上一个?问号,则可以使匹配次数不定的表达式尽可能少的匹配,使可匹配可不匹配的表达式,尽可能的"不匹配"。如果少匹配就会导致整个表达式匹配失败的时候,与贪婪模式类似,非贪婪模式会最小限度的再多匹配一些,以使整个表达式匹配成功。

表达式<td>(.*?)</td>匹配上面的字符串时,将只得到<td>苹果</td>,再次匹配下一个时,可以得到<td>桃子</td>,以此类推。

import re

s = """<table><tr><td>苹果</td><td>桃子</td><td>香蕉</td></tr></table>"""

r = re.search(r'<td>(.*?)</td>', s).group()

print(r)

常用正则表达式

校验数字的相关表达式:

功能表达式
数字^[0-9]*$
n位的数字^\d{n}$
至少n位的数字^\d{n,}$
有两位小数的正实数^[0-9]+(.[0-9]{2})?$
非零的负整数^-[1-9]\d*$
非负浮点数^\d+(\.\d+)?$
浮点数^(-?\d+)(\.\d+)?$

特殊场景的表达式:

功能表达式
Email地址^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
日期格式^\d{4}-\d{1,2}-\d{1,2}
空白行的正则表达式\n\s*\r (可以用来删除空白行)
IP地址提取\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)
腾讯QQ号[1-9][0-9]{4,} (腾讯QQ号从10000开始)

小练习

大家可以先别运行,根据列表中的值,自己先判断哪些可以匹配出来

import re


def fn(ptn, lst):
    """
    :param ptn: 模板
    :param lst: 列表 里面放有待匹配的数据
    :return:
    """
    for i in lst:
        result = re.match(ptn, i)
        if result:
            print(i, "匹配成功!匹配结果是", result.group())
        else:
            print(i, "匹配失败!")


lst = ['abc1', 'ab', 'aba', 'abbcd', 'other']
ptn = r'ab.'
fn(ptn, lst)


lst = ['man', 'men', 'mdn', 'nba', 'mon']
ptn = r'm[aeo]n'
fn(ptn, lst)


lst = ['py2', 'py3', 'other', 'pyxx']
ptn = 'py\d'
fn(ptn, lst)


# \D是对\d取反
lst = ['py2', 'py3', 'other', 'pyxx']
ptn = r'py\D'
fn(ptn, lst)


# \s 空格、制表符(tab)等空白字符的其中任意一个
lst = ['hello world', 'helloxxxx', 'hello,world']
ptn = r'hello\sworld'
fn(ptn, lst)


# 任意一个字母或者数字或者下划线,也就是A-Z,a-z,0-9,_中的任意一个
lst = ['1-age', 'a-age', '#-age', '_-age']
ptn = r'\w-age'
fn(ptn, lst)


lst = ['hello', 'python', 'xxx', 'h']
ptn = r'h[a-z]*'
fn(ptn, lst)


lst = ['hello', 'python', 'xxx', 'h']
ptn = r'h[a-z]+'
fn(ptn, lst)

re模块常用的方法

在Python中,通过内置的re模块提供对正则表达式的支持。提供了下面的方法进行字符串的查找、替换和分割等各种处理操作

方法描述返回值
compile(pattern[, flags])根据包含正则表达式的字符串创建模式对象re对象
search(pattern, string[, flags])在字符串中查找第一个匹配到的对象或者None
match(pattern, string[, flags])在字符串的开始处匹配模式在字符串开头匹配到的对象或者None
split(pattern, string[, maxsplit=0,flags])根据模式的匹配项来分割字符串分割后的字符串列表
findall(pattern, string,flags)列出字符串中模式的所有匹配项所有匹配到的字符串列表
sub(pat,repl, string[,count=0,flags])将字符串中所有的pat的匹配项用repl替换完成替换后的新字符串
finditer(pattern, string,flags)将所有匹配到的项生成一个迭代器所有匹配到的字符串组合成的迭代器
subn(pat,repl, string[,count=0,flags])在替换字符串后,同时报告替换的次数完成替换后的新字符串及替换次数
escape(string)将字符串中所有特殊正则表达式字符串转义转义后的字符串
purge(pattern)清空正则表达式
template(pattern[,flags])编译一个匹配模板模式对象
fullmatch(pattern, string[, flags])match方法的全字符串匹配版本类似match的返回值

compile(pattern, flags=0)

这个方法是re模块的工厂方法,用于将字符串形式的正则表达式编译为Pattern模式对象,可以实现更高效率的匹配。第二个参数flag是匹配模式。

使用compile()完成一次转换后,再次使用该匹配模式的时候就不用进行转换了。经过compile()转换的正则表达式对象也能使用普通的re方法。其用法如下:

import re
pat = re.compile(r"abc")
pat.match("abc123")
<_sre.SRE_Match object; span=(0, 3), match='abc'>

经过compile()方法编译过后的返回值是个re对象,它可以调用match()、search()、findall()等其他方法,但其他方法不能调用compile()方法。实际上,match()和search()等方法在使用前,Python内部帮你进行了compile的步骤。

re.match(r"abc","abc123").compile()
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    re.match(r"abc","abc123").compile()
AttributeError: '_sre.SRE_Match' object has no attribute 'compile'

flag匹配模式

Python的re模块提供了一些可选的标志修饰符来控制匹配的模式。可以同时指定多种模式,通过与符号|来设置多种模式共存。如re.I | re.M被设置成I和M模式。

匹配模式 描述
re.A ASCII字符模式
re.I 使匹配对大小写不敏感,也就是不区分大小写的模式
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^$
re.S 使 . 这个通配符能够匹配包括换行在内的所有字符,针对多行匹配
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

match(pattern, string, flags=0)

match()方法会在给定字符串的开头进行匹配,如果匹配不成功则返回None,匹配成功返回一个匹配对象,这个对象有个group()方法,可以将匹配到的字符串给出。

ret = re.match(r"abc","ab1c123")
print(ret)
None
re.match(r"abc","abc123")
<_sre.SRE_Match object; span=(0, 3), match='abc'>
obj = re.match(r"abc","abc123")
obj.group()
'abc'

search(pattern, string, flags=0)

在文本内查找,返回第一个匹配到的字符串。它的返回值类型和使用方法与match()是一样的,唯一的区别就是查找的位置不用固定在文本的开头。

obj = re.search(r"abc","123abc456abc789")
obj
<_sre.SRE_Match object; span=(3, 6), match='abc'>
obj.group()
'abc'

findall(pattern, string, flags=0)

作为re模块的三大搜索函数之一,findall()和match()、search()的不同之处在于,前两者都是单值匹配,找到一个就忽略后面,直接返回不再查找了。而findall是全文查找,它的返回值是一个匹配到的字符串的列表。

obj = re.findall(r"abc","123abc456abc789")
obj
['abc', 'abc']

这个列表没有group()方法,没有start、end、span,更不是一个匹配对象,仅仅是个列表!如果一项都没有匹配到那么返回一个空列表。

obj.group()
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    obj.group()
AttributeError: 'list' object has no attribute 'group'


obj = re.findall(r"ABC","123abc456abc789")
print(obj)
[]

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

re模块的split()方法和字符串的split()方法很相似,都是利用特定的字符去分割字符串。但是re模块的split()可以使用正则表达式,因此更灵活,更强大,而且还有“杀手锏” 。

import re
s = "8+7*5+6/3"
a_list = re.split(r"[\+\-\*\/]", s)
print(a_list)
['8', '7', '5', '6', '3']

split有个参数maxsplit,用于指定分割的次数:

a_list = re.split(r"[\+\-\*\/]", s, maxsplit= 2)
print(a_list)
['8', '7', '5+6/3']

sub(pattern, repl, string, count=0, flags=0)

sub()方法类似字符串的replace()方法,用指定的内容替换匹配到的字符,可以指定替换次数。

s = "i am jack! i am nine years old ! i like swiming!"
import re
s = re.sub(r"i", "I", s)
print(s)
'I am jack! I am nIne years old ! I lIke swImIng!'

分组功能

Python的re模块有一个分组功能。所谓的分组就是去已经匹配到的内容里面再筛选出需要的内容,相当于二次过滤。实现分组靠圆括号(),而获取分组的内容靠的是group()、groups()和groupdict()方法,其实前面我们已经展示过。re模块里的几个重要方法在分组上,有不同的表现形式,需要区别对待。

例一:match()方法,不分组时的情况:

import re
origin = "hasdfi123123safd"
# 不分组时的情况
r = re.match("h\w+", origin)
print(r.group())         # 获取匹配到的整体结果 hasdfi123123safd
print(r.groups())        # 获取模型中匹配到的分组结果元组  ()
print(r.groupdict())     # 获取模型中匹配到的分组中所有key的字典 {}

例二:match()方法,有分组的情况(注意圆括号!)

import re
origin = "hasdfi123123safd123"
# 有分组  (?P<name>)	分组起别名  (?P=name)	引用别名为name分组匹配到的字符串
r = re.match("h(\w+).*(?P<name>\d)$", origin)
print(r.group())  # 获取匹配到的整体结果
print(r.group(1))  # 获取匹配到的分组1的结果
print(r.group(2))  # 获取匹配到的分组2的结果
print(r.groups())  # 获取模型中匹配到的分组结果元组
print(r.groupdict())  # 获取模型中匹配到的分组中所有key的字典

执行结果:
hasdfi123123safd123
asdfi123123safd12
3
('asdfi123123safd12', '3')
{'name': '3'}

例三,search()方法,有分组的情况:

import re
origin = "sdfi1ha23123safd123"      # 注意这里对匹配对象做了下调整
# 有分组
r = re.search("h(\w+).*(?P<name>\d)$", origin)
print(r.group())  
print(r.group(0))  
print(r.group(1))  
print(r.group(2))
print(r.groups())  
print(r.groupdict()) 

执行结果:
ha23123safd123
ha23123safd123
a23123safd12
3
('a23123safd12', '3')
{'name': '3'}

例四,findall()方法,没有分组的情况:

import re
origin = "has something have do"
# 无分组
r = re.findall("h\w+", origin)
print(r)

执行结果:
['has', 'hing', 'have']
# 一切看起来没什么不一样

例五,findall()方法,有一个分组的情况:

import re
origin = "has something have do"
# 一个分组
r = re.findall("h(\w+)", origin)
print(r)

执行结果:
['as', 'ing', 'ave']

例六,findall()方法,有两个以上分组的情况:

import re
origin = "hasabcd something haveabcd do"    # 字符串调整了一下
# 两个分组
r = re.findall("h(\w+)a(bc)d", origin)
print(r)

运行结果:
[('as', 'bc'), ('ave', 'bc')]