Ch4 正则表达式

2018-10-17  本文已影响0人  OzanShareing

概要


正则表达式是一个特殊的符号系列,它能帮助开发人员检查一个字符串是否与某种模式匹配。而Python中的re模块拥有着全部的正则表达式功能,为网络爬虫提供了可能。将讲解正则表达式的常用符号及Python中re模块的使用方法,在不需要解析库的情况下完成一个简单的爬虫程序。

涉及的主要知识点如下:


1.正则表达式常用符号

1.1 一般字符

正则表达式的一般字符有3个,如下:

字符 含义
. 匹配任意单个字符(不包括换行符\n)
\ 转义字符(把所有特殊含义的字符转换成字面意思)
[...] 字符集。对应字符集中的任意字符

说明:

(1).字符为匹配任意单个字符。例如:a.b可以的匹配结果为abc、aic、a&c等,但不包括换行符。

(2)\字符为转义字符,可以把字符改变为原来的意思。听上去不是很好理解,例如.字符是匹配任意的单个字符,但有时不需要这个功能,只想让它代表一个点,这时就可以用\.,就能匹配为.了。

(3)[...]为字符集,相当于在中括号中任选一个。例如a[bcd],匹配的结果为ab、ac和ad。

1.2 预定义字符集

正则表达式预定义字符集有6个,如下表:

字符 含义
\d 匹配一个数字字符。等价于[0-9]
\D 匹配一个非数字字符。等价于[^0-9]
\s 匹配任何空白字符,包括空格、制表符、换页符等。等价于[\f\n\r\t\v]
\S 匹配任何非空白字符,等价于[^\f\n\r\t\v]
\w 匹配包括下划线的任何单词字符,等价于[A-Za-z0-9]
\W 匹配任何非单词字符,等价于[^A-Za-z0-9]

正则表达式的预定义字符集易于理解,在爬虫实战中,常常会匹配数字而过滤文字部分的信息。例如:“字数3450”,只需要数字信息,可以通过“\d+”来匹配数据,“+”为数量词没匹配前一个字符1或无限次,这样便可以匹配到所有的数字。

1.3 数量词

正则表达式中的数量词列表如下:

字符 含义
* 匹配前一个字符0或无限次
+ 匹配前一个字符1或无限次
? 匹配前一个字符0或1次
{m} 匹配前一个字符m次
{m,n} 匹配前一个字符m至n次

说明:

(1)*数量词匹配前一个字符0或无限次。例如: ab*c匹配ac、abc、abbc、abbbc等

(2)+*很类似,只是至少匹配前一个字符一次。例如:ab+c 匹配abc、abbc、abbbc等。

(3)?数量词匹配前一个字符0或1次。例如:ab?c 匹配ac、abc。

(4){m}数量词匹配前一个字符m次。例如:ab{3}c匹配abbbc。

(5){m,n}数量词匹配前一个字符m至n次。例如:ab{1,3}c匹配abc、abbc、abbbc。

1.4 边界匹配

边界匹配的关键符号如下表:

字符 含义
^ 匹配字符串开头
$ 匹配字符串结尾
\A 仅匹配字符串开头
\Z 仅匹配字符串结尾

说明:

(1)^匹配字符串的开头。例如: ^abc匹配abc开头的字符串

(2)``匹配字符串结尾。 例如:`abc`匹配abc结尾的字符串

(3)\A仅匹配字符串开头。 例如: \Aabc

(4)\Z仅匹配字符串结尾。例如:abc\Z

边界匹配在爬虫实战中的使用较少,因为爬虫爬取的数据大部分为标签中的数据,例如<span class="stats-vote"><i class="number">186</i>好笑</span>中提取数字信息,边界匹配在这里没有任何作用。

最后介绍爬虫实战中常用的(.*?)()表示括号的内容作为返回结果,.*?是非贪心算法,匹配任意的字符。例如,字符串xxIxxjshdxxlovexxsffaxxpythonxx,可以通过xx(.*?)xx匹配符合这种规则的字符串,代码如下:

import re

a = 'xxIxxjshdxxlovexxsffaxxpythonxx'

infos = re.findall('xx(.*?)xx', a)
print(infos)#findall方法返回的为列表结构

2. re模块及其方法

re模块使Python语言拥有全部的正则表达式功能,下面主要介绍Python中re模块常用的3中函数使用方法。

2.1 search()函数

re模块的search()函数匹配并提取第一个符合规律的内容,返回一个正则表达式对象。search()函数的语法如下:

re.match(pattern, string, flags=0)

其中:
(1)pattern为匹配的正则表达式。
(2)string为要匹配的字符串
(3)flags为标志位,用于控制正则表达式的匹配方式,如是否区分大小写,多行匹配等。

例如:

import re

a = 'one1two2three3'

infos = re.search('\d+', a)
print(infos)
# search方法返回的是正则表达式对象
返回的是正则表达式对象

可以看出,search()函数返回的是正则表达式对象,通过正则表达式匹配到了“1”这个字符串,可以通过下面的代码返回匹配到的字符串:

import re

a = 'one1two2three3'

infos = re.search('\d+', a)
print(infos.group())
# group方法返回获取信息

2.2 sub()函数

re模块提供了sub()函数用于替换字符串中的匹配项,sub()函数的语法如下:

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

其中:
(1)pattern为匹配的正则表达式
(2)repl为替换的字符串
(3)string为要被查找替换的原始字符串
(4)counts为模式匹配后替换的最大次数,默认0表示替换所有的匹配。
(5)flags为标识位,用于控制正则表达式的匹配方式,如是否区分大小写、多行匹配等

例如,一个电话号码123-4567-1234,通过sub()函数把中间的-去除掉,可以通过下面代码实现:

import re

phone = '123-4567-1234'
new_phone = re.sub('\D', '', phone)
print(new_phone)
# sub方法用于替换

程序运行结果如下:

sub()函数的用途类似于字符串中的replace()函数,但sub函数更加灵活,可以通过正则表达式来匹配需要替换的字符串,而replace是不能做到的。在爬虫实战中,sub()函数的使用也是极少的,因为爬虫所需的是爬取数据,而不是替换数据。

2.3 findall()函数

findall()函数匹配所有符合规律的内容,并以列表的形式返回结果。例如:前面的one1two2three3,通过search()函数只能匹配到第一个符合规律的结果,而通过findall()函数可以返回字符串所有的数字。

import re

a = 'one1two2three3'
infos = re.findall('\d', a)
print(infos)

运行结果如下:

在爬虫实战中,findall()的使用频率最多,下面以北京地区短租房的价格为例(http://bj.xiaozhu.com/),看一下通过正则表达式如何提取所需的信息没通过观察网页源代码可以看出,短租房的价格都是在<span class="result_price">&#165;<i>1580</i>起/晚</span>这个标签中。

这时就可以通过构建正则表达式和findall()函数来获取房租价格。

import re
import requests

res = requests.get('http://bj.xiaozhu.com/')

# print(res.text)
# <span class="result_price">&#165;<i>1580</i>起/晚</span>

prices = re.findall('&#165;<i>(.*?)</i>起/晚</span>', res.text)
print(prices)
返回结果

不难看出,通过正则表达式的方法爬取数据,比之前的方法代码更少也更简单,那是因为少了解析数据这一步,通过Requests库请求返回的HTML文件就是字符串的类型,代码可以直接通过正则表达式来提取数据。

2.4 re模块修饰符

re模块中包含一些可选标志修饰符来控制匹配的模式,如下表:

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

在爬虫中,re.S是最常用的修饰符,它能够换行匹配。举个例子,例如提取<div>指数</div>中的文字,可以通过以下代码实现:

import re

a = '<div>指数</div>'

word = re.findall('<div>(.*?)</div>', a)
print(word)
# result ['指数']

但如果字符串是下面这样的:

a = '''<div>指数' 
    </div>'''

通过上面的代码则匹配不到div标签中的文字信息。这是因为findall()函数是逐行匹配的,当第一行没有匹配到数据时,就会从第2行开始重新匹配,这样就没办法匹配到div标签中的文字信息,这时便可通过re.S来进行跨行匹配。

import re

a = '''<div>指数' 
    </div>'''

word = re.findall('<div>(.*?)</div>', a, re.S)
print(word)
# result ['指数']
跨行匹配

从结果中可以看出,跨行匹配的结果会有一个换行符,这种数据需要清洗才能存入数据库,可以通过strip()方法去除换行符。

import re

a = '''<div>指数
    </div>'''

word = re.findall('<div>(.*?)</div>', a, re.S)
print(word[0].strip())
# strip()方法去除换行符
# result 指数

3. 综合案例1----爬取《斗破苍穹》全文小说

利用Requests库和正则表达式方法,爬取斗破苍穹小说网(http://www.doupoxs.com/doupocangqiong/)中该小说的全文信息,并把爬取的数据存储到本地文件中。

3.1 爬虫思路分析

(1)需爬取的内容为斗破苍穹小说网的全文小说,如下图:

斗破苍穹小说网

(2)爬取所有章节的信息,通过手动浏览,以下为前5张的网址:

http://www.doupoxs.com/doupocangqiong/
http://www.doupoxs.com/doupocangqiong/2.html
http://www.doupoxs.com/doupocangqiong/
http://www.doupoxs.com/doupocangqiong/5.html
http://www.doupoxs.com/doupocangqiong/
http://www.doupoxs.com/doupocangqiong/6.html
http://www.doupoxs.com/doupocangqiong/
http://www.doupoxs.com/doupocangqiong/7.html
http://www.doupoxs.com/doupocangqiong/
http://www.doupoxs.com/doupocangqiong/8.html

第1章与第2章没有明显规律,但第2章后的URL规律很明显,通过数字递增来分页。手动输入http://www.doupoxs.com/doupocangqiong/3.html,会发现是404页面。

所以具体的思路为:从第一章开始构造URL,中间有404错误就跳过不爬取。

(3)需要爬取的信息为全文的文字信息,如下图:

需爬取的内容

(4)运用Python对文件的操作,把爬取的信息存储在本地TXT文本中。

3.2 爬虫代码及分析

爬虫代码如下:

import requests
import re
import time
import html

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/70.0.3538.67 Safari/537.36"
}

# 新建TXT文档,追加的方式
f = open("C:/Users/Think/Desktop/doupo.txt", 'a+', encoding='utf-8')


def get_info(url):
    # 定义获取信息的函数
    res = requests.get(url, headers=headers)
    if res.status_code == 200:
        # print(res.content.decode("gbk"))
        contents = re.findall("<p>(.*?)</p>", res.content.decode("utf-8"), re.S)
        # print(contents)
        for content in contents:
            # f.write(content + '\n')
            # print(content)
            # print(html.unescape(content))
            f.write(html.unescape(content) + '\n')
        print(url)


# get_info('http://www.doupoxs.com/doupocangqiong/2.html')

if __name__ == '__main__':
    # 程序主入口
    urls = ['http://www.doupoxs.com/doupocangqiong/{}.html'.format(str(i)) for i in range(2, 1665)]
    # print(urls)
    for url in urls:
        get_info(url)
        time.sleep(1)
    f.close()
    # 关闭TXT文档

程序运行的结果保存在计算机,文件名为doupo的文导航中。

代码分析:

(1)第1~3行导入程序需要的库,Requests库用于请求网页获取网页数据。运用正则表达式不需要用BeautifulSoup解析网页数据,而是使用Python中的re模块匹配正则表达式。time库的sleep()方法可以让程序暂停。

(2)第5~8行通过Chrome浏览器的开发者工具,复制User-Agent,用于伪装为浏览器,便于爬虫的稳定性。

(3)第10行新建TXT文档,用于存储小说的全文信息。
(4)第12~19行定义get_info()函数用于获取网页信息并存储信息。传入URL后,进行请求。通过正则表达式定位到小说的文本内容,并写入TXT文档中。
(5)第21~26行为程序的主入口。通过对网页URL的观察,使用列表推导式构造所有小说URL并依次调用get_info()hanshu ,time.sleep(1)的意思是每循环一次,让程序暂停1秒,防止请求网页频率过快而导致爬虫失败。

注意:HTML 经常会混杂有转移字符,这些字符我们需要把它转义成真正的字符。代码中使用html.unescape(content)将html的转义字符转义成真正的字符,python3.4 之后的版本,在html模块新增了 unescape 方法。

3. 综合案例2----爬取糗事百科网的段子信息

将利用Requests和正则表达式方法,爬取糗事百科网中“文字”专题的段子信息,并把爬取的数据存储在本地文件中。

4.1 爬虫思路分析

(1)爬取的内容为糗事百科网“文字”专题的段子信息,如下图:


(2)爬取糗事百科文字35页的信息,通过手动浏览,以下为前4页的地址:

http://www.qiushibaike.com/text/
http://www.qiushibaike.com/text/page/2/
http://www.qiushibaike.com/text/page/3/
http://www.qiushibaike.com/text/page/4/

依次来构造出13页的网址。
(3)需要爬取的信息有:用户ID、用户等级、用户性别、好笑数量和评论数量,如下图:


需获取的网页信息

(4)运用Python对文件的操作,把爬取的信息存储在本地的TXT文本中。

爬虫代码及分析

爬虫代码如下:

import requests
import re
import time

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) '
                         'AppleWebKit/537.36 (KHTML, like Gecko) '
                         'Chrome/70.0.3538.67 Safari/537.36'}

# 初始化列表,用于装入爬虫信息
info_lists = []


def judgment_sex(class_name):
    # 定义获取用户性别的函数
    if class_name == 'man':
        return "男"
    else:
        return "女"


def get_info(url):
    # 解决
    res = requests.get(url, headers=headers)
    # print(res.text)
    ids = re.findall("<h2>(.*?)</h2>", res.text, re.S)
    # print(ids)
    # ['\n小鸟任天驰\n', '\n没有昵称行天下\n'...]
    # print(len(ids))

    # <div class="articleGender manIcon">28</div>
    ages = re.findall('<div class="articleGender \D+Icon">(.*?)</div>', res.text, re.S)
    # print(ages)
    # ['28', '30'...]
    # print(len(ages))

    sexs = re.findall('<div class="articleGender (.*?)Icon">', res.text, re.S)
    # print(sexs)
    # ['man', 'women'...]
    # print(len(sexs))

    # <span class="stats-vote"><i class="number">679</i> 好笑</span>
    laughs = re.findall('<span class="stats-vote"><i class="number">(\d+)</i> 好笑</span>', res.text, re.S)
    # print(laughs)
    # print(len(laughs))

    # <i class="number">6</i> 评论
    comments = re.findall('<i class="number">(\d+)</i> 评论', res.text, re.S)
    # print(comments)
    # print(len(comments))

    # <div class="content"><span>儿子礼拜里一次,那天晚上猪也从圈出来了,找猪的时候 </span></div>
    # contents = re.findall('<div class="content">\n<span>(.*?)</span>', res.text, re.S)
    contents = re.findall('<div class="content">.*?<span>(.*?)</span>', res.text, re.S)
    # print(contents)
    # print(len(contents))

    for id, age, sex, content, laugh, comment in zip(ids, ages, sexs, contents, laughs, comments):
        info = {
            'id': id.strip(),
            'age': age,
            'sex': judgment_sex(sex),
            'content': content.strip(),
            'laugh': laugh,
            'comment': comment
        }
        info_lists.append(info)
    # print(info_lists)


# get_info("http://www.qiushibaike.com/text/page/1/")

if __name__ == '__main__':
    urls = ['http://www.qiushibaike.com/text/page/{}/'.format(str(i)) for i in range(1, 14)]
    for url in urls:
        # print(url)
        get_info(url)

    f = open('C:/Users/Think/Desktop/qiushibaike.txt', 'a+')
    for info_list in info_lists:
        try:
            f.write(info_list['id'] + '\n')
            f.write(info_list['age'] + '\n')
            f.write(info_list['sex'] + '\n')
            f.write(info_list['content'] + '\n')
            f.write(info_list['laugh'] + '\n')
            f.write(info_list['comment'] + '\n')
        except UnicodeEncodeError:
            print("Unicode error")
    f.close()

程序运行结果如下:


上一篇下一篇

猜你喜欢

热点阅读