自学Python第十五天-爬虫解析工具 RE 、BS4 、 xpath 和 jsonpath


使用 python 编写爬虫一个重要的步骤就是在爬到的网页内容中,抓取所需要的数据。这里就需要使用各种解析工具了。

解析工具

解析工具用来分析爬取到的内容,常用的工具有:

  • 正则表达式
    其实对于解析工具多数都会首推正则表达式,解析速度最快,因为它最好用,但是也最难用。由于正则表达式的使用范围广,用法复杂,所以单独进行研究。这里只研究 python 中如何使用正则表达式。
  • BeautifulSoup
    BeautifulSoup 是个第三方库,它用起来最简单,但是解析速度是最慢的,这里使用版本4。
  • lxml(xpath)
    lxml 也是个第三方库,解析速度较快,使用难度还算简单。如果想使用的好,还需要 xpath 的相关知识。

re 库

re 库是 python 用来操作正则表达式的一个内置模块,直接引用就可以使用了。

注,很多时候返回的是一个 Match 对象,可以使用 group 方法获取对象中的匹配结果

常用的方法

re 模块常用的方法有:

  • findall :查找所有,返回字符串列表,如进行了分组则返回元组列表,re.findall(pattern, string[,flags])->tuple
lst = re.findall('m', 'mai le fo len, mai ni mei!')
print(lst)		# ['m', 'm', 'm']
lst = re.findall(r"\d+", '5点之前,你要给我5000万')
print(lst)		# ['5', '5000']
  • search :查找匹配,返回第一个查找结果的Match对象,如果没有匹配则返回 None,re.search(pattern,string[,flags])->Match
ret = re.search(r"\d", '5点之前,你要给我5000万').group()
print(ret)		# 5
  • match :从字符串的开始进行匹配,返回Match对象
  • finditer :类似findall,但是返回是Match对象的迭代器对象。
  • compile :预加载正则表达式,返回正则对象 re.compile(string[,flag])->Re
obj = re.complie(r'\d')
ret = obj.search('5点之前,你要给我5000万')
  • sub :字符替换,re.sub(pattern, repl, string, count=0, flags=0),repl 是替换的字符串(也可以是一个函数),count 是替换的最大次数(0表示不限次)
import re

phone = '2004-959-559 # 这是一个电话号码'
# 删除注释
num = re.sub('#.*$','',phone)
print(num)				# 2004-959-559
# 移除非数字的内容
num = re.sub(r'\D','',phone)
print(num)				# 2004959559
  • split :字符串分割,re.split(pattern, string, maxsplit=0, flags=0)
import re

names = '关羽; 张飞, 赵云,   马超, 黄忠  李逵'

name_list = re.split(r'[;,\s]\s*', names)
print(name_list)

flag 参数

可以发现,很多查找方法都有个 flag 参数,它表示了匹配模式,取值可以使用 “|” 表示同时生效,例如 re.I | re.M 。可选值有:

可选参数全拼说明
re.IIGNORECASE忽略大小写
re.MMULTILINE多行模式,改变 ‘^’ 和 ‘$’ 的行为
re.SDOTALL点的任意匹配模式,改变 ‘.’ 的行为(例如能够匹配换行等)
re.LLOCALE使预定字符类 \w \W \b \B \s \S 取决于当前区域设定
re.UUNICODE使预定字符类 \w \W \b \B \s \S \d \D 取决于 unicode 定义的字符属性
re.XVERBOSE详细模式。这个模式下正则表达式可以是多行、忽略空白字符,并可以加入注释

分组

当只需要匹配结果中的一部分时,可以将此部分用括号 () 括起来,进行分组,RE模块会只返回分组内的内容

import re

s = """
苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的
"""

for temp in re.findall(r'^(.*),', s, re.M):		# 不会返回匹配到的 , 字符
    print(temp)

需要匹配多个分组时,可以这样写:

import re

content = '''张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901'''


for temp in re.findall(r'^(.+),.+?(\d+)', content, re.M):
    print(temp)
"""
执行结果:
('张三', '15945678901')
('李四', '13945677701')
('王二', '13845666901')
"""

也可以对分组起别名,通过别名调用

import re

s = """
<div class='class1'><span id='1'>孙悟空</span></div>
<div class='class2'><span id='2'>猪八戒</span></div>
<div class='class3'><span id='3'>唐僧</span></div>
<div class='class4'><span id='4'>沙和尚</span></div>
<div class='class5'><span id='5'>白龙马</span></div>
"""

obj = re.compile(r"<div class='.*?'><span id='\d+'>(?P<gp1>.*?)</span></div>", re.S)

res = obj.finditer(s)		# 需要注意的是,不能使用 findall,因为 findall 返回的是元组。

for it in res:
    print(it.group('gp1'))

此例中,(?P ) 内为分组,分组名称为 <> 内的 gp1 ,分组内容就是括号中的 .*? 所匹配的内容。使用 group 方法调用分组信息,如果此方法没有参数则是返回全部匹配信息,有参数则返回参数指定的分组信息。

Match 类

re模块的查询返回的大都是 Match 类或 Match 列表,这个类记录了匹配的结果、匹配索引(位置)等信息。

获取匹配结果

默认获取到的匹配数据存在 Match 类的组里,可以使用 Match.group() 方法来获取匹配对象的组里的结果,也可以将组名称字符串作为参数传入,返回特定组的结果。或使用 Match.groups() 方法获取所有组的结果。

import re

pattern = re.compile('abc')
result = re.match(pattern, 'abcdefg')
print(result.group())

需要注意的是,如果正则表达式中使用了多个分组,则第一个分组(索引号为0)表示表达式本身匹配的结果,而不是分组结果。而使用 groups() 方法的返回值则剔除了 0 号组。

import re

pattern = re.compile('(cde).*g(.*)l')
result = re.search(pattern, 'abcdefghijklmn')
print(result.group(), result.group(1), result.group(2)) # Match.group() 相当于 Match.group(0)
# 运行结果
# cdefghijkl cde hijk

获取匹配索引

可以通过 Match.span() 方法获取匹配结果的索引,方法使用及参数同 group() 方法,例如:

import re

pattern = re.compile('abc')
result = re.match(pattern, 'abcdefg')
print(result.span())	# (0, 3)

返回值是起始字符在字符串中的索引号(第一个匹配的字符索引号)和结束字符在字符串中的索引 -1(其实返回的是第一个不匹配的字符的索引,所以最后一个匹配字符索引就是返回值-1)

BS4 解析

BS4 是一个第三方库,全称 beautiful soup 版本4。它可以根据 HTML 的标签来进行分析并获取需要的数据

BS4文档
BS4文档

BS4 的安装和引用

使用 pip install bs4 进行安装,使用 from bs4 import BeautifulSoup 引用 BS4

使用BS4需要注意的是必须安装解析器,BS4其实是方便调用解析器的一个工具。推荐使用的解析器如下表(注:markup为需要解析的文本):

解析器使用方法优势劣势
Python标准库BeautifulSoup(markup, "html.parser")1.Python的内置标准库 2.执行速度适中 3.文档容错能力强Python 2.7.3 或 3.2.2 前的版本中文档容错能力差
lxml HTML解析器BeautifulSoup(markup, "lxml")1.速度快 2.文档容错能力强需要安装C语言库
lxml XML解析器BeautifulSoup(markup, "xml")1.速度快 2.唯一一个支持XML的解析器需要安装C语言库
html5libBeautifulSoup(markup, "html5lib")1.最好的容错性 2.以浏览器的方式解析文档 3.生成HTML5格式的文档1速度慢 2.不依赖外部扩展

推荐使用 lxml作为解析器,因为效率更高。其次推荐使用标准库,因为不用另安装第三方库和C语言库。

另:如果一段HTML文档格式不正确的化,在不同解析器中返回的结果可能是不一样的。

使用bs4进行解析的步骤

使用 bs4 解析的步骤如下:

  1. 获取需要解析的内容
  2. 将解析内容交给 bs 处理,生成 bs 对象
  3. 从 bs 对象查找需要的数据

获取解析内容就是正常的获取页面源代码

import requests

url ='https://movie.douban.com/top250'
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}
resp = requests.get(url, headers=headers)

生成 bs 对象

使用 BeautifulSoup(markup, parser)->BeautifulSoup 来生成 bs 对象

from bs4 import BeautifulSoup
page = BeautifulSoup(resp.text, "html.parser")

参数 markup 是输入的文档,参数 parser 为使用的解析器

bs 对象的常用方法

按标签查找

使用 find(name=None, attrs={}, recursive=True, string=None, **kwargs)find_all(name=None, attrs={}, recursive=True, string=None, limit=None, **kwargs) 这两个方法来获取查找的数据对象。name 是标签名称,attrs 是标签属性,recursive 是否遍历符合条件的所有子节点,string 搜索文本中的字符串内容,limit 是最多查找结果的数量。

需注意的是,string 内容是完全匹配字符串,如果是不完全匹配,例如包含特定字符串,需要使用正则表达式 string=re.compile("string") 。name 参数也可以使用正则表达式进行匹配。

find 方法返回的是一个标签结果,类型是 bs4.element.Tag;find_all 方法返回的是一个结果集,类型是 bs4.element.ResultSet 。但是在使用中可以当成 BeautifulSoup 对象使用。另外 find 只查找符合条件的第一个标签。

# 查找并获取第一个 ol 标签
ol = page.find("ol", class_='grid_view')		# class 是关键字,所以使用 class_

如果要避免使用关键字或常用字产生冲突,可以这样使用:

ol = page.find("ol", attrs={"class": "grid_view"})
# 参数中也可以不传入标签名,只按照属性筛选
ol = page.find(attrs={"class": "grid_view"})

这两者是等同的。

获取所有数据:

lis = ol.find_all("li")		# 获取所有 li 标签,生成一个列表

根据获取的标签,取得标签的文本内容:

for li in lis:
    title = li.find("span", attrs={"class": "title"})
    print(title.text)       # .text 为被标签的文本内容

find_all() 方法支持传入一个列表,匹配列表中任意元素的标签均在返回值列表中,即取或。如果提取标签不存在,则返回空列表。

CSS 选择器

BS支持大部分的CSS选择器,在 TagBeautifulSoup 对象的 .select() 方法中传入字符串参数,即可使用CSS选择器的语法找到标签 tag。

选择器名称用法
标签选择器.select("title")
层级选择器.select("body a")
父子选择器.select("head > title")
类选择器.select(".sister")
id选择器.select("#link1")
属性选择器(含有特定属性的标签).select("[href='...']")

需注意的是, .select() 方法返回值是一个 Tag 对象的列表

获取标签属性字典

可以使用 Tag.attrs 返回标签的属性字典。

格式化输出

对于一个 BeautifulSoup 对象,可以直接使用 print() 输出查看,但是如果格式不标准,例如是单行文本形式的,查看起来非常麻烦。这时候可以使用方法 BeautifulSoup.prettify() 进行格式化

import requests
from bs4 import BeautifulSoup

url = 'https://movie.douban.com/top250'
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}
resp = requests.get(url, headers=headers)
page = BeautifulSoup(resp.text, "html.parser")

print(page.prettify())

此时输出结果就是带换行和缩进的HTML文档了,方便阅读。

获取标签的属性

使用另一个实例演示如何获取标签属性

import requests, time
from bs4 import BeautifulSoup

url = 'https://www.umei.cc/bizhitupian/weimeibizhi/'
resp = requests.get(url)
resp.encoding = 'utf-8'
# 获取主页面代码
main_page = BeautifulSoup(resp.text, "html.parser")
# 查找子页面超链接
a_list = main_page.find("div", attrs={"class": "pic-box"}).find("ul", attrs={"class", "pic-list after"}).find_all("a")
for a in a_list:
    herf = 'https://www.umei.cc/' + a.get('href').strip('/')  # 通过 get 方法获取标签属性的值
    # 获取子页面源代码
    resp_child = requests.get(herf)
    resp_child.encoding = 'utf-8'
    # 获取子页面需要的数据
    child_page = BeautifulSoup(resp_child.text, 'html.parser')
    img = child_page.find("section", attrs={"class": "img-content"}).find('img')	# 获取 img 标签
    src = img.get('src')	# 获取 img 标签的 src 属性值
    img_name = src.split('/')[-1]  # 以 / 进行切割,取最后一部分,作为图片名称
    img_resp = requests.get(src)  # 请求图片
    with open("./download/" + img_name, mode='wb') as f:
        f.write(img_resp.content)  # 图片内容写入文件
    print('Over!!', img_name)
    time.sleep(1)		# 设置间隔时间,防止请求速度太快

即,使用 Tag.get(attrs_name)->String 来获取标签内的属性值

获取标签文本

可以使用标签对象的 get_text() 方法或 text 属性来获取标签文本

title = soup.select('title')
print(title.text)	# 或 title.get_text()

xpath 解析

xpath 解析是一种新的解析方式,比 re 简单,比 bs 高效,所以会经常用到。xpath 是在 xml 文档中搜索内容的一种语言,类似正则表达式。

语法规则

xpath 使用路径表达式来选取文档中的节点或者节点集,遵循一定的规则:

表达式描述示例结果备注
nodename选中该元素bookstore选择 bookstroe 标签元素如不确定元素标签名称,可以使用通配符*
/从根节点选取/bookstore选择根元素 bookstore如果路径起始于/,则此路径代表到某元素的绝对路径
/上下级元素过渡bookstore/book在 bookstore 元素中选择所有 book 子元素
//从匹配选择的节点选择文档中的节点,而不考虑其位置//book选择所有的 book 元素,而不考虑其位置
.选取当前节点.//title选择当前节点下,所有title节点常用在跨节点继续查询中
..选取当前节点的父节点
@选择节点属性//book/title/@lang选择所有book下的title节点中的lang属性
@查询属性为特定值的节点//title[@lang='eng']查询所有属性lang值为eng的title元素节点
text()选择节点的文本内容//book/title/text()选择所有book节点下的title节点的文本
text()根据文本内容查询节点//title[text()='数学']查询所有文本为“数学”的title节点
contains()选择包含特定文本的节点//title[contains(text(),'数学')]获取文本中包含“数学”的节点
contains()选择属性包含特定字符的节点//a[contains(@href,'baidu')]选择href属性包含’baidu’字符的a节点
[n]选择符合条件的第n个元素//bookstore/book[2]选择bookstore节点下的第2个book节点
[last()]选择符合条件的最后元素/bookstore/book[last()]选择bookstore节点下的最后一个book节点可以进行运算,例如倒数第二个节点为:/bookstore/book[last()-1]
[position()]按照定位条件选择节点/bookstore/book[position()>1]选择bookstore下的元素,从第2个开始选择如果从第2选到第4个则为:/bookstore/book[position()>1 and position()<5]
[][]多条件查询//title[@lange=eng'][text()='数学']查找满足多个条件的title节点

注意,xpath 中,第一个元素位置是1,最后一个是 last(),倒数第2个是 last()-1

lxml 的安装和引用

python 中,xpath 解析使用第三方库 lxml,需要注意的是 xpath 检索出来的数据都是列表

使用 pip install lxml 安装 lxml库,使用 from lxml import etree 来引用

生成 xpath 对象

xpath 可以根据输入的数据类型不同指定处理格式,例如:

from lxml import etree

xml = """..."""	# 定义一段 xml 文本
tree_xml = etree.XML(xml)           # 生成基于 xml 的对象,可以使用 html 方法来解析 html 文本生成对象
tree_file = etree.parse('b.html')	# 基于文件生成对象

查找数据

这里先定义一段 xml 文本

xml = """
<book>
    <id>1</id>
    <name>野花遍地香</name>
    <price>1.23</price>
    <nick>臭豆腐</nick>
    <author>
        <nick id="10086">周大强</nick>
        <nick id="10010">周芷若</nick>
        <nick class="joy">周杰伦</nick>
        <nick class="jolin">蔡依林</nick>
        <div>
            <nick>热热热热</nick>
        </div>
        <div>
            <nick>特别热</nick>
            <div>
                <nick>好热好热</nick>
            </div>
        </div>
        <span>
            <nick>span的热</nick>
        </span>
    </author>
    
    <partner>
        <nick id="ppc">胖胖陈</nick>
        <nick id="ppbc">胖胖不陈</nick>
    </partner>
</book>
"""
tree = etree.XML(xml)           # 生成基于 xml 的对象
# result = tree.xpath('/book')    # / 表示层级关系,第一个 / 是根节点
# 获取 name 的文本内容,使用 text()
# result = tree.xpath('/book/name/text()')        # ['野花遍地香']
# 获取的节点不是唯一,输出的是同胞节点列表
# result = tree.xpath('/book/author/nick/text()')     # ['周大强', '周芷若', '周杰伦', '蔡依林']
# 跨层级查找同名节点,使用 // 即跨层级查找所有后代节点,查找节点 nick
# result = tree.xpath('/book/author//nick/text()')    # ['周大强', '周芷若', '周杰伦', '蔡依林', '热热热热', '特别热', '好热好热', 'span的热']
# 跨节点(任意节点)查找节点,使用通配符 *
result = tree.xpath('/book/author/*/nick/text()')       # ['热热热热', '特别热', 'span的热']

print(result)

xpath 的筛选定位

除了按照层级和节点来获取需要的数据,还可以根据情况,进行具体的筛选定位。假设筛选对象为 b.html 文件

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8"/>
        <title>Title</title>
    </head>
    <body>
        <ul>
            <li><a href="http://www.baidu.com">百度</a></li>
            <li><a href="http://www.google.com">谷歌</a></li>
            <li><a href="http://www.sogou.com">搜狗</a></li>
        </ul>
        <ol>
            <li><a href="feiji">飞机</a></li>
            <li><a href="dapao">大炮</a></li>
            <li><a href="huoche">火车</a></li>
        </ol>
        <div class="job">李嘉诚</div>
        <div class="common">胡辣汤</div>
    </body>
</html>
from lxml import etree

tree = etree.parse("b.html")    # 以文件创建 xpath 对象
# 获取节点文本列表
# result = tree.xpath('/html/body/ul/li/a/text()')    # ['百度', '谷歌', '搜狗']
# 获取第一个 li 节点里的 a 标签的文本内容,使用 [1] ,注意从1开始而不是0
# result = tree.xpath('/html/body/ul/li[1]/a/text()')     # ['百度']
# 使用属性筛选定位标签,使用 [@属性名=属性值]
# result = tree.xpath('/html/body/ol/li/a[@href="dapao"]/text()')     # ['大炮']

# print(result)

# 分次查找
ol_li_list = tree.xpath('/html/body/ol/li')
for li in ol_li_list:
    # 从每一个 li 中提取文本信息
    result = li.xpath('./a/text()')     # 在li中继续寻找, . 表示当前节点 (相对路径)
    result2 = li.xpath('./a/@href')     # 获取 a 标签的 href 属性的值
    print(result)
    print(result2)

# 快速获取属性值列表
print(tree.xpath('/html/body/ul/li/a/@href'))

xpath 的一个应用实例

通常爬网页上的数据时,可以将相同类的数据做为一个块(div 的概念),然后使用循环获取每个块的需要的数据。

import requests
from lxml import etree

# 获取页面代码
url = 'https://beijing.zbj.com/search/f/?kw=saas&r=1'
resp = requests.get(url)
# 解析
html = etree.HTML(resp.text)
# 获取每一个块内容
divs = html.xpath('//div[@id="__nuxt"]//div[@class="search-content"]//div[@class="search-result-list-service"]/div')
# 从块内中获取需要的数据
for div in divs:
    com_name = div.xpath('./a[@class="name-address"]//div[@class="shop-detail"]/div/text()')[0]
    price = div.xpath('.//div[@class="price"]/span/text()')[0].strip('¥')
    title = div.xpath('.//div[@class="bot-content"]/a/text()')[0]
    location = div.xpath('.//div[@class="price"]/div/text()')[0].strip()
    print(com_name, price, title, location)

xpath 节点转字符串

可以使用 etree.tostring() 方法将节点对象转为字符字节

html_str = etree.tostring(etree.HTML(resp.text))

使用注意

需要注意的是,xpath 获取的都是列表。

另可以使用 标签名称[contains(text(),"文本")] 这样来查找包含文本内容包含表达式中文本 的标签,也可以使用 标签名称[text()="文本"] 进行文本内容的绝对匹配查找标签。

jsonpath 模块

和 bs4、xpath不同,这两个工具常用来解析静态页面,jsonpath 通常用来解析动态获取的 json 数据。jsonpath 对于 json 来说,相当于 xpath 相当于 xml。

官网(英文)

python 使用 jsonpath ,需要安装模块

pip install jsonpath

jsonpath 语法

类似于 xpath,jsonpath 也有自己的语法

XPathJSONPath描述
/$根节点
.@当前节点
/. or []子节点(下级节点)
..父节点 jsonpath 不支持
//..不管位置,选择所有符合条件的节点
**所有节点
@属性,json是键值对递归结构,不需要属性访问
[][]迭代器标识(下标、索引、内容选值等)
|[,]支持迭代器中做多选
[]?()支持过滤操作,括号内部支持逻辑运算符 and、or、not、&&、||、!等
()支持表达式计算
()分组,json不支持

需要注意的是jsonpath下标从0开始,而xpath是从1开始。

xpath 和 jsonpath 语法使用示例

XPathJSONPath结果
/store/book/author$.store.book[*].authorstore下所有book下的author
//author$..author所有的author
/store/*$.store.*store下的所有节点
/store//price$.store..pricestore下所有的price
//book[3]$..book[2]所有book节点中的第3个
//book[last()]$..book[(@.length-1)]最后一个book(使用索引)
//book[last()]$..book[-1:]最后一个book(使用切片)
//book[@price]$..book[?(@.price)]所有包含有price的book节点
//book[position()<3]$..book[0,1]前两个book(使用索引)
//book[position()<3]$..book[:2]前两个book(使用切片)
//book[@price<10]$..book[?(@.price<10)]过滤book下的price小于10的book(xpath是book的属性,jsonpath是下级节点)
//*$..*所有元素和节点

jsonpath 的使用

使用 jsonpath 解析 json 数据,使用的是 jsonpath.jsonpath(obj,expr) 方法。此方法接受两个参数,obj 是字典,即需要解析的 json 数据;expr 是使用 jsonpath 语法的解析表达式。需要注意的是,此方法返回值是value的列表,如果没有匹配值则返回 False。另外,解析表达式中如果使用了字符串,必须使用单引号'括起来,整个解析表达式使用双引号"括起来,否则无法匹配。

import jsonpath

info = {
    "error_code": 0,
    "stu_info": [
        {
            "id": 2059,
            "name": "小白",
            "sex": "男",
            "age": 28,
            "addr": "河南省济源市北海大道xx号",
            "grade": "天蝎座",
            "phone": "1837830xxxx",
            "gold": 10896,
            "info": {
                "card": 12345678,
                "bank_name": '中国银行'
            }
        },
        {
            "id": 2067,
            "name": "小黑",
            "sex": "男",
            "age": 28,
            'addr': '河南省济源市北海大道XX号',
            'grade': '天蝎座',
            'phone': '87654321',
            'gold': 100,
            'info': {
                "card": 9876543,
                "bank_name": '农业银行'
            }
        }
    ]
}

ret_1 = jsonpath.jsonpath(info, '$.stu_info[0].name')
print(ret_1)
ret_2 = jsonpath.jsonpath(info, '$..name')
print(ret_2)
# 对于值为列表的节点必须确定索引或切片,也可以使用 * 通配符或 [::] 全部切片
ret_3 = jsonpath.jsonpath(info, "$['stu_info'][*]['info']['bank_name']")		# 解析表达式中外层必须双引号,内层必须单引号
print(ret_3)
"""
执行结果:
['小白']
['小白', '小黑']
['中国银行', '农业银行']
"""
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值