Python爬虫教程

第一章: 初学乍练-Python快速入门

第二章: 初窥门径-从全局把握网络爬虫

第三章: 爬虫数据-网页与JSON

第四章: 爬虫核心-HTTP协议

第五章: 手到擒来-数据的抓包

第六章: 利刃出鞘-HTTP请求库

第七章: 尘埃落定-数据的解析

第八章: 逆向初探-JS逆向

第九章: 爬虫进阶-Selenium, 中间人拦截

第十章:斗转星移-常用的反爬策略及应对方法

首页 > Python爬虫教程 > 第七章: 尘埃落定-数据的解析 > 7.1节:使用正则表达式解析网页

7.1节:使用正则表达式解析网页

薯条老师 2021-03-09 13:40:10 237664 0

编辑 收藏

广州番禺Python爬虫小班周末班培训

第四期线下Python爬虫小班周末班已经开课了,授课详情请点击:http://chipscoco.com/?id=232

7.1.1 理解正则表达式

正则表达式的英文为regular expression, regular为有规则,有规律的意思,regular expression可简单地理解为"有规则的表达式"。这里的规则是指通过预先定义好的特定字符(模式字符,限定字符,修饰字符)组成一个规则字符串。每一个规则字符串代表一种匹配规则,然后利用规则字符串对目标字符串进行匹配或过滤。

比如正则表达式中定义了模式字符\w,用来匹配字母,数字,下划线。定义了模式字符\s,用来匹配任意空白字符。同时定义了限定符,用来限定对目标字符的匹配次数,比如?字符用来限定匹配0次或1次,+字符用来限定字符串至少出现1次。

在使用正则表达式时,可以对这些模式字符和限定字符进行灵活组合,以组合成更复杂的匹配规则,比如\w+,表示目标字符串中至少有一个字母,数字,或下划线,否则匹配失败。

正则表达式也是一个字符串,在正则表达式中可以包含其它的普通字符。我们学习正则表达式,主要是学习正则表达式中的模式字符和限定符。然后根据正则表达式中的语法规则,将普通字符,模式字符,限定符组合成一个高效的正则表达式。

7.1.2 正则表达式中的模式字符

下表为正则表达式中常用的模式字符:

模式字符

描述

\w

匹配字母,数字,下划线

\W

匹配除\w以外的任意字符

\s

匹配空白字符。计算机中的空白字符:\t, \n, \r, \f

\S

匹配除\s以外的任意字符,即非空字符

\A

匹配字符串开头

\z

匹配字符串结尾,会匹配到行尾的换行符

\Z

匹配字符串结尾,不会匹配到行尾的换行符

\d

匹配任意数字字符:1-9

\D

匹配除\d以外的任意字符

^

匹配一行字符串的开头

$

匹配一行字符串的结尾

.

匹配除换行符以外的任意字符

[]

匹配[]内的任意一个字符,表示某一个范围可使用-进行连接,比如[a-z],表示匹配a到z之间的任意字符

[^]

匹配除[]以外的任意字符

|

左右两边表达式是"或"关系,表示匹配左边或者右边

()

(1) 表示将括号内的字符作为一个整体进行匹配

(2) 取匹配结果的时候,括号中的表达式作为一个整体被获取

7.1.3 正则表达式中的限定符

限定字符用来限定匹配的次数,下表为正则表达式中常用的限定符:

限定符

描述

匹配表达式0次或1次

+

匹配表达式1次以上

    *

匹配表达式0次或1次以上

{n}

重复匹配表达式n次

{m, n}

至少匹配m次,最多匹配n次

{m,}

至少匹配m次

7.1.4 贪婪匹配与非贪婪匹配

使用限定字符限定匹配次数时,在默认情况下会进行贪婪匹配。所谓贪婪匹配,是指尽可能进行多的匹配。假设目标字符串为"薯条老师的第三期Python线下就业培训班于2021年3月7号开学",正则表达式为.*(\d+),用来提取出字符串中的2021。

由于*符号使用了贪婪匹配,所以会匹配到目标字符串中的第一个字符到"2021"的最后一个数字字符1之间的所有字符,最后只留下数字字符1给\d模式进行匹配,最终匹配的结果为1,不符合预期。

为什么只留下数字字符1给\d模式进行匹配呢?因为模式符号.也可以匹配到数字字符,在贪婪模式下会尽可能多的匹配,所以最终\d模式只匹配到1。

peix.jpg 

在限定符后面加上一个?,可将贪婪匹配改为非贪婪匹配。在非贪婪匹配中,会尽可能少的匹配。那么,使用正则表达式.*?(\d+),就可以匹配出2021。

7.1.5 正则表达式中的零宽断言

在正则表达式的使用场景中,我们可能会有这样的需求:只做判断,不做捕获。例如"薯条老师","汉堡助教"为需要捕获的目标字符串,且它们前面的位置必须存在字符串"chipscoco",那么该怎么做呢?一些同学可能会这么写正则表达式:

pattern = 'chipscoco(薯条老师|汉堡助教)'

这样写就错了,因为这样的正则表达式会同时捕获到chipscoco。只判断目标字符串前后是否存在特定字符,而不保存匹配结果,可以使用正则表达式中的零宽断言。

下表为零宽断言的常用符号:

零宽断言

描述

?=exp

正预测先行断言,目标字符串后面必需匹配表达式exp

?!exp

负预测先行断言,!即非,表示目标字符串后面不能出现表达式exp

    ?<=exp

正回顾后发断言,目标字符串前面必需匹配表达式exp

?<!exp

负回顾后发断言,表示目标字符串前面不能出现表达式exp

所以,只捕获"薯条老师","汉堡助教",同时判断其前面的位置必须存在字符串"chipscoco",正则表达式应为:

pattern = '薯条老师|汉堡助教(?<=chipscoco)'

7.1.6 Python中的正则表达式模块

Python提供了内置模块re来处理正则表达式,下表为re模块中的常用方法:

常用方法

描述

match(pattern,string,flags=0)

从字符串起始位置开始匹配,通过返回值的group方法来获取匹配的结果。

search(pattern, string, flags=0)

扫描整个字符串,返回第一次匹配的结果

findall(pattern, string,flags=0)

扫描整个字符串,返回所有匹配的结果

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

将string中的repl子串使用pattern进行替换

compile(pattern, flags=0)

将模式字符串编译成可复用的正则表达式对象

以上方法中的flags参数用来对模式匹配进行修饰,flags参数的常用取值如下表所示:

flags参数

描述

re.I

I是英文单词ignore的首字母,表示在匹配过程中不区分大小写

re.M

M是英文单词multilines的首字母,表示多行匹配

re.S

对于模式字符.会匹配包含换行符的任意字符

re.U

U是unicode的首字母,表示根据unicode编码来解析字符

代码实例-解析所有URL中的域名:

import re
login_url = 'http://chipscoco.com/zb_users/plugin/YtUser/cmd.php?act=verify'
register_url = 'Http://chipscoco.cn:8090/register/'
page_url = 'https://chipscoco.cn/?id=9'
home_url = 'HTTP://www.chipscoco.com'
official_url = 'http://薯条橙子IT教育.com:6666/'

urls =  [login_url, register_url, page_url, home_url, official_url]
"""
(1) (?<!\-)系负回顾后发断言,表示目标字符串前面不能出现字符-
(2) 正则表达式([\w\.\u4e00-\u9fa5]+)用来捕获url中的域名,其中u4e00-\u9fa用来匹配中文字符
(3) :?用来匹配域名和端口号之间的分隔符:,(\d*)用来捕获url中的端口号
"""
pattern = "https?://(?<!\-)([\w\.\u4e00-\u9fa5]+[\w\.\u4e00-\u9fa5]*)" \ 
":?(\d*)[/\w\?\.=&]*$"
for url in urls:
    # re模块的match方法表示从头开始匹配,只匹配一次
    o =  re.match(pattern, url, re.I)
    print(o.group(1))

代码实例-解析日志文件中的客户端IP地址及请求时间:

import re
log = """
2020-08-31 16:39:58--INFO--{"remote_addr":192.168.2.3, "request_time": "0.618"}
2020-08-31 16:57:19--INFO--{"remote_addr":192.168.2.3, "request_time": "1.018"}
2020-08-31 16:39:58--INFO--{"remote_addr":192.168.1.7, "request_time": "0.638"}
2020-08-31 16:57:19--INFO--{'remote_addr':192.168.2.3, "request_time": "2.001"}
2020-08-31 16:39:58--INFO--{"remote_addr":192.168.1.7, "request_time": "0.708"}
2020-08-31 16:57:19--INFO--{"remote_addr":192.168.2.3, 'request_time': "0.816"}
2020-08-31 16:39:58--INFO--{"remote_addr":192.168.2.5, 'request_time': "0.616"}
2020-08-31 16:57:19--INFO--{'remote_addr':192.168.2.3, "request_time": "1.091"}
"""
 
"""
(1) 注意日志文件中的remote_addr,request_time,其引号为单引号或双引号
所以在正则表达式中用['"]进行了匹配。
(2) \s*表示匹配任意的空白字符,空白字符包括空格
(3) ([\d\.]*)用来捕获ip地址或请求时间
"""

pattern = """
{['"]remote_addr['"]:\s*([\d\.]*), \s*['"]request_time['"]:\s*['"]([\d\.]*)['"]}
"""

# re模块的findall方法会查找到所有匹配的目标字符串
o =  re.findall(pattern, log, re.I)
for _ in o:
    print(_)

7.1.7 简易的HTML解析器:HTMLParser

现在通过正则表达式来编写一个简单的HTML解析器,用来解析HTML中的常用标签。

以下为源码:

# __author__ = 薯条老师
import re
 
class HTMLParser:
    class Label:
        def __init__(self, label, text, **kwargs):
            self.__label = label
            self.__text = text
            self.__build_attrs(**kwargs)
 
        def __build_attrs(self, **kwargs):
            for name,value in kwargs.items():
                self.__dict__[name] = value
 
        def get_text(self):
            return re.sub("<.*?>|</.*?>", "", self.__text)
 
        def __getitem__(self, name):
            if name == "text" and name not in self.__dict__:
                self.__dict__[name] = self.get_text()
            return self.__dict__.get(name)
 
    def __init__(self, html):
        self.__html = html
 
    @property
    def html(self):
        return self.__html
 
    def __build_pattern(self, label, **kwargs):
        pattern = "<{}.*?".format(label)
        for attr, value in kwargs.items():
            if attr == "class_":
                attr = "class"
            pattern+='{}=["\']{}["\']'.format(attr, value)
        pattern += "\s*>(.*?)</{}>".format(label)
        return pattern
 
    def find(self, label, **kwargs):
        # 根据关键字参数来构建一个正则表达式
        pattern = self.__build_pattern(label, **kwargs)
        instances = []
        for _ in re.findall(pattern, self.__html):
            instances.append(HTMLParser.Label(label, _, **kwargs))
        return instances
 
if __name__ == "__main__":
    html =  "<html><body><p>在线Python教程 " \
            "<a href='www.chipscoco.com'>Python零基础入门指南</a></p>" \
            "<div><p><a href=\"www.chipscoco.com\">与薯条老师一起学Python</a></p>" \
            "</div></body></html>"
 
    html_parser = HTMLParser(html)
    # 查找HTML中的P标签
    labels = html_parser.find("p")
    for label in labels:
        # 调用标签对象的get_text方法获取标签文本
        print(label.get_text())
    
    """
    (1) 通过关键字参数class_获取class为article的标签。
    (2) class是Python中的关键字,故用class_作为关键字参数
    """
    
    labels = html_parser.find("p",class_="article")
    print(labels[0].get_text())
 
    labels = html_parser.find("a", href="www.chipscoco.com")
    for label in labels:
        # 读者可进行优化,比如可以输出该标签的其它标准属性
        print(label["href"], label["text"])

输出结果:

在线Python教程 Python零基础入门指南

与薯条老师一起学Python

在线Python教程 Python零基础入门指南

www.chipscoco.com Python零基础入门指南

www.chipscoco.com 与薯条老师一起学Python

7.1.8 知识要点

(1) 正则表达式也是一个字符串,在正则表达式中可以包含其它的普通字符。我们学习正则表达式,主要是学习正则表达式中的模式字符和限定符。

(2) 所谓贪婪匹配,是指尽可能进行多的匹配,而非贪婪匹配指尽可能少的匹配。

(3) 如需只做判断,不做捕获,可以使用正则表达式中的零宽断言。

(4) Python一个内置的re模块用来处理正则表达式

7.1.9 高薪就业班

(1) Python后端工程师高薪就业班,月薪10K-15K,免费领取课程大纲
(2) Python爬虫工程师高薪就业班,年薪十五万,免费领取课程大纲
(3) Java后端开发工程师高薪就业班,月薪10K-20K, 免费领取课程大纲
(4) Python大数据工程师就业班,月薪12K-25K,免费领取课程大纲

扫码免费领取学习资料:


欢迎 发表评论: