vlambda博客
学习文章列表

对着爬虫网页HTML学习Python正则表达式re


正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。


20

  • 1.正则表达式初探

  • 2.用正则表达式匹配更多模式

    • 2.1.利用括号()进行分组

    • 2.2.利用管道|匹配多个分组

    • 2.3.用问号?实现可选匹配

    • 2.4.用星号*实现0次或多次

    • 2.5.用加号+实现1次或多次

    • 2.6.用花括号{}匹配特定次数

  • 3.贪心和非贪心匹配

  • 4.字符类型

  • 5.split()函数

1.正则表达式初探

正则表达式,简称为 regex,是文本模式的描述方法。例如,\d 是一个正则表达式,表示一位数字字符,即任何一位 0 到 9 的数字。Python 使用正则表达式\d\d\d-\d\d\d\d-\d\d\d\d,来匹配3 个数字、一个短横线、4 个数字、一个短横线、4 个数字。所有其他字符串都不能匹配\d\d\d-\d\d\d\d-\d\d\d\d 正则表达式。

在一个表达式后加上花括号包围的 3({3}),就是说,“匹配这个模式 3 次”。所以较短的正则表达式\d{3}-\d{4}-\d{4},也可以匹配正确的手机号码格式。

引入正则表达式库 re,该库是python自带的哈。

In [1]: import re
   ...: # 创建一个regex模式对象
   ...: phoneNum = re.compile(r'\d\d\d-\d\d\d\d-\d\d\d\d')
   ...: # 匹配regex对象
   ...: mo = phoneNum.search('我现在用的电话是188-8888-8888,之前那个186-6666-6666已经不用了')

In [2]: mo.group()
Out[2]: '188-8888-8888'

其实,以下是等价的

# 创建一个regex模式对象,pattern指待匹配的正则表达式
phoneNum = re.compile(pattern)
# 匹配regex对象,string指代匹配的文本内容
mo = phoneNum.search(string)

等价于

mo = phoneNum.search(pattern, string)

如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。

不过,我们发现其实在待匹配的文本内容中出现了2个手机号码,但是re.search()只返回了第一个匹配成功的文本。如何可以获取全部匹配成功的项呢,咱们可以使用re.findall()来进行操作,其返回的结果是由所有匹配组成的列表

In [3]: re.findall(r'\d{3}-\d{4}-\d{4}''我现在用的电话是188-8888-8888,之前那个186-6666-6666已经不用了')
Out[3]: ['188-8888-8888''186-6666-6666']

2.用正则表达式匹配更多模式

在实际解析网页HTML文本的时候,我们可能需要取匹配中某个部分分组文本、或者需要选择性匹配多个文本、又或者对某些字符或者分组需要匹配0/1次或者多次等等。

以下是待解析的某待租房间信息

info= '''
             <h5 class="title sign"><a href="//www.ziroom.com/x/712447913.html" target="_blank" style="line-height: 0.9em;">合租·DBC加州小镇C区4居室-南卧</a></h5>
             <div class="desc">
               <div>23.3㎡ | 5/15层</div>
               <div class="location">
                                      小区距高楼金站步行约178米                                </div>
             </div>
                             <div class="price ">
              <span class="rmb"></span>
                                        <span class="num">188</span>
                               <span class="unit">/天</span>
             </div>
                                             <div class="tag">
                                              <span>可短租</span>
                                                             <span>离地铁近</span>
                                                             <span>米苏4.0</span>
                                           </div>
                                  
'''

对于这种文本,由于存在很多空白字符类如换行、空格等等,我需要先用re.sub()进行简单的清洗。

info = re.sub(r'\s','',info) # \s 匹配任意空白字符

2.1.利用括号()进行分组

比如,我需要匹配子字符中的房间租金信息,因租金为数字但是还有别的一些信息也是数字(如房间大小等),因此我们在匹配的时候需要代入前后一些字符做唯一匹配,但是实际只需要对应的数字文本内容,因此需要进行分组。

<spanclass="num">188</span>

比如以上,我们想要获得价格188,可以使用(\d{3})进行匹配。

注意:这里是的匹配模式是4位数字的精确匹配,在实际的操作中价格可能存在不确定的位置甚至带有小数,我们需要用到更复杂的匹配模式,具体见后续讲解。

In [4]: re.findall(r'<spanclass="num">(\d{3})</span>', info)
Out[4]: ['188']

2.2.利用管道|匹配多个分组

以示例的info文本,在爬虫过程中其价格有时候类型是天或者月,我们匹配的可能就是诸多表达式中的一个,此时可以使用 | 进行操作。正则表达式r“天|月”即可匹配 天 或者 月。

<spanclass="unit">/天</span>
# 或者
<spanclass="unit">/月</span>

我们采用正则表达式 r“天|月” 可实现匹配。

In [5]: re.findall(r'<spanclass="unit">/(月|天)</span>', info)
Out[5]: ['天']

In [6]: s = '<spanclass="unit">/月</span>'
In [7]: re.findall(r'<spanclass="unit">/(月|天)</span>', s)
Out[7]: ['月']

2.3.用问号?实现可选匹配

对于房间的面积,有的可能是整数有的可能是小数,因此小数点及小数点后的数字其实是可选项,为了更好的匹配这个面积文本,我们需要用到问号?。字符?表示它前面的分组在这个模式中是可选的。

<div>23.3㎡|5/15层</div>
# 或者
<div>23㎡|5/15层</div>

我们可以用 r'(\d{2}.?\d?)'来进行匹配,如果为了在整个html里找且怕存在重复,可以用r'(\d{2}.?\d?)|5/15层'。这里需要注意我们在 | 前面加了 转义字符 \,区别于 | 本身,否则可能无法得出正确结果。

In [8]: re.findall(r'<div>(\d{2}\.?\d?)㎡\|5/15层</div>',info)
Out[8]: ['23.3']

In [9]: re.findall(r'<div>(\d{2}\.?\d?)㎡\|5/15层</div>','<div>23㎡|5/15层</div>')
Out[9]: ['23']

2.4.用星号*实现0次或多次

对于楼层信息来说,我们要获取其楼层和楼高,有的可能有楼层信息但是有的可能没有,楼层和楼高可能是个位数或者十位数。这种情况下,我们可以使用星号进行匹配。字符*表示它前面的分组在这个模式中是出现0次或者多次。

<div>23.3㎡|5/15层</div>
# 或者
<div>23㎡|9层</div>

由于楼高是一定存在的,而楼层不一定存在,因为我们可以用r'(\d*)/*(\d+)'来进行匹配,注意字符+代表至少一次,详见后续说明。

In [10]: re.findall(r'<div>\d{2}\.?\d?㎡\|(\d*)/*(\d+)层</div>', info)
Out[10]: [('5''15')]

In [11]: re.findall(r'<div>\d{2}\.?\d?㎡\|(\d*)/*(\d+)层</div>''<div>23㎡|9层</div>')
Out[11]: [('''9')]

2.5.用加号+实现1次或多次

我们在2.4中其实看到了 字符 + 的使用场景,其代表的就是 它前面的分组在这个模式中是出现1次或者多次。

<spanclass="num">188</span>
# 或者
<spanclass="num">1888</span>

我们回到 2.1.中 匹配租金的案例,其实对于租金来说除了3位数之外,租金金额其实是一个大于0的值,也就是至少出现1次数字,因此我们可以用 r'(\d+)' 来匹配。

In [12]: re.findall(r'<spanclass="num">(\d+)</span>', info)
Out[12]: ['188']

In [13]: re.findall(r'<spanclass="num">(\d+)</span>''<spanclass="num">1888</span>')
Out[13]: ['1888']

2.6.用花括号{}匹配特定次数

再以2.3.中的房间面积为例,我们认为房间面积不可能超过3位数、最低1位数 为正常值。如果想要一个分组重复特定次数,就在正则表达式中该分组的后面,跟上花括号包围的数字。例如,正则表达式(Ha){3}将匹配字符串'HaHaHa',但不会匹配'HaHa',因为后者只重复了(Ha)分组两次。

除了一个数字,还可以指定一个范围,即在花括号中写下一个最小值、一个逗号和一个最大值。例如,正则表达式(Ha){3,5}将匹配'HaHaHa'、 'HaHaHaHa'和'HaHaHaHaHa'。 

也可以不写花括号中的第一个或第二个数字, 不限定最小值或最大值。例如,(Ha){3,}将匹配 3 次或更多次实例, (Ha){,5}将匹配 0 到 5 次实例。 

不过,在使用过程中一定要慎重,同样的分组在不同的匹配模式可能带来不同的结果。

In [14]: re.findall(r'(\d{2,3})㎡','<div>3456㎡|5/15层</div>')
Out[14]: ['456']

In [15]: re.findall(r'<div>(\d{2,3})㎡','<div>3456㎡|5/15层</div>')
Out[15]: []

3.贪心和非贪心匹配

Python 的正则表达式默认是“贪心” 的,这表示在有二义的情况下,它们会尽可能匹配最长的字符串。 

在表达式后面加上符号?,即为非贪心匹配。

In [16]: greedyHaRegex = re.compile(r'(Ha){3,5}')

In [17]: mo1 = greedyHaRegex.search('HaHaHaHaHa')

In [18]: mo1.group()
Out[18]: 'HaHaHaHaHa'

In [19]: greedyHaRegex = re.compile(r'(Ha){3,5}?')

In [20]: mo2 = greedyHaRegex.search('HaHaHaHaHa')

In [21]: mo2.group()
Out[21]: 'HaHaHa'

In [22]: re.findall(r'(Ha){3,5}?','HaHaHaHaHa')
Out[22]: ['Ha']

In [23]: re.findall(r'(Ha){3,5}','HaHaHaHaHa')
Out[23]: ['Ha']

In [24]: re.findall(r'((Ha){3,5})','HaHaHaHaHa')
Out[24]: [('HaHaHaHaHa''Ha')]

In [25]: re.findall(r'((Ha){3,5}?)','HaHaHaHaHa')
Out[25]: [('HaHaHa''Ha')]

4.字符类型

模式 描述
^ 匹配字符串的开头
$ 匹配字符串的末尾。
. 匹配任意字符,除了换行符,当.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[...] 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
* 匹配0个或多个的表达式。
+ 匹配1个或多个的表达式。
? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{ n} 精确匹配 n 个前面表达式。例如, o{2} 不能匹配 "Bob" 中的 "o",但是能匹配 "food" 中的两个 o。
{ n,} 匹配 n 个前面表达式。例如, o{2,} 不能匹配"Bob"中的"o",但能匹配 "foooood"中的所有 o。"o{1,}" 等价于 "o+"。"o{0,}" 则等价于 "o*"。
{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b 匹配a或b
() 对正则表达式分组并记住匹配的文本
(?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
(?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
(?: ) 类似 (...), 但是不表示一个组
(?imx: ) 在括号中使用i, m, 或 x 可选标志
(?-imx: ) 在括号中不使用i, m, 或 x 可选标志
(?#...) 注释.
(?= ) 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?! ) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
(?> ) 匹配的独立模式,省去回溯。
\w 匹配字母数字及下划线
\W 匹配非字母数字及下划线
\s 匹配任意空白字符,等价于 [ \t\n\r\f]。
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9].
\D 匹配任意非数字
\A 匹配字符串开始
\Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
\z 匹配字符串结束
\G 匹配最后匹配完成的位置。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
\B 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
\n, \t, 匹配一个换行符,匹配一个制表符
\1...\9 匹配第n个分组的内容。
\10 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。
实例 描述
[Pp]ython 匹配 "Python" 或 "python"
rub[ye] 匹配 "ruby" 或 "rube"
[aeiou] 匹配中括号内的任意一个字母
[0-9] 匹配任何数字。类似于 [0123456789]
[a-z] 匹配任何小写字母
[A-Z] 匹配任何大写字母
[a-zA-Z0-9] 匹配任何字母及数字
[^aeiou] 除了aeiou字母以外的所有字符
[^0-9] 匹配除了数字外的字符

正则匹配模式表

标志 含义
re.S(DOTALL) 使.匹配包括换行在内的所有字符
re.I(IGNORECASE) 使匹配对大小写不敏感
re.L(LOCALE) 做本地化识别(locale-aware)匹配,法语等
re.M(MULTILINE) 多行匹配,影响^和$
re.X(VERBOSE) 该标志通过给予更灵活的格式以便将正则表达式写得更易于理解
re.U 根据Unicode字符集解析字符,这个标志影响\w,\W,\b,\B

5.split()函数

根据正则匹配分割字符串,返回分割后的一个列表split(pattern, string, maxsplit=0, flags=0)

pattern:正则模型
string :要匹配的字符串
maxsplit:指定分割个数
flags  :匹配模式

当我们获取了全部房源信息后,需要对一些信息进行二次解析,比如房屋信息的解析。

In [26]: # 房屋信息解析
     ...: s1 = '合租·李村东里3居室-北卧'
     ...: s2 = '合租·强佑·府学上院4居室-北卧'
     ...: s3 = '整租·铁二区1室1厅-北'
     ...: s4 = '整租·厂甸11号院1室1厅-东'
     ...: s5 = '整租·牛街182室1厅-西'

In [27]: re.split(r'(\S*?)·(.*)(\d居*室.*)-(.*)',s1)
Out[27]: ['''合租''李村东里''3居室''北卧''']

In [28]: re.split(r'(\S*?)·(.*)(\d居*室.*)-(.*)',s2)
Out[28]: ['''合租''强佑·府学上院''4居室''北卧''']

In [29]: re.split(r'(\S*?)·(.*)(\d居*室.*)-(.*)',s3)
Out[29]: ['''整租''铁二区''1室1厅''北''']

In [30]: re.split(r'(\S*?)·(.*)(\d居*室.*)-(.*)',s4)
Out[30]: ['''整租''厂甸11号院''1室1厅''东''']

In [31]: re.split(r'(\S*?)·(.*)(\d居*室.*)-(.*)',s5)
Out[31]: ['''整租''牛街18''2室1厅''西''']

大家可以尝试更多种正则表达式匹配规则,比如能把前后的空字符串去掉的等等。

如果我们要解析出 房间面积、楼层和楼高信息,观测数据发现存在以下3种情况,大家觉得怎么写正则表达式能实现呢?

# 房间信息解析
# 我们在数据处理中发现存在异常数据(楼层如 7层 或 -1/5层)
s1 = '87.26㎡|11/29层'
s2 = '87㎡|7层'
s3 = '8.6㎡|-1/5层'
- END -

参考:

① https://docs.python.org/zh-cn/3.7/library/re.html

②《Python编程快速上手  让繁琐工作自动化》

③ https://www.runoob.com/python/python-reg-expressions.html




往期推荐




 默默关注才哥

然后惊艳所有人

可以叫我才哥



                 我就知道你在看!