用Python爬取图片的两种姿势:从静态到动态的完整攻略(二)

一、前言

在前文中,我们详细介绍了 BeautifulSoup 这一工具。在处理静态数据爬取任务时,它确实展现出了快捷、方便的特性,能够高效地完成相关工作。然而,当前市面上诸多规范运营的网站,为了保障自身数据安全与运营秩序,纷纷采取了一系列反爬虫措施。

这些措施不仅涵盖了懒加载技术,使得页面数据不会一次性全部加载,增加爬取难度;部分网站还设置了登录认证拦截,只有通过特定身份验证的用户才能访问数据。在此情形下,BeautifulSoup 的局限性便暴露无遗,面对这些复杂的反爬虫机制,它往往显得力不从心。

那么,该如何突破这些阻碍,实现对数据的有效爬取呢?此时,我们不得不引入另一种功能强大的数据爬取工具 ——Selenium。它将为我们在应对复杂反爬虫场景时,开辟出一条全新的路径 。

二、动态网页的征服者——Selenium介绍

2.1.Selenium是什么?

原则上Selenium 是一个自动化测试工具,这个对于玩自动化测试的同学应该特别熟悉。同时也能很好地应用在爬虫领域。

它就像一个虚拟的浏览器,可以模拟用户在浏览器里的各种操作,比如点击按钮、滚动页面、输入文字等等。通过控制真实的浏览器(像 Chrome、Firefox),Selenium 能获取到完整的网页内容,包括那些需要加载才能显示的部分。

所以在动态爬取数据上,这个万一就很方便了,因此咱们直接用它来爬取一个动漫全集试试。因为是找百度上的动漫网站,但是其中的内容可能有些是一些特定平台的付费内容,所以

特此说明:

这里只提是供学习方法参考,而不涉及商用或者兜售传播之类的东西哈!!!

2.2.Selenium在Python爬虫中的常见函数介绍

2.2.1.初始化浏览器驱动

在使用 Selenium 开展网页爬取工作之前,初始化浏览器驱动是必不可少的前置步骤。浏览器驱动作为 Selenium 与浏览器之间沟通的桥梁,能够让我们通过代码控制浏览器的行为。

常见的浏览器驱动有 ChromeDriver、FirefoxDriver 等。不同的浏览器需要对应的驱动程序,并且驱动的版本要与浏览器版本相匹配,否则可能会出现兼容性问题,导致无法正常启动浏览器。

from selenium import webdriver

# 设置 ChromeDriver 的路径
driver_path = 'path/to/chromedriver'
# 创建 Chrome 浏览器驱动实例
driver = webdriver.Chrome(executable_path=driver_path)

为了避免每次都手动指定驱动路径,可以将驱动所在目录添加到系统的环境变量中,这样在代码中就可以直接使用 webdriver.Chrome() 来创建驱动实例,无需再指定 executable_path 参数。

2.2.2 打开网页

使用 get() 方法可以打开指定的网页。这是一个非常基础且常用的操作,通过该方法可以让浏览器访问指定的 URL 地址。

# 打开指定网页
driver.get('https://www.example.com')

当执行 get() 方法时,浏览器会尝试加载指定的网页,直到页面加载完成或者达到超时时间。

但是在实际应用中,有些网页可能加载速度较慢,为了避免因页面未完全加载而导致后续操作失败,可以结合等待机制来确保页面加载完成后再进行下一步操作。

2.2.3 查找元素

Selenium 提供了多种查找元素的方法,这些方法可以根据元素的不同属性来定位元素。

通过 ID 查找元素

使用 find_element_by_id() 方法可以通过元素的 ID 属性查找元素。ID 属性在一个 HTML 页面中是唯一的,因此通过 ID 查找元素是一种非常高效且准确的方式。

# 查找 ID 为 'element_id' 的元素
element = driver.find_element_by_id('element_id')

在查找元素时,如果元素的 ID 是动态生成的,那么就不能直接使用这种方法。此时可以考虑结合其他属性或者使用 XPath 来定位元素。

通过类名查找元素

使用 find_element_by_class_name() 方法可以通过元素的类名查找元素。类名通常用于给一组具有相同样式的元素添加样式,因此一个页面中可能会有多个元素具有相同的类名。

# 查找类名为 'element_class' 的元素
element = driver.find_element_by_class_name('element_class')

当一个页面中有多个元素具有相同的类名时,该方法只会返回第一个匹配的元素。如果需要查找所有匹配的元素,可以使用 find_elements_by_class_name() 方法。

通过标签名查找元素

使用 find_element_by_tag_name() 方法可以通过元素的标签名查找元素。HTML 页面中有很多不同类型的标签,如 divpa 等。

# 查找标签名为 'div' 的元素
element = driver.find_element_by_tag_name('div')

同样,该方法只会返回第一个匹配的元素。如果需要查找所有匹配的元素,可以使用 find_elements_by_tag_name() 方法。而且,由于一个页面中可能会有大量相同标签名的元素,使用这种方法定位元素时可能会不够准确,需要结合其他方法进行筛选。

通过 CSS 选择器查找元素

使用 find_element_by_css_selector() 方法可以通过 CSS 选择器查找元素。CSS 选择器是一种强大的元素定位工具,可以根据元素的标签名、类名、ID 等属性组合来定位元素。

# 查找 CSS 选择器为 'div.class_name' 的元素
element = driver.find_element_by_css_selector('div.class_name')

CSS 选择器的语法非常灵活,可以通过组合不同的属性来实现更精确的元素定位。例如,可以使用 #element_id 来定位具有特定 ID 的元素,使用 .class_name 来定位具有特定类名的元素。

通过 XPath 查找元素

使用 find_element_by_xpath() 方法可以通过 XPath 表达式查找元素。XPath 是一种用于在 XML 文档中定位节点的语言,在 HTML 页面中也可以使用 XPath 来定位元素。

# 查找 XPath 为 '//div[@id="element_id"]' 的元素
element = driver.find_element_by_xpath('//div[@id="element_id"]')

XPath 可以根据元素的属性、位置等信息进行非常精确的元素定位。但是,XPath 表达式的编写相对复杂,需要对 HTML 结构有一定的了解。在编写 XPath 表达式时,可以使用浏览器的开发者工具来辅助生成。

2.2.4 操作元素

找元素后,可以对其进行各种操作,常见的操作有以下几种:

输入文本

使用 send_keys() 方法可以向输入框中输入文本。这在模拟用户在表单中输入信息时非常有用。

# 查找 ID 为 'input_id' 的输入框元素
input_element = driver.find_element_by_id('input_id')
# 向输入框中输入文本
input_element.send_keys('Hello, World!')

在输入文本之前,可以先使用 clear() 方法清空输入框中的原有内容,确保输入的文本是准确的。另外,如果需要输入特殊字符,可以使用 Keys 类来模拟按键操作。

点击元素

使用 click() 方法可以点击元素。这在模拟用户点击按钮、链接等操作时非常常用。

# 查找 ID 为 'button_id' 的按钮元素
button_element = driver.find_element_by_id('button_id')
# 点击按钮
button_element.click()

在点击元素之前,可以先使用 is_enabled() 和 is_displayed() 方法来检查元素是否可用和可见,避免因元素不可用或不可见而导致点击失败。

2.2.5 等待页面加载

在网页中,有些元素可能需要一定的时间才能加载完成,因此需要使用等待机制。Selenium 提供了两种等待方式:显式等待和隐式等待。

显式等待

显式等待是指在代码中指定一个条件,直到该条件满足为止。这种方式可以让我们更加精确地控制等待时间,避免不必要的等待。

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 等待最多 10 秒,直到 ID 为 'element_id' 的元素可见
element = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, 'element_id'))
)

显式等待可以根据不同的条件进行等待,如元素可见、元素可点击、元素存在等。在实际应用中,要根据具体的需求选择合适的条件。另外,为了避免等待时间过长,可以设置一个合理的超时时间。

隐式等待

隐式等待是指在查找元素时,等待一段时间,如果在这段时间内元素没有出现,则抛出异常。这种方式比较简单,只需要设置一次等待时间,后续所有的元素查找操作都会自动应用这个等待时间。

# 设置隐式等待时间为 10 秒
driver.implicitly_wait(10)

隐式等待适用于页面加载速度相对稳定的情况。但是,如果页面中有一些元素加载时间较长,使用隐式等待可能会导致整个程序的运行时间过长。因此,在实际应用中,可以结合显式等待和隐式等待来提高效率。

2.2.6 关闭浏览器

使用 quit() 方法可以关闭浏览器。这是在爬虫程序结束时必须要执行的操作,否则浏览器进程可能会一直存在,占用系统资源。

# 关闭浏览器
driver.quit()

在关闭浏览器之前,可以先保存一些必要的数据,如爬取到的信息、浏览器的会话状态等。另外,为了确保浏览器能够正常关闭,可以使用 try...finally 语句来保证 quit() 方法一定会被执行。

另外要注意的是:

浏览器驱动实例不是线程安全的,这意味着在多线程环境下,如果多个线程同时操作同一个浏览器驱动实例,可能会导致数据不一致、操作冲突等问题。例如,一个线程正在点击某个元素,而另一个线程同时在查找另一个元素,这可能会导致浏览器的行为变得不可预测。

因此,在多线程环境下,每个线程都应该有自己独立的浏览器驱动实例,避免多个线程共享同一个实例。

三、Selenium 爬取动漫网站流程

Step1:安装Python的Selenium 库及浏览器驱动

pip install selenium
# 记得下载对应浏览器的driver,推荐ChromeDriver

与BeautifulSoup不同在于,除了对应的Selenium 库以外要使用 Selenium,得先下载对应浏览器的驱动,比如 Chrome 浏览器就得下载 ChromeDriver。把驱动的路径配置好,这样 Selenium 才能和浏览器 “沟通”。

这里我们使用谷歌浏览器的驱动就行,对应资源大家可自行自行或者到我资源自取。

 Step2:分析网页动漫图片元素信息

这里我们以以前的人气漫画《偷星九月天》为例,先进入其中某一章节后进行元素分析

这个就更简单了,每张图片地址就在div块下面的img里面,也可以选data-original里面的链接,因为都一样。只要能确定图片地址就行,所以具体过程就不在重复赘述了,与上面一致。 

但是需要注意的是,这个网站咱们在访问漫画的章节内容是他是有个加载中的过程。所以使用BeautifulSoup获取是得不到实际的网站信息的,不信咱们看看整体网页的解析信息如何:

D:\pthon_project\venv\Scripts\python.exe D:\pthon_project\30-Days-Of-Python\22_Day_Web_scraping\动漫数据爬取-常规-BeautifulSoup\web_test.py 
<!DOCTYPE html>

<html>
<head>
……
<body>
<header class="header level">
<div class="level-item">
<div aria-label="breadcrumbs" class="breadcrumb">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/category">漫画</a></li>
<li><a href="/25988">偷星九月天</a></li>
<li><span>第02话 初遇九月天</span></li>
</ul>
</div>
<button class="button is-danger is-rounded"><span class="j-read-i">1</span>/24</button>
</div>
</header>
<main class="main-content">
<div class="container" id="content-container">
<div class="chapter-detail">
<div class="chapter-images">
<div class="chapter-image loading">
<div class="progress-container">
<svg class="progress-ring" height="150" width="150">
<circle class="progress-ring__background" cx="50" cy="50" r="45"></circle>
<circle class="progress-ring__circle" cx="50" cy="50" r="45"></circle>
</svg>
<div class="progress-text">0%</div>
</div>
<div class="loading-message">
<p>正在加载图片,请稍候</p>
</div>
</div>
</div>
</div>
</div>
<div id="floatbtn">
<a href="/25988">
<i class="iconfont icon-chapter"></i>
<span class="txt">章节</span>
</a>

……

进程已结束,退出代码为 0

 很明显图片信息没有,仅仅获取到了<p>正在加载图片,请稍候</p>所以这时候就需要使用Selenium来帮忙了。

Step3:Selenium 爬取漫画章节图片

首先设置一下谷歌浏览器及驱动的配置信息,尝试通过Selenium 启动页面:

# 设置Chrome选项
chrome_options = Options()
chrome_options.binary_location = "C:\Program Files\Google\Chrome\Application\chrome.exe"
chrome_options.add_argument('--start-maximized')  # 最大化窗口
chrome_options.add_argument('--disable-extensions')  # 禁用扩展
chrome_options.add_argument('--disable-gpu')  # 禁用GPU,使用无头模式
# 将随机的 User - Agent 添加到 ChromeOptions
chrome_options.add_argument(f'user-agent={random_user_agent}')

# 设置chromedriver路径
service = Service("C:\Program Files\Google\Chrome\Application\chromedriver.exe")

# 初始化浏览器
driver = webdriver.Chrome(service=service, options=chrome_options)

# 定义漫画章节地址
comic_chapter_url = 'https://www.haoguoman.net/25988/1.html'

# 打开章节页面
driver.get(comic_chapter_url)

运行后可以看到已经可以自动打开我们指定的网址界面了:

但是就像我之前说的,它这里会比我们平常用浏览器直接访问有一个明显的懒加载过程,所以我们得在其中加一些js的代码来模拟滚动加载的过程:

# 等待页面加载完成,这里设置为最大等待时间为30秒
WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.chapter-image")))

# 模拟滚动加载页面内容
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(4)  # 等待页面加载新内容
    new_height = driver.execute_script("return document.body.scrollHeight")
    if new_height == last_height:
        break
    last_height = new_height

这也是Selenium比BeautifulSoup更方便的地方,我们可以通过js的形式定制化我们的操作需求并让Selenium自动执行。

最后再把我们定位图片地址信息、新建文件夹以及下载保存的操作补充上去,就完成基础的流程:

# 提取图片信息
image_elements = driver.find_elements(By.CSS_SELECTOR, "div.chapter-image img")
image_urls = [img.get_attribute('src') for img in image_elements]

# 确保保存图片的文件夹存在
save_folder = '偷星九月天'
if not os.path.exists(save_folder):
    os.makedirs(save_folder)

# 下载并保存图片
for index, url in enumerate(image_urls):
    response = requests.get(url)
    if response.status_code == 200:
        file_path = os.path.join(save_folder, f'image_{index + 1}.jpg')
        with open(file_path, 'wb') as file:
            file.write(response.content)
        logging.info(f"图片已保存: {file_path}")
    else:
        logging.error(f"无法下载图片: {url}")

# 关闭浏览器
driver.quit()

但是这个逻辑在实际使用上还是有点问题,图片加载过慢,网站一下就直接到底部栏了,所以导致中间有部分图片没加载因此下载图片缺失的问题。

D:\pthon_project\venv\Scripts\python.exe D:\pthon_project\30-Days-Of-Python\22_Day_Web_scraping\动漫数据爬取-进阶-Selenium\selenium_get_data.py 
2025-03-20 21:03:54,263 - INFO - 图片已保存: 偷星九月天\image_1.jpg
2025-03-20 21:03:56,008 - INFO - 图片已保存: 偷星九月天\image_2.jpg
2025-03-20 21:03:58,915 - INFO - 图片已保存: 偷星九月天\image_3.jpg
2025-03-20 21:04:04,201 - INFO - 图片已保存: 偷星九月天\image_4.jpg
2025-03-20 21:04:06,038 - INFO - 图片已保存: 偷星九月天\image_5.jpg
2025-03-20 21:04:08,558 - INFO - 图片已保存: 偷星九月天\image_6.jpg

进程已结束,退出代码为 0

因此还需要优化一下逻辑,让其有规律的下滑加载,保证图片能加载完成后被下载下来,当然为了让整体代码更加优雅我们一般推荐封装函数,并且再有问题的地方加上日志记录,这样后面调试或使用就更加方便了:

import os
import logging
import time
from fake_useragent import UserAgent
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support import expected_conditions as EC
import requests
from selenium.webdriver.support.wait import WebDriverWait

# 设置日志配置,方便调试信息输出
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def setup_driver():
    # 创建 UserAgent 对象
    ua = UserAgent()
    # 获取随机的 User - Agent
    random_user_agent = ua.random

    # 设置 Chrome 选项
    chrome_options = Options()
    chrome_options.binary_location = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
    chrome_options.add_argument('--start-maximized')  # 最大化窗口
    chrome_options.add_argument('--disable-extensions')  # 禁用扩展
    chrome_options.add_argument('--disable-gpu')  # 禁用 GPU,使用无头模式
    # 将随机的 User - Agent 添加到 ChromeOptions
    chrome_options.add_argument(f'user-agent={random_user_agent}')

    # 设置 chromedriver 路径
    service = Service(r"C:\Program Files\Google\Chrome\Application\chromedriver.exe")

    # 初始化浏览器
    driver = webdriver.Chrome(service=service, options=chrome_options)
    return driver

def wait_for_element(driver, css_selector, timeout=30):
    try:
        element = WebDriverWait(driver, timeout).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, css_selector))
        )
        element_found_time = time.time()
        logging.info(f"元素在 {element_found_time} 时刻被获取到,元素选择器: {css_selector}")
        return element
    except Exception as e:
        logging.error(f"元素未找到,元素选择器: {css_selector},错误信息: {e}")
        return None

def scroll_to_bottom(driver, scroll_pause_time=2, max_scrolls=100, scroll_height=1000):
    scroll_count = 0
    last_height = driver.execute_script("return document.body.scrollHeight")
    consecutive_same_height = 0
    logging.info(f"开始滚动,初始页面高度: {last_height}")

    while scroll_count < max_scrolls:
        # 滚动指定的高度
        driver.execute_script(f"window.scrollBy(0, {scroll_height});")
        logging.info(f"第 {scroll_count + 1} 次滚动,滚动高度: {scroll_height}")

        # 等待页面加载新内容
        time.sleep(scroll_pause_time)

        # 计算新页面的高度
        new_height = driver.execute_script("return document.body.scrollHeight")
        logging.info(f"第 {scroll_count + 1} 次滚动后,页面高度: {new_height}")

        if new_height == last_height:
            consecutive_same_height += 1
            logging.info(f"页面高度未变化,连续未变化次数: {consecutive_same_height}")
            if consecutive_same_height >= 5:
                # 如果连续 5 次高度相同,认为已到底部
                logging.info("连续 5 次页面高度未变化,认为已滚动到页面底部,停止滚动")
                break
        else:
            consecutive_same_height = 0
            logging.info("页面高度发生变化,重置连续未变化次数为 0")

        # 更新上次高度
        last_height = new_height
        scroll_count += 1

    logging.info(f"滚动结束,共滚动 {scroll_count} 次")

def download_images(driver, target_folder):
    # 获取所有图片的地址信息
    try:
        images = WebDriverWait(driver, 30).until(
            EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.chapter-image img"))
        )
        logging.info(f"成功获取到 {len(images)} 张图片元素")
    except Exception as e:
        logging.error(f"获取图片元素失败,错误信息: {e}")
        return

    # 确保目标文件夹存在
    if not os.path.exists(target_folder):
        os.makedirs(target_folder)
        logging.info(f"创建目标文件夹: {target_folder}")

    # 下载图片并按顺序保存到本地文件夹
    for index, image in enumerate(images):
        try:
            image_url = image.get_attribute("src")
            if image_url:
                # 下载图片
                response = requests.get(image_url, stream=True)
                response.raise_for_status()

                # 保存图片
                image_path = os.path.join(target_folder, f"image_{index + 1}.jpg")
                with open(image_path, 'wb') as handler:
                    for chunk in response.iter_content(chunk_size=8192):
                        handler.write(chunk)

                logging.info(f"成功下载图片: {image_url} 到 {image_path}")
            else:
                logging.warning(f"第 {index + 1} 张图片 URL 为空或未找到")
        except Exception as e:
            logging.error(f"下载第 {index + 1} 张图片失败,错误信息: {e}")


# 定义漫画章节地址
comic_chapter_url = 'https://www.haoguoman.net/25988/1.html'

# 初始化浏览器
driver = setup_driver()

# 打开章节页面
driver.get(comic_chapter_url)
logging.info(f"打开页面: {comic_chapter_url}")

# 等待元素出现
wait_for_element(driver, "div.chapter-image")

# 模拟滚动加载
scroll_to_bottom(driver)

# 下载图片
download_images(driver, "偷星九月天")

# 关闭浏览器
driver.quit()
logging.info("浏览器已关闭")

然后这一章节的漫画也就全部下载到本地了:

Step4:功能优化提升 

虽然完成了一个章节的漫画爬取和下载但是肯定有同学会问:

一个动漫四五百个章节呢,这么一个一个下不得累死?还有更好的更快地办法吗,兄弟?

有的!兄弟,有滴!这样的办法还有九种!

玩笑归玩笑哈,九种可能太多但是现成最简单的还是有的。每一个章节无非对应的就是一个<ul><li>包裹的地址:

所以咱们把他遍历出来然后逐步爬取不就搞定了。 

因此咱们需要先在主页取获取所有子章节链接信息:

def get_chapter_links(driver):
    chapter_links = []
    try:
        logging.info("开始获取子章节链接信息...")
        elements = driver.find_elements(By.XPATH, "//*[@id='content-container']/div[3]/div[2]/ul/li/a")

        for element in elements:
            link = element.get_attribute("href")
            title = element.text
            chapter_links.append((link, title))
            # 模拟鼠标悬停操作,增加真实性
            ActionChains(driver).move_to_element(element).perform()
            time.sleep(0.5)
        logging.info(f"成功获取到 {len(chapter_links)} 个子章节链接")
    except Exception as e:
        logging.error(f"获取子章节链接失败,错误信息: {e}")
    return chapter_links

但是呢,由于章节太多了,480多张获取起来其实很慢,因此我们可以适当去创建协程的方式来解决。

协程在处理 I/O 密集型任务时效率非常高,因为它可以在等待 I/O 操作完成时让出控制权,让其他协程继续执行,从而充分利用 CPU 资源。在章节链接获取这种 I/O 密集型任务中,协程通常比多线程更高效。

async def process_element(element):
    loop = asyncio.get_running_loop()
    link = await loop.run_in_executor(None, element.get_attribute, "href")
    title = await loop.run_in_executor(None, lambda: element.text)
    # 模拟鼠标悬停操作,增加真实性
    await loop.run_in_executor(None, lambda: ActionChains(webdriver.Chrome()).move_to_element(element).perform())
    await asyncio.sleep(0.5)
    return (link, title)

async def get_chapter_links(driver):
    chapter_links = []
    try:
        logging.info("开始获取子章节链接信息...")
        elements = driver.find_elements(By.XPATH, "//*[@id='content-container']/div[3]/div[2]/ul/li/a")

        tasks = [process_element(element) for element in elements]
        results = await asyncio.gather(*tasks)
        chapter_links.extend(results)

        logging.info(f"成功获取到 {len(chapter_links)} 个子章节链接")
    except Exception as e:
        logging.error(f"获取子章节链接失败,错误信息: {e}")
    return chapter_links

# 主函数
async def main():
    base_url = 'https://www.haoguoman.net'
    start_url = 'https://www.haoguoman.net/25988'

    driver = setup_driver()
    driver.get(start_url)

    chapter_links = await get_chapter_links(driver)
    print(chapter_links)
    driver.quit()

但是在批量数据爬取的时候可能会遇到很多异常情况,比如网络异常,因此在下载图片的过程中要引入重试机制,记录未下载完成的图片地址或者章节。

同时在批量下载数据时,可以加上进度查询的功能,让其在控制台日志打印时显示漫画的下载进度。最后在优化一下图片的爬取效率,同样给加上协程配合。这时候代码就比较长了,所以咱们最好用模块化开发的方式,把每个模块分开编写代码得到大致的整体的模块信息:

动漫数据爬取-进阶-Selenium/
├── 偷星九月天/
├── main.py
├── browser_setup.py
├── link_fetcher.py
├── image_downloader.py
├── logger_setup.py

日志配置模块 logger_setup.py

import logging
import colorlog


def setup_logger():
    """
    日志格式设置,使用不同颜色显示时间、日志级别和消息,便于其他模块调用。
    :param log_colors: 日志颜色设置
    :param fmt: 日志格式设置
    :param datefmt: 时间格式设置
    :return:
    """
    log_colors = {
        'DEBUG': 'cyan',
        'INFO': 'green',
        'WARNING': 'yellow',
        'ERROR': 'red',
        'CRITICAL': 'red,bg_white'
    }
    formatter = colorlog.ColoredFormatter(
        '%(log_color)s%(asctime)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        log_colors=log_colors
    )
    logger = colorlog.getLogger(__name__)
    logger.setLevel(logging.INFO)
    # 创建控制台处理器并设置格式
    ch = logging.StreamHandler()
    ch.setFormatter(formatter)
    logger.addHandler(ch)
    return logger

 浏览器驱动配置模块browser_setup.py

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from fake_useragent import UserAgent


def setup_driver():
    """
    设置浏览器驱动
    1. 创建 UserAgent 对象
    2. 获取随机的 User - Agent
    3. 设置 Chrome 选项
    4. 设置 chromedriver 路径
    5. 初始化浏览器
    :return:
    """
    # 创建 UserAgent 对象
    ua = UserAgent()
    # 获取随机的 User - Agent
    random_user_agent = ua.random

    # 设置 Chrome 选项
    chrome_options = Options()
    chrome_options.binary_location = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
    chrome_options.add_argument('--start-maximized')  # 最大化窗口
    chrome_options.add_argument('--disable-extensions')  # 禁用扩展
    chrome_options.add_argument('--disable-gpu')  # 禁用 GPU
    chrome_options.add_argument('--headless')  # 无头模式, 即不显示浏览器界面,但仍然能够执行页面渲染、JavaScript等操作。

    # 将随机的 User - Agent 添加到 ChromeOptions
    chrome_options.add_argument(f'user-agent={random_user_agent}')
    # 模拟浏览器指纹
    chrome_options.add_argument('--lang=en-US,en;q=0.9')
    chrome_options.add_argument('--accept-language=en-US,en;q=0.9')
    # 设置代理(如果需要)
    # chrome_options.add_argument('--proxy-server=http://your_proxy_ip:port')

    # 设置 chromedriver 路径
    service = Service(r"C:\Program Files\Google\Chrome\Application\chromedriver.exe")

    # 初始化浏览器
    driver = webdriver.Chrome(service=service, options=chrome_options)
    return driver

 章节链接获取模块link_fetcher.py

import asyncio
import json
import redis
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

# 连接 Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
CACHE_KEY = "chapter_links"


async def process_element(element, driver):
    loop = asyncio.get_running_loop()
    link = await loop.run_in_executor(None, element.get_attribute, "href")
    title = await loop.run_in_executor(None, lambda: element.text)
    # 模拟鼠标悬停操作,增加真实性
    await loop.run_in_executor(None, lambda: ActionChains(driver).move_to_element(element).perform())
    await asyncio.sleep(0.5)
    return (link, title)


async def get_chapter_links(driver, logger):
    # 确保 Redis 操作同步执行
    loop = asyncio.get_running_loop()
    cached_links = await loop.run_in_executor(None, redis_client.get, CACHE_KEY)
    if cached_links:
        # 将字节类型转换为字符串类型
        cached_links = cached_links.decode('utf-8')
        chapter_links = json.loads(cached_links)
        logger.info(f"从 Redis 缓存中获取到 {len(chapter_links)} 个子章节链接")
        return chapter_links

    chapter_links = []
    try:
        logger.info("开始获取子章节链接信息...")
        elements = driver.find_elements(By.XPATH, "//*[@id='content-container']/div[3]/div[2]/ul/li/a")

        tasks = [process_element(element, driver) for element in elements]
        results = await asyncio.gather(*tasks)
        chapter_links.extend(results)

        logger.info(f"成功获取到 {len(chapter_links)} 个子章节链接")

        # 将获取到的章节链接存入 Redis 缓存
        await loop.run_in_executor(None, redis_client.set, CACHE_KEY, json.dumps(chapter_links))
    except Exception as e:
        logger.error(f"获取子章节链接失败,错误信息: {e}")
    return chapter_links

这里考虑到如果其中出现显网络异常,那么每次都得取爬取所有章节信息,所以我们这块加上了了Redis来作为临时的数据缓存,尽量节约网络资源。同时引入协程加快章节链接获取效率。 

章节动漫图片下载模块 image_downloader.py

import os
import requests
import asyncio

from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from tqdm import tqdm
from selenium.webdriver.support import expected_conditions as EC

# 未下载完成的图片和章节记录
failed_images = []
failed_chapters = []


async def download_image_with_retry(image_url, image_path, max_retries=5, logger=None):
    """
    异步下载图片,重试 max_retries 次,失败后记录到 failed_images 列表中
    :param image_url:  漫画单个章节的图片 URL
    :param image_path: 漫画 images 文件夹下单个章节的图片路径
    :param max_retries: 最大重试次数
    :param logger:
    :return:
    """
    retries = 0  # 重试次数
    while retries < max_retries:
        try:
            response = requests.get(image_url, stream=True)
            response.raise_for_status()

            with open(image_path, 'wb') as handler:
                #  8192 为每次写入的块大小
                for chunk in response.iter_content(chunk_size=8192):
                    handler.write(chunk)

            logger.info(f"成功下载图片: {image_url} 到 {image_path}")
            return True
        except Exception as e:
            retries += 1
            logger.warning(f"下载图片 {image_url} 失败,第 {retries} 次重试,错误信息: {e}")

    logger.error(f"下载图片 {image_url} 失败,已达到最大重试次数")
    failed_images.append(image_url)
    return False


async def download_images(driver, target_folder, logger):
    """
    异步下载漫画所有章节的图片
    :param driver:
    :param target_folder:
    :param logger:
    :return:
    """
    # 获取所有图片的地址信息
    try:
        images = WebDriverWait(driver, 30).until(
            EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.chapter-image img"))
        )
        logger.info(f"成功获取到 {len(images)} 张图片元素")
    except Exception as e:
        logger.error(f"获取图片元素失败,错误信息: {e}")
        failed_chapters.append(target_folder)
        return

    # 确保目标文件夹存在
    if not os.path.exists(target_folder):
        os.makedirs(target_folder)
        logger.info(f"创建目标文件夹: {target_folder}")

    # 使用 tqdm 显示下载进度
    tasks = []
    for index, image in enumerate(images):
        try:
            image_url = image.get_attribute("src")
            if image_url:
                # 下载图片
                image_path = os.path.join(target_folder, f"image_{index + 1}.jpg")
                # 创建任务
                task = asyncio.create_task(download_image_with_retry(image_url, image_path, logger=logger))
                # 记录任务
                tasks.append(task)
            else:
                logger.warning(f"第 {index + 1} 张图片 URL 为空或未找到")
        except Exception as e:
            logger.error(f"下载第 {index + 1} 张图片失败,错误信息: {e}")

    for future in tqdm(asyncio.as_completed(tasks), total=len(tasks), desc=f"下载 {target_folder} 图片"):
        await future

主要运行模块 main.py

import asyncio
import os
import time

from browser_setup import setup_driver
from link_fetcher_redis import get_chapter_links
from image_downloader import download_images, failed_images, failed_chapters
from logger_setup import setup_logger
from tqdm import tqdm
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By

# 等待元素加载
def wait_for_element(driver, css_selector, timeout=30, logger=None):
    try:
        element = WebDriverWait(driver, timeout).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, css_selector))
        )
        element_found_time = time.time()
        logger.info(f"元素在 {element_found_time} 时刻被获取到,元素选择器: {css_selector}")
        return element
    except Exception as e:
        logger.error(f"元素未找到,元素选择器: {css_selector},错误信息: {e}")
        return None

# 滚动页面到底部
def scroll_to_bottom(driver, scroll_pause_time=2, max_scrolls=100, scroll_height=1000, logger=None):
    scroll_count = 0
    last_height = driver.execute_script("return document.body.scrollHeight")
    consecutive_same_height = 0
    logger.info(f"开始滚动,初始页面高度: {last_height}")

    while scroll_count < max_scrolls:
        # 滚动指定的高度
        driver.execute_script(f"window.scrollBy(0, {scroll_height});")
        logger.info(f"第 {scroll_count + 1} 次滚动,滚动高度: {scroll_height}")

        # 等待页面加载新内容
        time.sleep(scroll_pause_time)

        # 计算新页面的高度
        new_height = driver.execute_script("return document.body.scrollHeight")
        logger.info(f"第 {scroll_count + 1} 次滚动后,页面高度: {new_height}")

        if new_height == last_height:
            consecutive_same_height += 1
            logger.info(f"页面高度未变化,连续未变化次数: {consecutive_same_height}")
            if consecutive_same_height >= 5:
                # 如果连续 5 次高度相同,认为已到底部
                logger.info("连续 5 次页面高度未变化,认为已滚动到页面底部,停止滚动")
                break
        else:
            consecutive_same_height = 0
            logger.info("页面高度发生变化,重置连续未变化次数为 0")

        # 更新上次高度
        last_height = new_height
        scroll_count += 1

    logger.info(f"滚动结束,共滚动 {scroll_count} 次")

# 主函数
async def main():
    logger = setup_logger()
    start_url = 'https://www.haoguoman.net/25988'

    driver = setup_driver()
    driver.get(start_url)

    chapter_links =await get_chapter_links(driver, logger)
    logger.info(f"获取到的章节链接: {chapter_links}")
    print(f"chapter_links 类型: {type(chapter_links)}")
    if chapter_links:
        print(f"第一个元素类型: {type(chapter_links[0])}")
        print(f"第一个元素内容: {chapter_links[0]}")

    # 使用 tqdm 显示章节处理进度
    for link, title in tqdm(chapter_links, desc="处理章节"):
        # 假设 link 已经是完整地址
        full_link = link
        target_folder = os.path.join("偷星九月天", title)

        driver.get(full_link)
        wait_for_element(driver, "div.chapter-image", logger=logger)
        scroll_to_bottom(driver, logger=logger)
        await download_images(driver, target_folder, logger)

    driver.quit()

    # 记录未下载完成的图片和章节
    if failed_images:
        logger.error(f"未下载完成的图片: {failed_images}")
    if failed_chapters:
        logger.error(f"未下载完成的章节: {failed_chapters}")

if __name__ == "__main__":
    asyncio.run(main())

最后运行后就可以看到整个动漫按章节进行图片爬取下载了,当然由于数量太多,时间也会比长,毕竟480多章呢,但是相比于一章一章的爬取肯定是方便多了。 

D:\pthon_project\venv\Scripts\python.exe D:\pthon_project\30-Days-Of-Python\22_Day_Web_scraping\动漫数据爬取-进阶-Selenium\优化后的整体代码模块\main.py 
2025-03-21 22:21:30 - INFO - 开始获取子章节链接信息...
2025-03-21 22:24:36 - INFO - 成功获取到 469 个子章节链接
2025-03-21 22:24:36 - INFO - 获取到的章节链接: [('https://www.haoguoman.net/25988/1.html', '第02话 初遇九月天'), ('https://www.haoguoman.net/25988/2.html', '第03话 超级发型师'), ('https://www.haoguoman.net/25988/3.html', '第04话 第二次交手'), ('https://www.haoguoman.net/25988/4.html', '第05话 水晶面具'), 
……
('https://www.haoguoman.net/25988/469.html', '第459话 未来再见')]

下载 偷星九月天\第02话 初遇九月天 图片:   0%|          | 0/24 [00:00<?, ?it/s]2025-03-22 11:18:41 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992941TII9Oei_47GP2tJe.jpg 到 偷星九月天\第02话 初遇九月天\image_1.jpg
2025-03-22 11:18:44 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992940kQKTk2uPS0uz8dRO.jpg 到 偷星九月天\第02话 初遇九月天\image_2.jpg
2025-03-22 11:18:46 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992938NnH1zfhQyCRmvR51.jpg 到 偷星九月天\第02话 初遇九月天\image_3.jpg
2025-03-22 11:18:47 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992937Lkp6P1imMwy9cQMq.jpg 到 偷星九月天\第02话 初遇九月天\image_4.jpg
2025-03-22 11:18:48 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992936VSQDTPx94DPpMcZh.jpg 到 偷星九月天\第02话 初遇九月天\image_5.jpg
2025-03-22 11:18:50 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992935LBs-0CNU4XwOSAfE.jpg 到 偷星九月天\第02话 初遇九月天\image_6.jpg
2025-03-22 11:18:51 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992934zDxXWx6dChECy1xT.jpg 到 偷星九月天\第02话 初遇九月天\image_7.jpg
2025-03-22 11:18:53 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/15779929330aEzbkQeZENgniEE.jpg 到 偷星九月天\第02话 初遇九月天\image_8.jpg
2025-03-22 11:18:54 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992932q8KerHkq7R376zsH.jpg 到 偷星九月天\第02话 初遇九月天\image_9.jpg
2025-03-22 11:18:56 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992931FZCPUMTqqpXmB7Cz.jpg 到 偷星九月天\第02话 初遇九月天\image_10.jpg
2025-03-22 11:18:57 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992930X5Nis8KMUNEuMxIu.jpg 到 偷星九月天\第02话 初遇九月天\image_11.jpg
2025-03-22 11:18:58 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/157799292958fOeqp1ljCHp1eB.jpg 到 偷星九月天\第02话 初遇九月天\image_12.jpg
2025-03-22 11:19:00 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992928b_Ho46x7mY6fCW_r.jpg 到 偷星九月天\第02话 初遇九月天\image_13.jpg
2025-03-22 11:19:03 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992927_K7FtMKmtQDwrUiz.jpg 到 偷星九月天\第02话 初遇九月天\image_14.jpg
2025-03-22 11:19:04 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/15779929265d9w5YUwr8vmBEk_.jpg 到 偷星九月天\第02话 初遇九月天\image_15.jpg
2025-03-22 11:19:07 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992925HUnjtEy4Tqdbh59M.jpg 到 偷星九月天\第02话 初遇九月天\image_16.jpg
2025-03-22 11:19:09 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/15779929249ESRjrPkEeafUJu7.jpg 到 偷星九月天\第02话 初遇九月天\image_17.jpg
2025-03-22 11:19:10 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992923eIp504Eg8BjFpqYW.jpg 到 偷星九月天\第02话 初遇九月天\image_18.jpg
2025-03-22 11:19:11 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992922TWMfyBkEWddczYsZ.jpg 到 偷星九月天\第02话 初遇九月天\image_19.jpg
2025-03-22 11:19:13 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992921SOuJn3vWM4eNJ8rO.jpg 到 偷星九月天\第02话 初遇九月天\image_20.jpg
2025-03-22 11:19:15 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992920SqO15Z3doc67mvnc.jpg 到 偷星九月天\第02话 初遇九月天\image_21.jpg
2025-03-22 11:19:17 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/15779929193AtCHywB_a_ZvN7d.jpg 到 偷星九月天\第02话 初遇九月天\image_22.jpg
2025-03-22 11:19:18 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992917q-Emr4J7ZIT_MfkZ.jpg 到 偷星九月天\第02话 初遇九月天\image_23.jpg
2025-03-22 11:19:22 - INFO - 成功下载图片: https://res.xiaoqinre.com/images/comic/576/1151872/1577992916sS1mVLn0Yj2_k0hu.jpg 到 偷星九月天\第02话 初遇九月天\image_24.jpg

下载 偷星九月天\第02话 初遇九月天 图片: 100%|██████████| 24/24 [00:42<00:00,  1.77s/it]
……

完成后我们不妨设想一下:既然可以在一个web网站上爬取整个漫画,那么能不能根据搜索条件自动去将获取到的结果,然后爬取相关的所有漫画呢,这样是不是就更方便!!!

留个设想大家可以去尝试一下完成这个需求~

四、总结

框架优缺点对比​

  1. BeautifulSoup:优点是使用简单,对简单静态网页的解析速度快,代码量相对较少。缺点就是面对复杂网页,尤其是有动态加载内容的网页,它就没辙了。它适用于那些结构清晰、内容不需要复杂交互就能获取的静态网页。​
  1. Selenium:优点很明显,能解决各种复杂网页的问题,不管是懒加载还是需要交互操作的网页,都能应对自如。但它也有缺点,就是运行速度相对较慢,因为要启动真实的浏览器,资源消耗也比较大。它适合爬取那些需要模拟用户操作才能获取完整数据的网站。​

安全与版权问题​

最后,一定要提醒大家,在进行数据爬取的时候,安全问题和版权问题千万不能忽视。首先,不要去爬取那些涉及个人隐私、商业机密或者有反爬虫机制的网站,不然可能会给自己带来法律风险。其次,对于有版权的图片、动漫等资源,仅仅用于个人学习和娱乐是可以的,但绝对不能用于商业用途,也不要随意传播。尊重知识产权,做一个合法合规的爬虫使用者。​

写在文末

好啦,今天关于 Python 爬虫用 BeautifulSoup 和 Selenium 爬取图片及动漫的分享就到这儿啦,希望对大家有所帮助,大家有问题可以随时交流哦!

后续继续带来爬虫在正式数据爬取存储与分析的文章,毕竟咱们玩这个东西肯定不仅限于娱乐,工作上可能也会有所需要,但是场景肯定也会更复杂,继续期待~


往期相关文章

用Python爬取图片的两种姿势:从静态到动态的完整攻略(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

深情不及里子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值