初始bs4

官网教程地址:https://beautifulsoup.readthedocs.io/zh-cn/v4.4.0/

lxml、pyquery、bs4、re执行效率对比执行速度对比:https://www.jianshu.com/p/d9812bbce6b6

安装

# 二选一即可
pip install bs4
pip install beautifulsoup4

# 建议安装lxml并设置为解析库,速度比html.parser快很多,容错能力也更强
pip install lxml

读取本地文件

from bs4 import BeautifulSoup

# 这里的第一个参数如果是本地文件,可以直接通过这种方法打开
# 这里的第二个参数就是解析库
soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')
print(type(soup)) <class 'bs4.BeautifulSoup'>

image-20240327090945130

通过 name 获取标签名

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

print(soup.a.name)  # a
print(soup.div.name) # div

通过 .标签名 获取数据

语法:bs4.BeautifulSoup.tagName

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

# 等价
print(soup.head.title)  # <title>xx读书 Top 250</title>
print(soup.find('head').find('title'))  # <title>xx读书 Top 250</title>

print(soup.img) # <img src="./xx读书 Top 250_files/s1070959.jpg" width="90"/>
print(soup.p) # <p class="appintro-title">xx</p>
print(soup.div)
print(soup.a) # <a class="nav-login" href="https://accounts.xx.com/passport/login?source=book" rel="nofollow">登录/注册</a>

获取属性

  1. 通过attrs
  2. 通过[属性名]
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

print(soup.a.attrs)  # {'href': 'https://accounts.xx.com/passport/login?source=book', 'class': ['nav-login'], 'rel': ['nofollow']}
print(soup.a['href'])  # https://accounts.xx.com/passport/login?source=book
print(soup.img.attrs)  # {'src': './xx读书 Top 250_files/s1070959.jpg', 'width': '90'}
print(soup.img['src'])  # ./xx读书 Top 250_files/s1070959.jpg

获取子节点 children 和子孙节点 descendants

children 方法只遍历当前节点的直接子节点,即只遍历当前节点的下一级子节点。

descendants 方法会递归遍历当前节点的所有子孙节点,包括子节点、孙子节点,以及更深层次的后代节点。

# 懒得敲 GPT写的 OVO
from bs4 import BeautifulSoup

# 示例 HTML 代码
html_doc = """
<html>
<head>
    <title>Sample Page</title>
</head>
<body>
    <div id="content">
        <p>Paragraph 1</p>
        <p>Paragraph 2</p>
        <div>
            <span>Span 1</span>
            <span>Span 2</span>
        </div>
    </div>
</body>
</html>
"""

# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_doc, 'html.parser')

# 使用 children 遍历当前节点的直接子节点
print("Using children:")
for child in soup.body.children:
    if child.name:
        print(child.name)

# 使用 descendants 遍历当前节点的所有子孙节点
print("\nUsing descendants:")
for descendant in soup.body.descendants:
    if descendant.name:
        print(descendant.name)

获取父节点 parent,所有父节点parents

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

# 打印示例节点
print(soup.select(".item td")[1].prettify())
"""
<td valign="top">
    这里省略了部分代码
 <p class="quote" style="margin: 10px 0; color: #666">
  <span class="inq">
   都云作者痴,谁解其中味?
  </span>
 </p>
</td>
"""

# 打印子节点
print(soup.find('span', 'inq')) # <span class="inq">都云作者痴,谁解其中味?</span>

# 打印父节点
print(soup.find('span', 'inq').parent)
"""
<p class="quote" style="margin: 10px 0; color: #666">
<span class="inq">都云作者痴,谁解其中味?</span>
</p>
"""

只拿1条数据 find

  1. 如果只是一个标签,默认拿到符合标签的第一个结果。soup.find(tagName)
  2. 也可以通过前面标签名,后面条件的方式更灵活的去筛选。 soup.find(tagName, 条件)
  3. 如果查找的条件是一个的属性是类class,不能直接写class,会报错,可以参考下面两种写法:
    1. soup.find('div', 'pl2')
    2. soup.find('div', class_='pl2')
  4. 我们前面学到了,attrs可以拿到属性,那么条件也可以通过指定attrs字典更灵活的去筛选
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

# 相当于 soup.p
print(soup.find('p')) # <p class="appintro-title">xx</p> 
print(soup.p) # <p class="appintro-title">xx</p>

# 下面两种写法是一样的
r1 = soup.find('div', 'pl2')
r2 = soup.find('div', class_='pl2')
print(r1 == r2)  # True

# 通过attrs实现更灵活的查找
# 如果标签包含多个属性,这里的attrs也可以添加多个
# print(soup.find('div', attrs={'class': 'pl2'})) # 查找class属性为pl2的div标签
# 这个案例不太合适 一般是通过id或者class去定位的 不过确实能这样操作
print(soup.find('td', attrs={'width': '100', 'valign': 'top'}))

# 小练习 获取图片的src
print(soup.find('a', 'nbg').img.attrs) 
print(soup.find('a', 'nbg').img['src'])  # ./xx读书 Top 250_files/s1070959.jpg
print(soup.find('a', 'nbg').img.attrs.get('src'))  # ./xx读书 Top 250_files/s1070959.jpg  这种写法会更麻烦一点

获取文本内容

  1. string 如果结果不包含额外的子标签,直接拿到内容,否则得到一个None
  2. text 拿到筛选条件的全部文本,如果内部还有子标签也包含直接文本,也能获取到。
  3. get_text() 同text 只是一这个是一个方法
  4. contents 获取文本内容,结果是列表显示
    1. 注意:字符串没有.contents 属性,因为字符串没有子节点。
  5. strings 拿到的是一个迭代器,可以通过next或者变量去取值
  6. stripped_strings是一个生成器,用于获取标签内所有文本内容,并去除每个文本的前后空白字符(包括空格、换行符等)。但是,并不会去除文本中间的空格字符,可以获取到结果后通过正则或者字符串的其他方法进一步处理。
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

print(soup.find('div', 'pl2').string) # None 因为包含其他的子标签
print(soup.find('p', 'pl').string) # [清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元 这里能拿到结果因为换了一个标签
print(soup.find('div', class_='pl2').text)
"""
[清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元

                红楼梦

"""
# soup.text 同 soup.get_text()
print(soup.find('p', 'pl').text) # [清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元
print(soup.find('p', 'pl').get_text()) # [清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元 

# 使用contents拿到文本结果,结果放列表种
print(soup.find('p', 'pl').contents)  # ['[清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元']

# 拿到可迭代对象
g = soup.find('p', 'pl').strings
print(next(g)) # [清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元

# 去掉换行和空格 很常用 推荐掌握
# 有的时候并
g = soup.find('tr', 'item').stripped_strings
print(list(g)) 
# ['红楼梦', '[清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元', '9.6', '(\n                    421411人评价\n                )', '都云作者痴,谁解其中味?']

美化匹配结果 prettify

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

# 未美化
print(soup.find('div', attrs={"class", 'pl2'}))
"""
<div class="pl2">
<a href="https://book.xx.com/subject/1007305/" onclick="&quot;moreurl(this,{i:'0'})&quot;" title="红楼梦">
                红楼梦


              </a>



                  <img alt="可试读" src="./xx读书 Top 250_files/read.gif" title="可试读"/>
</div>
"""

# 美化后
print(soup.find('div', 'pl2').prettify())
"""
<div class="pl2">
 <a href="https://book.xx.com/subject/1007305/" onclick="&quot;moreurl(this,{i:'0'})&quot;" title="红楼梦">
  红楼梦
 </a>
 <img alt="可试读" src="./xx读书 Top 250_files/read.gif" title="可试读"/>
</div>
"""

获取所有结果 find_all

  1. find_all 返回的结果是一个列表,如果拿不到不会报错,而是得到一个空列表[ ]
  2. 可以通过limit参数,控制获取结果的条数
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')

# 获取class属性为 pl2 的div标签的全部结果
print(soup.find_all('div', 'pl2'))  

# 通过切片获取前2条结果
print(soup.find_all('div', class_='pl2')[:2])  

# 通过limit去获取结果 学过数据库的应该对这个不陌生 这个参数需要写在括号里面
print(soup.find_all('div', 'pl2', limit=2))  

# 获取多个标签
# 注意 这里面只能放标签名称
print(soup.find_all(['p', 'img']))

css选择器 select

css选择器能使用的这里都能使用,得到的结果是一个列表,如果拿不到结果返回空列表,不会报错。

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')


# 拿到全部a标签
print(soup.select('a'))

# 类选择器 选择class为pl2的全部标签
print(soup.select('.pl2'))

# id选择器 获取 id为db-global-nav 的全部结果
print(soup.select('#db-global-nav'))

# 如果要拿到结果可以通过索引取值
print(soup.select('#db-global-nav')[0])

# 只拿一个结果,这样就不用在索引取值了
print(soup.select_one('title'))  # <title>xx读书 Top 250</title>

# 儿子选择器 > 拿到class属性为pl2的下面的第一个a
print(soup.select('.pl2>a'))

# 可以一直选 拿到class属性为pl2标签下面的第一个a标签,下面的第一个img标签
print(soup.select('.pl2>a>img'))

# 后代选择器 全部子孙
print(soup.select('.pl2 span'))

# 属性选择器
# 属性选择器就是 标签名[属性名=属性值]
print(soup.select('div[class="pl2"]'))

# 使用select()方法查询soup中所有带有class属性为"item"的tr标签下的p标签中的span标签,且这些span标签带有class属性为"inq"
print(soup.select('tr[class="item"] p.quote>span[class="inq"]'))

更多选择可以参考我的博客:点我跳转

通过正则去匹配

import re
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('xx读书 Top 250.html', encoding='utf-8'), 'lxml')


# 查询所有网址种包含 subject 字段的链接
print(soup.find_all(href=re.compile('subject')))

# 定义正则匹配规则
pattern = re.compile(r'.*?(\d+人评价).*?', re.S)

# 案例1
print(soup.find('span', 'pl').string)
"""
(
                    421411人评价
                
"""

# 案例2
print(soup.find('span', string=pattern).string)
"""
(
                    421411人评价
                
"""

获取注释 comments 了解即可

from bs4 import BeautifulSoup

# 示例 HTML 代码,包含注释内容
html_doc = """
<html>
<head>
    <title>Sample Page</title>
</head>
<body>
    <!-- This is a comment -->
    <div id="content">
        <p>Paragraph 1</p>
        <!-- Another comment -->
        <p>Paragraph 2</p>
    </div>
</body>
</html>
"""

# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_doc, 'html.parser')

# 使用 comments 方法获取注释内容
comments = soup.find_all(string=lambda text: isinstance(text, Comment))

# 输出注释内容
for comment in comments:
    print(comment)

bs4练习

import threading
from pathlib import Path
from itertools import zip_longest
from concurrent.futures import ThreadPoolExecutor

import requests
from openpyxl import Workbook
from bs4 import BeautifulSoup
from fake_useragent import UserAgent

# 获取当前文件所在路径
file_path = Path(__file__).parent

# 构建xx图书Top250各页的URL列表
url_list = [
    f'https://book.xx.com/top250?start={page * 25}'
    for page in range(10)
]

# 创建互斥锁,用于保护共享资源
lock = threading.Lock()

# 共享资源,用于计数已经处理的页面数量
shared_resource = 0

# 创建一个工作簿和工作表
wb = Workbook()
ws = wb.active
# 添加表头
ws.append(['书名', '书籍链接', '评分', '参与评分人数', '作者和价格', '一句话简介'])


def get_request(url):
    """
    发送HTTP请求获取页面内容

    Args:
        url (str): 要请求的URL

    Returns:
        bytes: 页面内容的字节串,或者None(如果请求失败)
    """
    try:
        # 发送HTTP GET请求
        response = requests.get(url=url, headers={'User-Agent': UserAgent().random})
        response.raise_for_status()  # 如果响应状态码不是200,则抛出异常
    except requests.exceptions.RequestException:
        pass  # 如果请求出现异常,不做任何处理
    else:
        return response.content  # 返回页面内容的字节串


def fetch_content(s):
    """
    解析页面内容,并将解析结果写入Excel表格

    Args:
        s (concurrent.futures.Future): 表示一个异步操作的Future对象
    """
    global shared_resource
    data = s.result()  # 获取异步操作的结果(即页面内容)

    with lock:
        shared_resource += 1  # 增加已处理页面的数量

        # 使用BeautifulSoup解析页面内容
        soup = BeautifulSoup(data, 'lxml')

        # 获取页面中各项信息
        title_name = soup.select(".item .pl2 a")  # 书名
        link_name = soup.select(".item .pl2 a")  # 书籍链接
        star_name = soup.select('.rating_nums')  # 评分
        evaluate_name = soup.select('.star>span.pl')  # 参与评分人数
        detail_name = soup.find_all('p', 'pl')  # 作者和价格
        inq_name = soup.find_all('span', attrs={'class': 'inq'})  # 一句话简介

        # 将信息写入Excel表格
        for title, link, star, evaluate, detail, inq in zip_longest(title_name, link_name, star_name, evaluate_name,
                                                                    detail_name, inq_name):
            # 处理空数据
            if title is None:
                title = '/'
            else:
                title = ''.join(title.text.split())

            link = link['href']
            star = star.string
            evaluate = evaluate.string.split()[1].replace('人评价', '')
            detail = detail.string

            # 处理空数据
            if inq is None:
                inq = '/'
            else:
                inq = inq.string

            # 写入Excel表格
            ws.append([title, link, star, evaluate, detail, inq])


if __name__ == '__main__':
    # 使用线程池执行HTTP请求
    with ThreadPoolExecutor(max_workers=10) as t:
        futures = [t.submit(get_request, url) for url in url_list]
        # 处理每个页面的响应内容
        for future in futures:
            future.add_done_callback(fetch_content)

    # 保存Excel文件
    wb.save(rf"{file_path}/xx读书top250结果.xlsx")

import base64
from threading import Thread
import requests
from bs4 import BeautifulSoup
from pathlib import Path
from fake_useragent import UserAgent


class SpiderMixin(Thread):
    BASE_DIR = Path(__file__).parent
    base_url = 'https://www.xx.com/'

    def __init__(self, url):
        super().__init__()
        self.url = url

    @staticmethod
    def random_header():
        headers = {
            'User-Agent': UserAgent().random
        }
        return headers

    def get_request(self):
        try:
            response = requests.get(url=self.url, headers=SpiderMixin.random_header())
            response.encoding = 'utf-8' if response.encoding == 'utf-8' else response.apparent_encoding
            response.raise_for_status()
        except requests.exceptions.RequestException:
            pass
        else:
            return response.text

    def encryption(self):
        url = ''
        return base64.b64decode(url).decode()


class FetchPage(SpiderMixin):
    title = ''
    url_list = []

    def __init__(self, url):
        super().__init__(url)
        self.url = url
        self.get_url()

    def get_url(self):
        html_doc = self.get_request()
        soup = BeautifulSoup(html_doc, 'lxml')
        title = soup.h1.string
        self.title = title
        link_list = soup.select(".book-mulu a")
        for link in link_list:
            novel_link = self.base_url + link['href']
            self.url_list.append(novel_link)

    def create_dir(self):
        file_name = self.BASE_DIR / self.title
        file_name.mkdir(parents=True, exist_ok=True)


class BookSpider(SpiderMixin):
    def __init__(self, url, file_name):
        super().__init__(url)
        self.url = url
        self.file_name = file_name

    def run(self):
        self.fetch_content()

    def fetch_content(self):
        content = self.get_request()
        soup = BeautifulSoup(content, 'lxml')
        title = soup.h1.string
        article_content = soup.select(".chapter_content p")
        with open(rf'{self.BASE_DIR}/{self.file_name}/{title}.txt', 'wt', encoding='utf-8') as file:
            for item in article_content:
                content = ''.join(list(item.stripped_strings))
                file.write(f"{content}\n")


if __name__ == '__main__':
    url = 'https://www.xx.com/book/suitangyanyi.html'

    page = FetchPage(url=url)
    title = page.title
    page.create_dir()

    for link in page.url_list:
        t = BookSpider(url=link, file_name=title)
        t.start()

posted @ 2024-03-28 15:24  小满三岁啦  阅读(8)  评论(0编辑  收藏  举报