数据解析之BS4

1. bs4基本使用

1.1. 简介

bs4的全称为 BeautifulSoup。和lxml一样,是一个html的解析器,主要功能也是解析数据和提取数据 。

本模块作为了解模块,实际开发中很少用这个模块去解析数据,大家可能会想为什么这个模块会逐渐被淘汰,它就真的一点优点都没有吗?优点吧其实也有,比如设计的接口比较人性化,使用起来比较方便,支持css选择器等等,但是缺点也是非常致命的,就是它的效率是没有 xpath 高,程序界都是很讲效率的,那既然 xpath 也很好用,效率也高,自然而然就没有bs4的市场了,但是大家也要了解该模块,可以不用,但是不能不知道。

1.2. 安装

pip install bs4

这里需要注意的一点是,如果你的项目是一个全新的项目文件,你只安装上bs4也是用不了该模块的解析方法的,还需要下载安装一个lxml库,因为bs4是依赖lxml的,不然是会报错的。都有lxml库了,正常都使用xpath了对吧,谁还去特意再安装bs4,这也是bs4被淘汰的一点因素。

安装成功之后想要在python程序当中使用的话就直接导入该模块即可。

from bs4 import BeautifulSoup

1.3. 基础语法

<!DOCTYPE html>
<html lang="en" xmlns="">
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
  </head>
  <body>
    <div>
      <ul>
        <li id="l1" class="a1">张三</li>
        <li id="l2">李四</li>
        <li>王五</li>
        <a id="Hello" href="" class="a1">Hello World</a>
        <a id="Hello1" href="" class="a1">Hello 美女</a>
        <span>嘿嘿嘿</span>
      </ul>
    </div>
    <a href="https://www.baidu.com" title="a2">百度</a>
    <a href="https://www.xiaomi.com" title="a3">小米</a>
    <div id="d1">
      <span>
        <h1>哈哈哈</h1>
      </span>
    </div>
    <p id="p1" class="p1">呵呵呵</p>
    <a href="">京东</a>
  </body>
</html>
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html', encoding='utf8'), 'lxml')

当操作的对象是文件时,就可以使用open('文件路径', encoding='utf8')以指定编码打开文件进行操作,当操作对象就是我们获取的响应时,就可以更改为BeautifulSoup(response, 'lxml')lxml是操作内核,也有其他的内核,但是lxml内核效率更高。

1.3.1.节点定位
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html', encoding='utf8'), 'lxml')

# 1.根据标签名进行查找
print(soup.a)  # 获取到第一个a标签   类型为 <class 'bs4.element.Tag'>
# 输出结果为:<a class="a1" href="" id="Hello">Hello World</a>

print(soup.a.attrs) # 获取到第一个a标签的属性值  字典类型
# 输出结果为:{'id': 'Hello', 'href': '', 'class': ['a1']}



# 2.函数方法  find() 和 find_all()
print(soup.find('a'))   # 等同于soup.a
#输出结果为: <a class="a1" href="" id="Hello">Hello World</a>
print(type(soup.find('a')))  # <class 'bs4.element.Tag'>

print(soup.find_all('a'))  # 获取所有的a标签,返回的是一个列表
# 输出结果为:[<a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>, <a href="https://www.baidu.com" title="a2">百度</a>, <a href="https://www.xiaomi.com" title="a3">小米</a>, <a href="">京东</a>]

print(soup.find('a', title="a2"))  # 获取title属性值为a2的a标签
# 输出结果:<a href="https://www.baidu.com" title="a2">百度</a>

print(soup.find('a', class_="a1"))  # 获取class值为a1的a标签,class属性需要在最后面加上_
# 输出结果为:<a class="a1" href="" id="Hello">Hello World</a>

print(soup.find_all('a', class_="a1"))  # 获取到所有class值为a1的a标签
# 输出结果为:[<a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>]

print(soup.find_all(['a', 'p']))  # 同时获取多个标签,这里是同时获取所有a标签和p标签
# 输出结果为:[<a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>, <a href="https://www.baidu.com" title="a2">百度</a>, <a href="https://www.xiaomi.com" title="a3">小米</a>, <p class="p1" id="p1">呵呵呵</p>, <a href="">京东</a>]



# 3.select() css选择器
print(soup.select('a'))  # 获取所有a标签 输出结果与.find_all('a')相同

print(soup.select('#l2'))  # 获取id属性值为l2的标签,返回值为列表,哪怕结果只有一个
# 输出结果:[<li id="l2">李四</li>]

print(soup.select('.a1'))  # 获取所有class属性值为a1的标签
# 输出结果:[<li class="a1" id="l1">张三</li>, <a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>]


# 属性选择器
print(soup.select('a[class="a1"]'))  # 获取class属性值为a1的a标签,等同于.find_all('a', class_="a1"))
# 输出结果:[<a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>]

print(soup.select('a[class]'))  # 获取有class属性的a标签
# 输出结果:[<a class="a1" href="" id="Hello">Hello World</a>, <a class="a1" href="" id="Hello1">Hello 美女</a>]


# 层级选择器
print(soup.select('div li'))  # 获取div标签下面的所有后代li标签 后代选择器 可跨多个节点
# 输出结果为:[<li class="a1" id="l1">张三</li>, <li id="l2">李四</li>, <li>王五</li>]

print(soup.select('div > ul > li'))  # 获取div标签下的子代ul标签下的子代li节点 子代选择器 只能一级一级向下递
# 输出结果为:[<li class="a1" id="l1">张三</li>, <li id="l2">李四</li>, <li>王五</li>]

print(soup.select('span, p'))  # 获取所有的span标签和p标签
# 输出结果为:[<span>嘿嘿嘿</span>, <span>
# <h1>哈哈哈</h1>
# </span>, <p class="p1" id="p1">呵呵呵</p>]
1.3.2. 节点信息
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html', encoding='utf8'), 'lxml')

obj = soup.select('li[id="l1"]')[0]  # 获取di属性值为l1的li节点
print(obj)  # <li class="a1" id="l1">张三</li>
print(type(obj))  # <class 'bs4.element.Tag'>

获取文本

现在节点是拿到了,那如果想要获取文本信息或者属性值又该如何获取呢?获取文本信息有两种方法。obj.stringobj.get_text()

看起来是一样的功能一样的效果,但其实也是有区别的。

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html', encoding='utf8'), 'lxml')

obj = soup.select('div[id="d1"]>span')[0]  # 获取id值为d1的div下面的子代span标签
print(obj)

这个span标签内部包裹了h1标签,文本值是在h1标签里面包裹的,这个时候再使用上述两个方法获取文本值。

是不是发现不同的地方了,也就是说,当文本值不是当前标签直接包含的时候,.string方法获取不到值,就会返回None,而.get_text()则是不管包裹到当前标签的哪一个后代标签里面,都会把文本值拿到。

获取属性值

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html', encoding='utf8'), 'lxml')

obj = soup.select('a[title="a2"]')[0]  # 获取title值为a2的a标签
print(obj)
# 输出结果为:<a href="https://www.baidu.com" title="a2">百度</a>

print(obj.attrs)
# 输出结果为:{'href': 'https://www.baidu.com', 'title': 'a2'}  字典

可以看到,.attrs属性值返回的是标签所有的属性值字典,需要取哪个具体值就直接使用字典取值方式取值即可,也就是obj.attrs.get("href"),也有其他的快捷取值方法。

...
print(obj.attrs.get("href"))
print(obj.get("href"))
print(obj["href"])

三种方式都能获取到,喜欢用哪种就用哪种。

2. 案例实战

本次案例为获取星巴克产品数据(该网站已经更新)

抓包的过程不再叙述,先看一些这些数据是否在页面源代码里面。

数据都是在页面源代码中的,图片是在内敛属性当中的,所以只需要获取style属性值提取url即可。

import requests
from bs4 import BeautifulSoup

class MySpider(object):
    def __init__(self):
        self.url = "https://www.starbucks.com.cn/menu/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        soup = BeautifulSoup(response, 'lxml')
        

if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

通过观察标签特征,发现所有数据都是在ul标签下面的。

虽然标签ul一共有22个,但每一个都是不同的类型,饮品数据都是在里面的,所以课通过这个ul标签特征进行定位。详细数据都是在li标签下面的a标签下面的strong标签里面的,所以这里可以用子代选择或者后代选择。

# 子代选择  也就是不跨节点,一个层级一个层级的往下递
soup.select('ul[class="grid padded-3 product"] > li > a > strong')

# 后代选择  跨节点 类似于xpath的 // 或者jsonpath的 .. 
soup.select('ul[class="grid padded-3 product"] strong')

现在拿到了标签,还需要拿到文本值,就需要循环title列表从中取值。

这样就拿到了文本,接下来就需要拿到图片的url,图片的url在div标签style属性值里面,所以也需要定位该div标签。

soup.select('ul[class="grid padded-3 product"] > li > a > div')  # 子代选择

soup.select('ul[class="grid padded-3 product"] div')  # 后代选择

拿到标签获取标签属性值就可以拿到该属性,但是会发现url片段是被包裹的,这里可以用正则去提取url,提取到片段url后再将其补全。

这里没有显示图片的完整url,而是详情页面,那咱们就进入详情页之后找找有没有图片url。

进入了详情页后,就能够找到完整的图片url,其实就是需要补全前面的域名https://www.starbucks.com.cn即可,拼接上即可。完整代码如下:

import re

import requests
from bs4 import BeautifulSoup

class MySpider(object):
    def __init__(self):
        self.url = "https://www.starbucks.com.cn/menu/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        soup = BeautifulSoup(response, 'lxml')
        title = soup.select('ul[class="grid padded-3 product"] > li > a > strong')
        # print(title)
        # for i in title:
        #     obj = i.get_text()
        #     print(obj)
        # pic_url = soup.select('ul[class="grid padded-3 product"] > li > a > div')
        pic_url = soup.select('ul[class="grid padded-3 product"] div')
        for pic in pic_url:
            url = pic.get("style")
            img_url = 'https://www.starbucks.com.cn' + re.findall('background-image: url\("(.*?)"\)', url)[0]
            print(img_url)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

如此便拿到了完整的url,接下来就是保存到本地文件,由于title列表与pic_url列表是相互独立的,保存的时候就不太好一一对应,所以就可以使用zip()方法将其整合,再将其强转成列表。

import re

import requests
from bs4 import BeautifulSoup

class MySpider(object):
    def __init__(self):
        self.url = "https://www.starbucks.com.cn/menu/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }

    def Urequest(self):
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        soup = BeautifulSoup(response, 'lxml')
        title = soup.select('ul[class="grid padded-3 product"] > li > a > strong')
        pic_url = soup.select('ul[class="grid padded-3 product"] div')
        lis = list(zip(title, pic_url))
        for li in lis:
            print(li)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

现在就是一个一个的元祖,虽然是元祖,但是里面的元素都是标签对象,通过下标取值依旧是能够使用.get_text()和取属性值这些bs4对象的方法的。现在再把前面的取值操作放到循环内部,代码就变成了:

... # 省略
        for li in lis:
            titleName = li[0].get_text()
            url = li[1].get("style")
            imgUrl = 'https://www.starbucks.com.cn' + re.findall('background-image: url\("(.*?)"\)', url)[0]
            print(titleName, imgUrl)
... # 省略

成功获取数据,保存为json文件的就只需要将数据先构造成json格式(在初始化的时候先定义一个字典,再在循环中生成字典添加到字典里面的列表中),再保存到json文件中即可。

完整代码:

import requests, re, json
from bs4 import BeautifulSoup


class MySpider(object):
    def __init__(self):
        self.url = "https://www.starbucks.com.cn/menu/"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
        }
        self.dic = {
            "list": []
        }

    def Urequest(self):
        return requests.get(url=self.url, headers=self.headers)

    def getMainHtml(self):
        response = self.Urequest().content.decode()
        soup = BeautifulSoup(response, 'lxml')
        title = soup.select('ul[class="grid padded-3 product"] > li > a > strong')
        pic_url = soup.select('ul[class="grid padded-3 product"] div')
        lis = list(zip(title, pic_url))
        for li in lis:
            titleName = li[0].get_text()
            url = li[1].get("style")
            imgUrl = 'https://www.starbucks.com.cn' + re.findall('background-image: url\("(.*?)"\)', url)[0]
            dic = {
                'titleName': titleName,
                'imgUrl': imgUrl
            }
            self.dic["list"].append(dic)
        with open("星巴克.json", 'w', encoding='utf8') as f:
            json.dump(self.dic, f, ensure_ascii=False, indent=4)


if __name__ == '__main__':
    s = MySpider()
    s.getMainHtml()

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的小su

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值