本教程所有源码下载链接:https://share.weiyun.com/5xmFeUO 密码:fzwh6g
正则表达式以及re库的用法
简介
正则表达式,又称规则表达式,英文Regular Expression,常简写为regex、regexp或者RE;它通常被用来快速检索、替换那些符合某个正则表达式的文本。
正则表达式的优势,决定了我们需要学习它:
- 具有很强的灵活性和逻辑性,同时功能性也非常强;
- 可以用及其简单的正则表达式找寻复杂多变的字符串;
但是对于新手而言,掌握它的使用方法又是比较困难。
re库是一个Python内置的用于进行一系列正则表达式操作的库。使用它,我们可以方便的使用正则表达式对字符串进行操作。它可以将一个正则表达式字符串编译为一个正则表达式特征,从而表达具有相同特征的字符串。
例如:我们有这样一组字符串:HI
、HII
、HIII
、HIIII
、……、HIIIIIII...
,那么,就可以用正则表达式HI+
来表达这一组无穷字符串。
正则表达式的用法
正则表达式的常用操作符
这些操作符是组成正则表达式的基本单元,因此,我们需要熟悉它们:
操作符 | 含义 | 例子 | ||
---|---|---|---|---|
. |
表示任意的单个字符 | |||
[] |
对单个字符设定取值范围,字符集 | [a-z] 表示a到z单个字符 |
||
[^] |
对单个字符设定排除范围,非字符集 | [^xyz] 表示非x非y非z的单个字符 |
||
* |
前一个字符,出现0次或者无限次扩展 | xyz* 表示xy,xyz,xyzz,xyzzz,… |
||
+ |
前一个字符,出现1次或无限次扩展 | xyz+ 表示xyz,xyzz,xyzzz,… |
||
? |
前一个字符,出现0次或者1次扩展 | xyz? 表示xy,xyz |
||
` | ` | 左右表达式任意一个字符串 | `abc | xyz`表示abc,xyz |
{m} |
重复前一个字符m次 | xy{2}z 表示xyyz |
||
{m,n} |
重复前一个字符m到n次,前后包含 | xy{2,3}z 表示xyyz,xyyyz |
||
^ |
匹配开头,匹配字符串的开头 | ^xyz 表示xyz在一个字符串的开头 |
||
$ |
匹配结尾,匹配字符串的结尾 | xyz$ 表示xyz在一个字符串的结尾 |
||
() |
分组标记,里面只能使用\ | 操作符 | `(abc | xyz)`表示abc,xyz |
\d |
匹配任意一个0-9的数字 | 相当于[0-9] | ||
\w |
非特殊字符并且非标点符号 | 相当于[a-zA-Z0-9] |
^
这个符号叫做异或符。
看几个小例子:
正则表达式 | 表示的字符串 | |||
---|---|---|---|---|
f(r\ | ri\ | rie\ | rien)?d | fd,frd,frid,fried,friend |
boy+ | boy,boyy,boyyy,boyyy,… … | |||
frien{:4}d | fried,friend,friennd,friennnd,friennnnd |
常用正则表达式的实例
这里总结了一些常用的正则表达式,既能够达到练手的目的,也能够方便日后直接使用。需要注意的是,这些常用正则表达式不一定精确,有时只在特定的业务背景下,能够得到自己想要的结果。
正则表达式 | 含义 | |||
---|---|---|---|---|
^[A-Za-z]+$ |
26个字母组成的字符串 | |||
^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
匹配Email地址 | |||
[1-9]\d{5} |
中国邮政编码 | |||
` ^([1-9]\d{5}[12]\d{3}(0[1-9] | 1[012])(0[1-9] | [12][0-9] | 3[01])\d{3}[0-9xX])$` | 匹配身份证 |
\d+.\d+.\d+.\d+ |
匹配ip地址,提取时有用 | |||
^[1-9]\d*$ |
匹配正整数 | |||
` ^[1-9]\d* | 0$` | 匹配非负整数(正整数 + 0) | ||
^[A-Z]+$ |
匹配由26个英文字母的大写组成的字符串 | |||
^[a-z]+$ |
匹配由26个英文字母的小写组成的字符串 | |||
^[A-Za-z0-9]+$ |
匹配由数字和26个英文字母组成的字符串 | |||
^w+$ |
匹配由数字、26个英文字母或者下划线组成的字符串 | |||
[\u4e00-\u9fa5] |
匹配中文字符 |
re库的使用方法
re库是Python的标准库,主要用于字符串的匹配。
使用时,导入re即可:
1 | import re |
正则表达式的表示类型
raw string类型,也叫原生字符串类型,指不包含转义字符的字符串。即,原生字符串中的转义字符
\
当做普通字符,不具有转义功能。re库采用raw string类型表示正则表达式,格式为:
r'[1-9]\d{5}'
或者r"[1-9]\d{5}"
。string类型,字符串类型,将
\
理解为转义字符,因此写起来比较繁琐。例如,上面的邮政编码正则表达式,就必须写为:
'[1-9]\\d{5}'
,第1个斜杠为转义字符标识,将第2个字符转义为普通的斜杠,从而表示\d
。
因此,我们推荐,当正则表达式中包含转义字符\
的时候,使用raw string类型表示。
re库的常用函数
函数 | 含义 |
---|---|
re.findall() | 返回列表类型,返回匹配正则表达式的全部子字符串 |
re.match() | 返回match对象,从字符串的开始位置起,匹配正则表达式 |
re.search() | 返回match对象,在字符串中搜索和正则表达式相匹配的第一个位置 |
re.sub() | 在字符串中替换掉所有匹配正则表达式的子字符串,返回替换后的字符串 |
re.finditer() | 在字符串中搜索匹配正则表达式的子字符串,返回迭代类型,其中元素是match对象 |
re.split() | 将字符串按照正则表达式进行匹配,将字符串匹配正则表达式的部分割开并返回一个列表 |
下面,我们对这些函数进行详细解释以及在ipython
中测试使用:
re.search(pattern,string,flags=0)
pattern:正则表达式的字符串或原生字符串表示;
string:待匹配字符串;
flags:正则表达式使用时的控制标记。
| 常用标记 | 说明 |
| ——————- | ———————————————————— |
| re.I(re.IGNORECASE) | 忽略正则表达式的大小写,例如,[A-Z]
可以匹配小写字符 |
| re.M(re.MULTILINE) |^
操作符能够将给定字符串的每行当做匹配开始,例如,字符串是一篇文章由多个段落组成,那么可以匹配每一行,并且从每一行的开始匹配 |
| re.S(re.DOTALL) |.
操作符能够匹配所有字符,默认匹配除换行外的所有字符,如果设置了,将可以匹配所有字符包括换行 |
例子,匹配字符串中的邮政编码,示例字符串为
北京海淀 100036
:1
2
3
4
5
6In [10]: match = re.search(r'[1-9]\d{5}', '北京海淀 100036')
In [11]: if match:
...: print(match.group(0))
...:
100036re.match(pattern,string,flags=0)
- 参数和
re.search()
的一致
例子,匹配字符串中的邮政编码,示例字符串为
北京海淀 100036
:1
2
3
4
5
6
7
8
9In [12]: match = re.match(r'[1-9]\d{5}', '北京海淀 100036')
In [13]: match.group(0)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-13-4d972d6c40f1> in <module>()
----> 1 match.group(0)
AttributeError: 'NoneType' object has no attribute 'group'报错了。也就是说这个
match
是空的。因为,re.match()
是从起始位置开始匹配,所以没有匹配到数据。验证了re.match()
是从开始位置进行匹配。1
2
3In [14]: match = re.match(r'[1-9]\d{5}', '100036 北京海淀')
In [15]: match.group(0)
Out[15]: '100036'- 参数和
re.findall(pattern,string,flags=0)
- 参数和
re.search()
的一致
例子,字符串为
北京海淀100036 郑州高新450001
:1
2
3In [16]: ls = re.findall(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001')
In [17]: ls
Out[17]: ['100036', '450001']- 参数和
re.split(pattern,string,maxsplit=0,flags=0)
:- maxsplit:最大分割数,剩余部分作为最后一个元素输出;
- 其他三个参数和上面的一致。
例子1,测试字符串为
北京海淀100036 郑州高新450001
:1
2
3
4In [18]: ls = re.split(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001')
In [19]: ls
Out[19]: ['北京海淀', ' 郑州高新', '']例子2,增加参数
maxsplit=1
:1
2
3
4In [20]: ls = re.split(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001', maxsplit=1)
In [21]: ls
Out[21]: ['北京海淀', ' 郑州高新450001']
例子3,是否保留匹配项的用法,给正则表达式加上小括号()
,则保留匹配项,反之不保留:
1 | In [22]: ls = re.split(r'([1-9]\d{5})', '北京海淀100036 郑州高新450001') |
re.findall(pattern,string,flags=0)
:- 参数和
re.search()
的一致
例子,测试字符串为
北京海淀100036 郑州高新450001
:1
2
3
4
5In [24]: for m in re.finditer(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001'):
...: print(m.group(0))
...:
100036
450001- 参数和
re.sub(pattern,repl,string,count=0,flag=0)
:- repl:替换匹配字符串的字符串,匹配到谁,就用repl将它替换;
- count:匹配的最大替换次数;
- 剩余参数和其他re函数一致。
例子,字符串为
北京海淀100036 郑州高新450001
:1
2
3
4
5
6In [29]: re.sub(r'[1-9]\d{5}',repl=":邮编",string="北京海淀100036 郑州高新450001")
Out[29]: '北京海淀:邮编 郑州高新:邮编'
# 将count=1传入
In [30]: re.sub(r'[1-9]\d{5}',repl=":邮编",string="北京海淀100036 郑州高新450001",count=1)
Out[30]: '北京海淀:邮编 郑州高新450001'
面向对象的用法
上面的讲解中,类似re.findall()
的用法,叫做函数式用法
;我们也可以使用面向对象的思想,来调用这些方法:
1 | In [31]: regex = re.compile(r'[1-9]\d{5}') |
优点:当我们需要多次使用同一个正则表达式规则时,特别方便,可以重复使用pattern
这个对象来调用不同的方法达到目的。可以提高匹配速度。
regex = re.compile(pattern, flags=0)
:
该函数根据包含的正则表达式的字符串创建模式对象,将正则表达式的字符串形式编译成正则表达式对象。
注意1:没有经过编译的正则表达式字符串仅仅是一种表达形式,只有经过编译的正则表达式字符串才能形成一个正则表达式对象,它表示了一组符合规则的字符串。
注意2:经过编译后得到的正则表达式对象,可以调用的方法和re
调用的函数一致,请注意,由于正则表达式已经被编译为模式对象,因此,通过模式对象regex
调用相应方法的时候,方法的参数pattern
不再需要提供。
re库的match对象
我们在前面的例子中,曾经用到一个对象match
,这个对象的类型是SRE_Match
:
1 | In [14]: match = re.match(r'[1-9]\d{5}', '100036 北京海淀') |
Match对象的常用属性有4个:
属性 | 含义 |
---|---|
.re |
匹配时使用的pattern对象(正则表达式) |
.string |
待匹配的文本 |
.pos |
搜索文本的开始位置 |
.endpos |
搜索文本的结束位置 |
我们在ipython
中使用一下:
1 | In [34]: match.re |
Match对象常用的方法:
方法 | 含义 |
---|---|
.group(0) |
获得匹配后的字符串 |
.start() |
匹配字符串在原始字符串的开始位置 |
.end() |
匹配字符串在原始字符串的结束位置 |
.span() |
返回(.start(),end()) |
我们在ipython
中使用一下:
1 | # 匹配的结构存储在这里 |
贪婪匹配和最小匹配
首先看一个例子:
1 | In [49]: match = re.search(r'one.*n','oneanbncndnen') |
g.*n
这个正则表达式可以匹配多个字符串,例如,gitopen、gin、等,但是,结果却返回最长的那个匹配字符串gitaabbccddopen
。
re库默认采用贪婪匹配,即输出匹配最长的子字符串。
但是有的时候,我们需要输出最短的那个子字符串,这时候,需要使用正则表达式的最小匹配:
1 | In [51]: match = re.search(r'one.*?n','oneanbncndnen') |
re库的最小匹配,需要对以下几个操作符进行扩展:
操作符 | 含义 |
---|---|
*? |
前一个字符0次或者无限次扩展,最小匹配 |
+? |
前一个字符1次或者无限次扩展,最小匹配 |
?? |
前一个字符0次或者1次扩展,最小匹配 |
{m,n}? |
扩展前一个字符m到n次,含n次,最小匹配 |
如果re库的操作符匹配不同长度的字符串的话,都可以在操作符后面加上?
进行最小匹配。
实战——定向爬取京东100页商品信息
问题0:按照销量进行爬取,这样可以排除页面中广告
得到的连接如下:
1 | https://search.jd.com/Search?keyword=书包&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=1&s=1&click=0 |
从浏览器开发者工具中可以得到这些GET请求的参数,如下:
1 | { |
这样,我们得到了用requests
发送GET说必须的连接和参数,连接为https:// search.jd.com/Search
,参数为上面的这个字典。其中,需要我们控制的几个参数为:keyword
、s
、page
问题1:请求链接的构造,page竟然是奇数?!
在京东搜索商品以后,我们会来搜索页面,这时观察页面的url不难发现一个规律,拼接页面url的时候的page参数,需要传入的数字为奇数。经过计算发现,总共100页搜索结果,如果我们想获取全部信息,就必须从把page的值赋值为200以内的奇数,即1,3,5,7,9,...,199
。
如:
1 | 第1页 page=1 |
问题2:编写页面请求,得到的页面中只有30个商品信息?!
当我们在编写程序的时候,用requests
库请求回来的html文本内容中,属性为class='gl-item'
的li
标签只有30个。什么?刚才不是数过了吗?一共60个呀。为什么是30个呢?
打开浏览器的开发者工具栏,将页面从上往下慢慢拖动,并且观察Network
中的网络请求,突然,有一个神秘的请求出现了,它的连接为https://search.jd.com/s_new.php?keyword=%E4%B9%A6%E5%8C%85&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=2&s=31&scrolling=y&log_id=1529934788.49163&tpl=3_M&show_items=4208541,12596174640,2804865,877258,4593360,3598223,5308846,11031192090,4028698,5471930,6511832,765798,3796277,6530586,311349,1015774,10552271564,6044018,5025051,10552555536,28941008721,1106925,10285134151,4120808,10492711739,4044524,25595327933,6401561,5181576,2437187963
,这是什么东西?
然后查看这个链接的Response
,搜索gl-item
,我们发现,竟然也有30个!那么。。。刚才的30加上现在的30,不就是60个商品?仔细观察这个链接,我们发现了一个某东做的小手脚!快看,连接中的page=2
!So,~~~~,似乎明白了什么。
原来,我们在问题1中得到的搜索页面一共有100页,实际上有200页,奇数页就是我们直接看到的搜索结果页面,一共请求到30个商品信息,而偶数页,则是当用户拖动滚动条的时候,看完了30个,就会自动后台请求另外30个商品,这后来请求的30个,就是偶数页的信息,并且动态的添加到页面上去。
请求的基本链接为https://search.jd.com/s_new.php
,请求的基本参数我们提取为字典,其中需要控制的参数为:keyword
、page
、s
、log_id
、show_items
。
log_id
和show_items
必须从奇数页请求结果中提取,show_items
是用一个列表转化成的字符串,其中数字是,奇数页中每个带有class='gl-item'
属性的div,它的data-pid
属性的值。
1 | { |
问题3:讲了这么多我们该如何获取到60个商品信息呢?
循环遍历,然后判断页码的奇偶性,根据奇偶性发送不同连接不同请求参数的请求,得到不同的结果进行内容解析。
整个程序的源码如下:
1 | import requests |