Python动态网页库存监控与Discord通知:Selenium实战指南

本教程详细探讨了使用python监控动态加载网页商品库存并发送discord通知的方法。针对传统网络爬虫(如beautifulsoup)在处理javascript渲染内容时的局限性,文章重点介绍了如何利用selenium这一无头浏览器工具来模拟用户行为,有效获取实时库存信息。教程涵盖了环境配置、代码实现、异步集成及最佳实践,旨在帮助读者构建健壮的库存监控系统。

1. 理解动态网页与传统爬虫的局限性

在构建库存监控系统时,我们通常会想到使用Python的requests库获取网页内容,再结合BeautifulSoup进行解析。然而,这种方法对于现代网站而言存在一个显著的局限性:许多网站的内容,尤其是商品库存状态,是通过JavaScript在浏览器端动态加载的。

问题分析: 当我们尝试使用requests.get(url)获取网页内容时,BeautifulSoup只能解析服务器返回的原始HTML源代码。如果商品的库存信息(例如特定尺码是否可选)是在页面加载后由JavaScript异步请求并渲染到DOM中的,那么这些信息将不会出现在requests获取的初始HTML中。

例如,在示例网站(courir.com)上,尺码选项的可用性(如尺码40)并不是直接嵌入在初始HTML中的。通过浏览器开发者工具检查,你会发现:

  • 当某个尺码缺货时,其对应的
  • 元素可能带有unselectable类或aria-disabled="true"属性。
  • 当尺码有货时,其
  • 元素可能带有selectable类,并且对应的链接(标签)是可点击的。

然而,这些状态的切换和元素的出现,都是在JavaScript执行之后才发生的。因此,单纯依赖soup.find('li', {'class': 'unselectable'})这样的代码,可能无法准确判断尺码40的实时库存状态,因为它无法“看到”JavaScript渲染后的页面。

2. 解决方案:使用Selenium处理动态内容

为了解决BeautifulSoup无法处理动态加载内容的问题,我们需要一个能够模拟真实浏览器行为的工具,即无头浏览器(Headless Browser)。Selenium就是这样一个强大的工具,它允许我们通过编程方式控制浏览器,执行JavaScript,等待元素加载,甚至模拟点击等用户交互。

Selenium工作原理: Selenium启动一个真实的浏览器实例(可以是可见的,也可以是无头的),然后通过WebDriver与该浏览器进行通信。这意味着Selenium能够“看到”并操作JavaScript渲染后的完整DOM。

2.1 环境准备

在使用Selenium之前,需要安装相应的库和浏览器驱动:

  1. 安装Selenium库:
    pip install selenium
  2. 安装浏览器驱动: Selenium需要一个与你本地浏览器版本匹配的WebDriver。常用的有:
    • ChromeDriver (for Chrome): 从 ChromeDriver Downloads 下载。
    • GeckoDriver (for Firefox): 从 GeckoDriver Releases 下载。 将下载的驱动文件(例如chromedriver.exe)放到系统的PATH环境变量中,或者在代码中指定其路径。

2.2 构建Selenium库存检查函数

我们将重构check_stock函数,使其使用Selenium来加载页面并检查尺码40的库存状态。为了与原有的asyncio框架兼容,我们将Selenium的同步操作封装在一个异步函数中,并使用loop.run_in_executor在单独的线程中执行。

import asyncio
import aiohttp
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time # For sleep in the main loop

# Discord Webhook URL (替换为你的实际URL)
webhook_url = 'YOUR_DISCORD_WEBHOOK_URL'

async def send_webhook_message(content):
    """
    异步发送Discord Webhook消息。
    """
    async with aiohttp.ClientSession() as session:
        try:
            async with session.post(webhook_url, json={"content": content}) as response:
                if response.status == 204:
                    print("Discord message sent successfully.")
                else:
                    print(f"Error sending Discord message. Status code: {response.status}, Response: {await response.text()}")
        except aiohttp.ClientError as e:
            print(f"Error sending Discord message: {e}")

def _check_stock_selenium_sync(url, size_to_check):
    """
    同步的Selenium函数,用于检查特定尺码的库存。
    此函数将在单独的线程中运行。
    """
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')  # 无头模式,浏览器不在UI中显示
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--disable-gpu') # 禁用GPU硬件加速,在某些环境下可能需要
    options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124124 Safari/537.36') # 设置User-Agent

    # 确保 ChromeDriver 路径正确,如果不在PATH中,请指定 executable_path
    driver = webdriver.Chrome(options=options) 

    is_in_stock = False
    try:
        driver.get(url)
        print(f"Navigated to {url}")

        # 等待尺码选择器元素加载。根据网站结构,这些通常是带有特定类名的
  • 或元素 # 这里我们等待所有包含 'swatchanchor' 类的链接元素出现 WebDriverWait(driver, 15).until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, "a.swatchanchor")) ) print("Size swatches loaded.") # 查找特定尺码(例如“40”)并且是可选择的元素 # 网站结构通常是:
  • ...
  • # 我们可以通过CSS选择器或XPath来定位 # CSS Selector: 查找父级为 'selectable' 的 'li' 元素,其子级 'a.swatchanchor' 的 title 属性包含 '40' # 或者更直接地查找所有 'swatchanchor',然后过滤 # 尝试查找所有尺码选项 size_elements = driver.find_elements(By.CSS_SELECTOR, "a.swatchanchor") for element in size_elements: title = element.get_attribute('title') parent_li_class = element.find_element(By.XPATH, "./..").get_attribute('class') # 获取父级
  • 的class if size_to_check in title: print(f"Found size element for {size_to_check} with title: '{title}' and parent class: '{parent_li_class}'") if 'selectable' in parent_li_class: is_in_stock = True break else: print(f"Size {size_to_check} found but is not selectable (class: {parent_li_class}).") if is_in_stock: print(f"尺码 {size_to_check} 有货。") else: print(f"尺码 {size_to_check} 缺货或未找到。") except TimeoutException: print(f"Timeout waiting for page elements on {url}. Page might not have loaded correctly or elements not found within time.") except NoSuchElementException: print(f"Specific elements (like parent li) not found for {size_to_check}.") except Exception as e: print(f"An unexpected error occurred during Selenium operation: {e}") finally: driver.quit() # 确保关闭浏览器实例 return is_in_stock async def check_stock(url, size): """ 异步包装器,在单独的线程中运行同步的Selenium库存检查。 """ loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _check_stock_selenium_sync, url, size)
  • 2.3 整合Discord通知与异步循环

    现在,我们将Selenium的库存检查功能集成到原有的异步主循环中,以便定期检查并发送Discord通知。

    product_data = [
        {'url': 'https://www.courir.com/fr/p/ugg-tasman-1499533.html', 'size': '40'}, # 目标尺码改为 '40'
    ]
    
    async def main():
        previous_stock_status = {}  # 存储每个产品的上一次库存状态
    
        while True:
            result_message = ""
    
            for product_info in product_data:
                url = product_info['url']
                size = product_info['size']
    
                print(f"\nChecking stock for {url} (size: {size})...")
                current_stock_status = await check_stock(url, size) # 调用异步的Selenium检查函数
    
                # 获取上一次的库存状态,如果不存在则默认为False(缺货)
                last_status = previous_stock_status.get(url, {}).get(size, False)
    
                # 检查:如果之前缺货(last_status为False)且现在有货(current_stock_status为True)
                if not last_status and current_stock_status:
                    message = f"? {url} - 尺码 {size} 现在有货啦!"
                    result_message += message + "\n"
                    print(message)
                elif last_status and not current_stock_status:
                    # 如果之前有货,现在缺货,也可以选择发送通知
                    message = f"⚠️ {url} - 尺码 {size} 已经售罄。"
                    # result_message += message + "\n" # 根据需求决定是否通知缺货
                    print(message)
                else:
                    print(f"尺码 {size} 状态未改变 ({'有货' if current_stock_status else '缺货'})。")
    
                # 更新当前产品的库存状态
                if url not in previous_stock_status:
                    previous_stock_status[url] = {}
                previous_stock_status[url][size] = current_stock_status
    
            # 如果有任何库存更新,发送Discord消息
            if result_message.strip(): # 确保消息不为空白
                print("\nSending Discord message...")
                await send_webhook_message(result_message)
                print("Discord message sent.")
            else:
                print("\nNo stock updates to send.")
    
            # 设置检查间隔
            check_interval_seconds = 600 # 10分钟
            print(f"Waiting {check_interval_seconds} seconds before next check...")
            await asyncio.sleep(check_interval_seconds)
    
    # 运行主函数
    if __name__ == "__main__":
        asyncio.run(main())

    3. 注意事项与最佳实践

    • WebDriver路径: 确保chromedriver或geckodriver可执行文件位于系统的PATH中,或者在webdriver.Chrome()或webdriver.Firefox()中通过executable_path参数明确指定其路径。
    • 无头模式: 在生产环境中,强烈建议使用options.add_argument('--headless')来运行Selenium,这样浏览器就不会在后台打开可见窗口,节省资源。
    • 等待策略: 动态加载内容需要时间。使用WebDriverWait和expected_conditions(如EC.presence_of_element_located或EC.visibility_of_element_located)是确保元素加载后再尝试查找的最佳实践,而不是使用硬编码的time.sleep()。
    • 元素定位: 选择器(CSS Selector或XPath)应尽可能健壮,避免使用过于依赖页面结构变化的定位方式。通过浏览器开发者工具仔细检查目标元素的属性(class, id, title, data-属性等)来构建可靠的选择器。
    • User-Agent: 设置User-Agent可以模拟真实浏览器,减少被网站识别为爬虫的风险。
    • 错误处理: 增加try-except块来捕获TimeoutException、NoSuchElementException以及其他可能的网络或Selenium相关的异常,提高程序的健壮性。
    • 爬虫道德与频率:
      • robots.txt: 在爬取任何网站之前,请检查其robots.txt文件(例如:https://www.courir.com/robots.txt),了解网站的爬取规则。
      • 服务条款: 遵守网站的服务条款。
      • 爬取频率: 设置合理的检查间隔(例如10分钟或更长),避免对网站服务器造成过大负担,防止IP被封禁。
    • 资源消耗: Selenium会启动一个完整的浏览器实例,相比requests+BeautifulSoup,资源消耗(CPU、内存)更大。在服务器上部署时,请考虑服务器性能。
    • 异步与同步: Selenium本身是同步的。为了将其集成到asyncio事件循环中,我们使用了loop.run_in_executor(None, sync_function, ...),这会将同步函数放到一个默认的线程池中执行,从而不阻塞主事件循环。

    4. 总结

    通过本教程,我们了解了在处理JavaScript动态加载内容的网站时,传统爬虫工具如BeautifulSoup的局限性。而Selenium作为强大的无头浏览器自动化工具,能够模拟真实用户行为,有效获取这些动态内容,从而实现精确的库存监控。结合Python的asyncio和Discord Webhook,我们可以构建一个高效、实时的库存变动通知系统。在实际应用中,务必注意遵守网站的爬取政策,并采取合理的爬取频率和错误处理机制,确保程序的稳定性和合规性。