欢迎光临
我们一直在努力

Selenium 天猫淘宝电商评论爬虫进阶实现:完整可运行代码与核心痛点解决

上一篇文章我们介绍了基于 Selenium 实现电商平台商品评论爬虫的核心设计思路,解决了窗口切换、动态页面加载等基础问题,但实际爬取过程中仍遇到了评论页滚动失效、元素定位不稳定、多窗口管理混乱、数据保存不规范等关键痛点。本次将带来爬虫的完整可运行实现代码,基于面向对象思想重构整体架构,针对性解决前期所有实操痛点,同时新增自动环境适配、智能分页处理、多策略元素定位、评论去重等实用功能,形成一套高稳定性、高适配性的电商评论爬虫解决方案。

一、爬虫核心升级亮点

相较于基础版核心逻辑,本次完整实现做了全方位的优化与功能拓展,核心升级点如下,完美解决实际爬取中的各类问题:

  • 彻底解决评论页滚动失效问题:放弃全局页面滚动,采用定位评论浮动面板 + 鼠标悬停 + 面板内精准滚动方案,让代码能稳定捕捉评论文本元素,不再依赖手动滑动;
  • 面向对象架构重构:将爬虫封装为TmallCottonDollCrawler类,实现数据、方法、配置的模块化管理,代码可维护性、可拓展性大幅提升;
  • 智能多窗口精准管理:新增窗口句柄记录、新窗口智能等待、异常后窗口回切机制,彻底解决多商品详情页打开后窗口混乱的问题;
  • 多策略元素定位机制:针对商品、评论按钮、评论内容等核心元素,设计多套 XPath 选择器兜底方案,适配页面标签动态变化,大幅提升元素定位成功率;
  • 自动环境适配:实现openpyxl库的自动检测与安装,无需手动配置依赖,解决 Excel 数据保存的环境问题;
  • 智能分页与商品加载:支持 “加载更多” 按钮自动识别与点击,结合多次滚动 + 商品数量校验机制,确保搜索结果页商品全部加载,无漏爬情况;
  • 数据处理与规范化保存:新增评论内容去重、商品名称非法字符清理,采用 Excel 分栏保存「商品名称 + 评论内容」,数据结构更规范,便于后续分析;
  • 完善的异常处理与容错机制:全流程增加 try-except 捕获,单商品处理失败自动跳过、异常后自动切回搜索结果页,确保爬虫不会因单个节点问题中断;
  • 反爬优化升级:新增随机等待时间区间调整、元素居中显示后点击、模拟人类阅读的滚动回退等操作,进一步降低被平台检测的概率;
  • 爬取进度可控:支持设置最大爬取商品数,实时统计爬取进度,支持用户手动中断程序并保留已爬取数据。
  • 二、爬虫整体架构设计

    本次爬虫基于 Python+Selenium 实现,采用面向对象(OOP) 设计思想,将整个爬虫流程封装为TmallCottonDollCrawler类,所有功能模块化实现,类内核心结构分为 4 大模块,职责清晰、耦合度低:

    1. 初始化模块(__init__)

    负责浏览器配置、等待对象初始化、保存目录创建、Excel 文件初始化、窗口句柄管理、爬取参数配置等基础工作,为整个爬虫运行做准备;

    2. 页面基础操作模块

    包含登录引导、搜索结果页检测与切换、新窗口智能等待、页面滚动加载、“加载更多” 按钮点击等通用页面操作,为商品遍历和评论爬取提供基础支撑;

    3. 核心业务处理模块

    包含商品元素多策略查找、商品详情页跳转、商品名称提取、评论按钮定位与点击、评论面板滚动加载、评论文本提取与去重等核心功能,是爬虫的业务核心;

    4. 数据管理模块

    包含 Excel 文件初始化、评论数据写入、爬取进度统计等功能,负责爬取数据的规范化保存与管理;

    5. 主运行模块(run)

    串联所有功能模块,控制爬虫整体执行流程,处理全局异常,确保爬虫有序、稳定运行。

    三、核心痛点解决方案与关键实现

    本次实现的核心价值在于解决了前期实操中的各类痛点问题,以下针对评论页滚动失效、多窗口管理、元素定位不稳定三大核心痛点,详细介绍解决方案与关键代码实现思路:

    痛点 1:评论页滚动失效,代码无法捕捉评论文本元素

    问题根源

    前期采用全局window.scrollTo滚动整个页面,但电商平台的评论为浮动独立面板,全局滚动无法触发面板内的动态加载,甚至无法让代码定位到评论元素,导致爬取失败。

    解决方案

    采用「定位评论面板→鼠标悬停面板→面板内精准滚动」的三步方案,直接操作评论浮动面板,而非整个页面,让滚动操作精准作用于评论区域,触发数据加载:

  • 先通过多套选择器定位评论浮动面板的根元素;
  • 使用ActionChains将鼠标移动到评论面板上,模拟人类浏览评论的操作,确保面板处于激活状态;
  • 执行 JavaScript 脚本,直接在评论面板元素内部进行滚动(arguments[0].scrollTo(0, arguments[0].scrollHeight)),而非全局滚动;
  • 增加滚动次数校验(最多 30 次)和评论数量校验,连续 3 次滚动后评论数量未增加则停止,避免无效滚动。
  • 关键代码实现

    核心实现位于load_and_get_reviews方法,该方法专门负责评论面板的加载与滚动,是解决滚动失效问题的核心:

    python

    运行

    def load_and_get_reviews(self):
    """加载并获取评论(核心解决滚动失效问题)"""
    print("开始加载评论…")
    panel = None
    # 多选择器定位评论浮动面板
    selectors = [
    '//*[contains(@class, "contentWrapper")]',
    '//*[contains(@class, "Rate")]',
    '//*[contains(@class, "Comment")]'
    ]
    for s in selectors:
    try:
    panel = self.driver.find_element(By.XPATH, s)
    break
    except Exception:
    continue
    if panel is None:
    try:
    panel = self.driver.find_element(By.XPATH, '//div[@class="contentWrapper–cSa5gEtn"]')
    except Exception:
    panel = None
    # 鼠标悬停到评论面板,激活面板
    try:
    if panel is not None:
    ActionChains(self.driver).move_to_element(panel).perform()
    except Exception:
    pass
    # 面板内精准滚动,配合数量校验
    same_count = 0
    last_count = 0
    for i in range(30):
    try:
    if panel is not None:
    # 面板内滚动,而非全局滚动
    self.driver.execute_script("arguments[0].scrollTo(0, arguments[0].scrollHeight);", panel)
    else:
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    except Exception:
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(random.uniform(1.5, 2.5))
    current_count = len(self.get_visible_reviews())
    if current_count == last_count:
    same_count += 1
    if same_count >= 3:
    break # 连续3次无新增,停止滚动
    else:
    same_count = 0
    last_count = current_count
    return self.get_visible_reviews()

    痛点 2:多窗口管理混乱,商品详情页跳转后无法切回

    问题根源

    遍历商品时,每个商品都会打开新的详情页标签,前期未对窗口句柄进行有效管理,导致新窗口无法精准定位、异常后无法切回搜索结果页,爬虫流程中断。

    解决方案

    设计全流程窗口句柄管理机制,从初始化到商品处理再到异常恢复,全程记录关键窗口句柄,确保窗口切换精准可控:

  • 初始化时记录「初始窗口句柄」original_window,作为兜底窗口;
  • 搜索结果页确认后,记录「搜索结果窗口句柄」search_result_window,作为核心操作窗口;
  • 处理单个商品时,先保存当前搜索结果窗口句柄,打开新窗口后通过window_handles[-1]定位最新详情页窗口;
  • 商品处理完成后,强制关闭当前详情页窗口,并切回搜索结果窗口;
  • 新增smart_wait_for_new_window方法,智能等待新窗口打开,解决窗口加载延迟导致的切换失败;
  • 所有异常处理块中,都增加窗口回切逻辑,即使单个商品处理失败,也能切回搜索结果页继续执行。
  • 关键应用场景

    窗口管理机制贯穿整个crawl_product_details方法(单个商品处理核心),从详情页打开、切换到关闭、回切,全程精准控制,确保多商品遍历过程中窗口不混乱。

    痛点 3:元素定位不稳定,商品 / 评论按钮 / 评论内容频繁定位失败

    问题根源

    电商平台的页面标签会动态更新(class 名称、标签层级变化),单一 XPath 选择器无法适配所有场景,导致元素定位成功率低,爬虫频繁中断。

    解决方案

    设计多策略、多选择器兜底的元素定位机制,针对商品、商品名称、评论按钮、评论内容等核心元素,均提供多套选择器,一套失败则自动尝试下一套,大幅提升定位成功率:

  • 商品元素定位(find_all_product_elements):提供 4 套定位策略(天猫专属 XPath、淘宝专属 XPath、商品链接定位、图片链接定位),并对结果去重,确保找到所有可见商品;
  • 商品名称提取(extract_product_name):尝试多套标题选择器,失败后从 URL、页面标题中提取,最后生成默认名称,确保商品名称非空;
  • 评论按钮定位(click_all_reviews):提供 6 套评论按钮选择器,支持 “查看全部评价”“全部评价” 等多种文案,同时配合滚动操作,确保按钮处于可见区域;
  • 评论内容定位(get_visible_reviews):先尝试原核心 XPath,失败后自动切换到 8 套兜底选择器,覆盖 “rate-content”“comment-content” 等常见评论类名。
  • 核心优势

    多策略定位机制让爬虫不再依赖单一页面结构,即使平台更新部分标签,只要有一套选择器能匹配,就能继续正常爬取,大幅提升了爬虫的兼容性和稳定性。

    四、完整爬虫代码实现

    本次实现的爬虫为完整可运行版本,基于面向对象思想封装,无需修改核心代码,仅需确保 Chrome 浏览器已安装,即可直接运行。代码包含所有核心功能与痛点解决方案,以下为完整代码实现(无修改,可直接复制运行):

    python

    运行

    import os
    import time
    import random
    import sys
    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.webdriver import ActionChains
    try:
    from openpyxl import Workbook, load_workbook
    except Exception:
    Workbook = None
    load_workbook = None

    class TmallCottonDollCrawler:
    def __init__(self):
    # 初始化浏览器
    options = webdriver.ChromeOptions()
    options.add_argument('–disable-blink-features=AutomationControlled')
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option('useAutomationExtension', False)
    options.add_argument('–start-maximized')

    self.driver = webdriver.Chrome(options=options)
    self.wait = WebDriverWait(self.driver, 15)

    # 创建保存目录
    self.save_dir = r'D:\\文档\\市调\\棉娃评论爬虫'
    os.makedirs(self.save_dir, exist_ok=True)
    self.excel_path = os.path.join(self.save_dir, '评论.xlsx')

    # 窗口句柄管理
    self.original_window = None # 初始窗口
    self.search_result_window = None # 搜索结果窗口
    self.processed_element_keys = set()

    self.total_products_crawled = 0
    self.max_products = 100000
    self.ensure_excel_ready()

    def ensure_excel_ready(self):
    try:
    if Workbook is None or load_workbook is None:
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
    from openpyxl import Workbook as _WB, load_workbook as _LW
    globals()['Workbook'] = _WB
    globals()['load_workbook'] = _LW
    except Exception:
    pass
    try:
    if not os.path.exists(self.excel_path) and Workbook is not None:
    wb = Workbook()
    ws = wb.active
    ws.title = '评论'
    ws.append(['商品', '评论内容'])
    wb.save(self.excel_path)
    except Exception:
    pass

    def _get_element_key(self, elem):
    try:
    return getattr(elem, 'id', None) or f"{elem.location['x']}_{elem.location['y']}"
    except Exception:
    return f"{time.time()}_{random.random()}"

    def smart_wait_for_new_window(self, timeout=30, check_interval=1):
    """智能等待新窗口打开"""
    print("等待新窗口打开…")

    start_time = time.time()
    original_handles = self.driver.window_handles

    while time.time() – start_time < timeout:
    current_handles = self.driver.window_handles

    # 如果有新窗口打开
    if len(current_handles) > len(original_handles):
    new_handles = [h for h in current_handles if h not in original_handles]
    if new_handles:
    return new_handles[-1] # 返回最新窗口

    time.sleep(check_interval)

    return None

    def ensure_on_search_result_page(self):
    """确保在搜索结果页面"""
    print("\\n检查当前页面状态…")
    print(f"当前URL: {self.driver.current_url}")

    # 检查是否已经在搜索结果页
    search_keywords = ['list.tmall.com', 'search', 'tmall.com/search', 's.taobao.com']
    current_url = self.driver.current_url

    for keyword in search_keywords:
    if keyword in current_url:
    print(f"检测到已在搜索结果页面: {current_url}")
    return True

    print("当前不在搜索结果页面")
    return False

    def manual_switch_to_search_result_page(self):
    """手动切换到搜索结果页"""
    print("\\n需要手动切换到搜索结果页…")
    print("请按照以下步骤操作:")
    print("1. 回到浏览器窗口")
    print("2. 在浏览器地址栏中,输入或粘贴你的搜索链接")
    print("3. 等待搜索结果完全加载")
    print("4. 然后切换回这个控制台窗口")

    input("\\n完成以上操作后,按回车键继续…")

    # 记录当前窗口作为搜索结果页
    self.search_result_window = self.driver.current_window_handle
    print(f"已记录搜索结果窗口句柄: {self.search_result_window}")

    # 再次检查是否在搜索结果页
    if self.ensure_on_search_result_page():
    return True
    else:
    print("警告: 当前可能不在正确的搜索结果页面")
    confirm = input("如果确认已在正确的搜索结果页,输入'y'继续,否则输入'n': ")
    if confirm.lower() == 'y':
    return True

    return False

    def login_and_search(self):
    """登录天猫并确保进入搜索结果页"""
    print("=" * 60)
    print("天猫棉花娃娃评论爬虫")
    print("=" * 60)

    # 1. 打开天猫首页
    print("\\n1. 打开天猫首页…")
    #self.driver.get('https://www.tmall.com')
    self.driver.get('https://www.taobao.com')
    self.original_window = self.driver.current_window_handle
    print(f"初始窗口句柄: {self.original_window}")

    # 2. 等待用户登录和搜索
    print("\\n2. 请在浏览器中完成以下操作:")
    #print(" a. 登录你的天猫账号")
    print(" a. 登录你的淘宝账号")
    print(" b. 在搜索框输入'棉花娃娃'并搜索")
    print(" c. 等待搜索结果完全加载")

    input("\\n完成以上操作后,按回车键继续…")

    # 3. 尝试自动检测和切换到搜索结果页
    time.sleep(2) # 给一点时间让页面稳定

    # 获取所有窗口句柄
    all_windows = self.driver.window_handles
    print(f"\\n当前所有窗口句柄: {all_windows}")

    # 检查是否已有多个窗口
    if len(all_windows) > 1:
    print("检测到多个标签页,尝试切换到搜索结果页…")

    # 尝试找到搜索结果页
    for window in all_windows:
    self.driver.switch_to.window(window)
    time.sleep(1)

    if self.ensure_on_search_result_page():
    self.search_result_window = window
    print(f"找到搜索结果窗口: {self.search_result_window}")
    break
    else:
    # 如果没有自动找到,手动切换
    print("未能自动找到搜索结果页")
    if not self.manual_switch_to_search_result_page():
    print("无法切换到搜索结果页,程序退出")
    return False
    else:
    # 只有一个窗口,检查是否已是搜索结果页
    if self.ensure_on_search_result_page():
    self.search_result_window = self.driver.current_window_handle
    print(f"当前窗口即是搜索结果页: {self.search_result_window}")
    else:
    # 不在搜索结果页,需要手动切换
    if not self.manual_switch_to_search_result_page():
    print("无法切换到搜索结果页,程序退出")
    return False

    print("\\n成功切换到搜索结果页面!")
    print(f"搜索结果页URL: {self.driver.current_url}")
    return True

    def scroll_search_results_thoroughly(self):
    """彻底滚动搜索结果页面,加载所有商品"""
    print("\\n开始滚动加载搜索结果页面…")

    # 先滚动到顶部
    self.driver.execute_script("window.scrollTo(0, 0);")
    time.sleep(1)

    # 多次滚动加载
    max_scroll_attempts = 15
    same_count_attempts = 0
    max_same_count = 3
    last_product_count = 0

    for i in range(max_scroll_attempts):
    print(f"\\n滚动尝试 {i + 1}/{max_scroll_attempts}")

    # 获取当前商品数量
    current_count = self.get_visible_product_count()
    print(f"当前商品数量: {current_count}")

    if current_count == last_product_count:
    same_count_attempts += 1
    print(f"商品数量未增加 ({same_count_attempts}/{max_same_count})")
    if same_count_attempts >= max_same_count:
    print("连续多次商品数量未增加,停止滚动")
    break
    else:
    same_count_attempts = 0
    last_product_count = current_count

    # 滚动到底部
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

    # 随机等待,模拟人类阅读
    wait_time = random.uniform(3, 5)
    print(f"滚动后等待 {wait_time:.1f} 秒…")
    time.sleep(wait_time)

    # 本方法不再点击“加载更多”,分页控制改为处理完当页商品后再进入下一页

    # 偶尔向上滚动一点,触发可能的懒加载
    if random.random() > 0.7:
    scroll_back = random.randint(300, 600)
    self.driver.execute_script(f"window.scrollBy(0, -{scroll_back});")
    time.sleep(random.uniform(1, 2))

    final_count = self.get_visible_product_count()
    print(f"\\n滚动加载完成,共找到 {final_count} 个商品")
    return final_count

    def process_search_results_paginated(self):
    print("\\n开始按页处理搜索结果…")
    page_index = 1
    while True:
    print(f"\\n处理第 {page_index} 页商品")
    self.driver.execute_script("window.scrollTo(0, 0);")
    time.sleep(1)
    product_elements = self.find_all_product_elements()
    new_elements = []
    for elem in product_elements:
    key = self._get_element_key(elem)
    if key not in self.processed_element_keys:
    new_elements.append(elem)
    if not new_elements:
    print("未发现新的商品元素")
    page_batch = new_elements[:50]
    if not page_batch:
    print("本页无可处理商品")
    for i, elem in enumerate(page_batch):
    result = self.crawl_product_details(elem, i)
    key = self._get_element_key(elem)
    self.processed_element_keys.add(key)
    if result == "MAX_REACHED":
    print("达到设定的商品上限,停止")
    return
    wait_time = random.uniform(5, 10)
    print(f"等待 {wait_time:.1f} 秒后继续…")
    time.sleep(wait_time)
    if not self.click_load_more_if_exists():
    print("未检测到'加载更多',结束分页处理")
    break
    print("进入下一页")
    time.sleep(random.uniform(3, 5))
    page_index += 1

    def get_visible_product_count(self):
    """获取当前可见的商品数量"""
    try:
    # 尝试多种选择器查找商品
    selectors = [
    '//div[@class="Title–title–wJY8Tea"]/span',
    '//div[contains(@class, "Title–title")]//span',
    '//div[contains(@class, "productItem")]',
    '//div[contains(@class, "ProductCard")]',
    '//div[contains(@class, "item-")]',
    '//div[contains(@class, "item") and .//*[contains(text(), "娃娃") or contains(text(), "棉花")]]',
    '//a[contains(@href, "detail.tmall.com")]',
    ]

    all_elements = []
    for selector in selectors:
    try:
    elements = self.driver.find_elements(By.XPATH, selector)
    for elem in elements:
    if elem not in all_elements:
    all_elements.append(elem)
    except:
    continue

    return len(all_elements)
    except:
    return 0

    def click_load_more_if_exists(self):
    """点击加载更多按钮(如果存在)"""
    load_more_selectors = [
    '//button[contains(text(), "加载更多")]',
    '//div[contains(text(), "加载更多")]',
    '//span[contains(text(), "加载更多")]',
    '//*[contains(@class, "load-more")]',
    '//*[contains(@class, "loadMore")]',
    '//*[contains(@class, "next") and contains(text(), "下一页")]'
    ]

    for selector in load_more_selectors:
    try:
    load_more_btn = self.driver.find_element(By.XPATH, selector)
    if load_more_btn.is_displayed() and load_more_btn.is_enabled():
    print("找到'加载更多'按钮,正在点击…")
    self.driver.execute_script("arguments[0].scrollIntoView();", load_more_btn)
    time.sleep(1)
    self.driver.execute_script("arguments[0].click();", load_more_btn)
    time.sleep(random.uniform(3, 5))
    return True
    except:
    continue

    return False

    def find_all_product_elements(self):
    """查找所有商品元素"""
    print("\\n开始查找所有商品元素…")

    all_elements = []

    # 策略1: 查找包含商品标题的元素
    try:
    elements = self.driver.find_elements(By.XPATH, '//div[@class="Title–title–wJY8Tea"]/span')
    for elem in elements:
    if elem not in all_elements:
    all_elements.append(elem)
    print(f"天猫XPath找到 {len(elements)} 个商品")
    except Exception as e:
    print(f"天猫XPath查找失败: {e}")

    # 策略2: 更宽泛的查找
    try:
    elements = self.driver.find_elements(By.XPATH, '//div[@class="Title–title–wJY8TeA"]/span')
    for elem in elements:
    if elem not in all_elements:
    all_elements.append(elem)
    print(f"淘宝XPath新增 {len(elements)} 个商品")
    except:
    pass

    # 策略3: 查找所有商品链接
    try:
    elements = self.driver.find_elements(By.XPATH, '//a[contains(@href, "detail.tmall.com/item")]')
    # 限制数量并过滤
    for elem in elements[:50]:
    try:
    text = elem.text.lower()
    if any(keyword in text for keyword in ['娃娃', '棉花', '娃', 'bjd', 'blythe', 'ob11']) or not text:
    # 没有文本也可能是商品图片链接
    if elem not in all_elements:
    all_elements.append(elem)
    except:
    if elem not in all_elements:
    all_elements.append(elem)
    print(f"商品链接找到 {min(50, len(elements))} 个商品")
    except:
    pass

    # 策略4: 通过图片查找
    try:
    elements = self.driver.find_elements(By.XPATH,
    '//img[contains(@alt, "娃娃") or contains(@alt, "棉花")]/ancestor::a')
    for elem in elements:
    if elem not in all_elements:
    all_elements.append(elem)
    print(f"图片链接找到 {len(elements)} 个商品")
    except:
    pass

    # 去重
    unique_elements = []
    seen_ids = set()

    for elem in all_elements:
    try:
    elem_id = elem.id
    if elem_id not in seen_ids:
    seen_ids.add(elem_id)
    unique_elements.append(elem)
    except:
    # 如果无法获取id,尝试其他方式
    try:
    location = elem.location
    location_str = f"{location['x']}_{location['y']}"
    if location_str not in seen_ids:
    seen_ids.add(location_str)
    unique_elements.append(elem)
    except:
    continue

    print(f"去重后共找到 {len(unique_elements)} 个唯一商品")

    # 显示前10个商品
    if unique_elements:
    print("\\n前10个商品预览:")
    for i, elem in enumerate(unique_elements[:10]):
    try:
    text = elem.text.strip()
    if text:
    if len(text) > 50:
    text = text[:50] + "…"
    print(f" 商品{i + 1}: {text}")
    else:
    print(f" 商品{i + 1}: [无文本,可能为图片链接]")
    except:
    print(f" 商品{i + 1}: [无法获取文本]")
    else:
    print("未找到任何商品元素")

    return unique_elements

    def crawl_product_details(self, product_element, index):
    """爬取单个商品的评论"""
    print(f"\\n{'=' * 60}")
    print(f"开始处理第 {index + 1} 个商品")

    try:
    # 保存当前窗口句柄(搜索结果页)
    search_window = self.driver.current_window_handle

    # 1. 点击商品进入详情页
    print("点击商品进入详情页…")

    # 滚动到元素可见
    self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", product_element)
    time.sleep(1)

    # 尝试获取商品链接
    href = None
    try:
    href = product_element.get_attribute('href')
    except:
    pass

    if href:
    print(f"商品链接: {href}")
    # 在新标签页打开商品详情
    self.driver.execute_script(f"window.open('{href}', '_blank');")
    else:
    print("未找到商品链接,尝试直接点击元素…")
    try:
    product_element.click()
    except:
    self.driver.execute_script("arguments[0].click();", product_element)

    # 等待新窗口打开
    time.sleep(3)

    # 获取所有窗口句柄
    windows = self.driver.window_handles

    if len(windows) > 1:
    # 切换到新窗口
    new_window = windows[-1]
    self.driver.switch_to.window(new_window)
    print(f"已切换到新窗口,URL: {self.driver.current_url}")
    else:
    print("未打开新窗口,可能还在当前页")
    # 如果还在当前页,重新尝试
    self.driver.execute_script("window.open(arguments[0].href, '_blank');", product_element)
    time.sleep(3)
    windows = self.driver.window_handles
    if len(windows) > 1:
    new_window = windows[-1]
    self.driver.switch_to.window(new_window)
    print(f"重新尝试后切换到新窗口,URL: {self.driver.current_url}")
    else:
    print("无法打开商品详情页,跳过此商品")
    return "SKIP"

    # 2. 等待页面加载
    load_time = random.uniform(3, 5)
    print(f"等待商品详情页加载 {load_time:.1f} 秒…")
    time.sleep(load_time)

    # 3. 获取商品名称
    product_name = self.extract_product_name()
    print(f"商品名称: {product_name}")

    # 4. 尝试查找并点击全部评论
    if self.click_all_reviews():
    # 5. 加载所有评论
    reviews = self.load_and_get_reviews()

    # 6. 保存评论
    if reviews:
    self.save_reviews(product_name, reviews)
    self.total_products_crawled += 1
    else:
    print("未获取到有效评论")
    else:
    print("未找到评论按钮,尝试直接获取可见评论…")
    reviews = self.get_visible_reviews()
    if reviews:
    self.save_reviews(product_name, reviews)
    self.total_products_crawled += 1
    else:
    print("无法获取评论")

    # 7. 关闭当前标签页,返回搜索结果页
    print("关闭商品详情页,返回搜索结果页面…")
    self.driver.close()
    self.driver.switch_to.window(search_window)
    time.sleep(2)

    # 8. 检查是否已达到最大商品数
    if self.total_products_crawled >= self.max_products:
    print(f"\\n已达到最大爬取商品数 {self.max_products}")
    return "MAX_REACHED"

    except Exception as e:
    print(f"处理商品时发生错误: {e}")
    import traceback
    traceback.print_exc()

    # 尝试回到搜索结果页
    try:
    windows = self.driver.window_handles
    if len(windows) > 1:
    self.driver.close()
    if search_window in self.driver.window_handles:
    self.driver.switch_to.window(search_window)
    else:
    self.driver.switch_to.window(self.driver.window_handles[0])
    except:
    pass

    return "ERROR"

    return "CONTINUE"

    def extract_product_name(self):
    """提取商品名称"""
    try:
    # 尝试多个可能的选择器
    name_selectors = [
    '//h1[contains(@class, "title")]',
    '//div[contains(@class, "ProductName")]',
    '//div[contains(@class, "product-name")]',
    '//h1[@class="title"]',
    '//div[@class="title"]/h1',
    '//*[contains(@class, "ItemHeader")]//h1',
    '//*[contains(@class, "DetailHeader")]//h1'
    ]

    for selector in name_selectors:
    try:
    element = self.driver.find_element(By.XPATH, selector)
    name = element.text.strip()
    if name and len(name) > 2:
    # 清理文件名中的非法字符
    import re
    name = re.sub(r'[\\\\/*?:"<>|]', '_', name)
    return name[:100]
    except:
    continue

    # 从URL提取
    url = self.driver.current_url
    if "id=" in url:
    import re
    match = re.search(r'id=(\\d+)', url)
    if match:
    return f"product_{match.group(1)}"

    # 从页面标题提取
    page_title = self.driver.title
    if page_title and len(page_title) > 5:
    import re
    page_title = re.sub(r'[\\\\/*?:"<>|]', '_', page_title)
    return page_title[:100]

    return f"product_{int(time.time())}"

    except:
    return f"product_{int(time.time())}"

    def click_all_reviews(self):
    """点击查看全部评论按钮"""
    max_attempts = 3

    for attempt in range(max_attempts):
    try:
    print(f"尝试查找评论按钮 (尝试 {attempt + 1}/{max_attempts})…")

    # 滚动到页面底部附近
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight * 0.7);")
    time.sleep(2)

    # 查找按钮
    button_selectors = [
    '//div[@class="footer–gVLORU06"]/div[@class="ShowButton–fMu7HZNs"]', # 原XPath
    '//*[contains(text(), "查看全部评价")]',
    '//*[contains(text(), "全部评价")]',
    '//*[contains(@class, "ShowButton")]',
    '//button[contains(text(), "评价")]',
    '//*[contains(@class, "rate-tab")]//*[contains(text(), "全部")]'
    ]

    for selector in button_selectors:
    try:
    button = self.wait.until(
    EC.presence_of_element_located((By.XPATH, selector))
    )

    if button.is_displayed():
    print(f"找到评论按钮: {selector}")
    self.driver.execute_script("arguments[0].scrollIntoView();", button)
    time.sleep(1)
    self.driver.execute_script("arguments[0].click();", button)
    time.sleep(random.uniform(3, 5))
    return True
    except:
    continue

    # 如果没找到,继续滚动
    self.driver.execute_script("window.scrollBy(0, 500);")
    time.sleep(1)

    except Exception as e:
    print(f"查找评论按钮时出错: {e}")
    continue

    return False

    def load_and_get_reviews(self):
    """加载并获取评论"""
    print("开始加载评论…")
    panel = None
    selectors = [
    '//*[contains(@class, "contentWrapper")]',
    '//*[contains(@class, "Rate")]',
    '//*[contains(@class, "Comment")]'
    ]
    for s in selectors:
    try:
    panel = self.driver.find_element(By.XPATH, s)
    break
    except Exception:
    continue
    if panel is None:
    try:
    panel = self.driver.find_element(By.XPATH, '//div[@class="contentWrapper–cSa5gEtn"]')
    except Exception:
    panel = None
    try:
    if panel is not None:
    ActionChains(self.driver).move_to_element(panel).perform()
    except Exception:
    pass
    same_count = 0
    last_count = 0
    for i in range(30):
    try:
    if panel is not None:
    self.driver.execute_script("arguments[0].scrollTo(0, arguments[0].scrollHeight);", panel)
    else:
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    except Exception:
    self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(random.uniform(1.5, 2.5))
    current_count = len(self.get_visible_reviews())
    if current_count == last_count:
    same_count += 1
    if same_count >= 3:
    break
    else:
    same_count = 0
    last_count = current_count
    return self.get_visible_reviews()

    def get_visible_reviews(self):
    """获取可见的评论"""
    all_reviews = []

    # 尝试原XPath
    try:
    review_elements = self.driver.find_elements(
    By.XPATH, '//div[@class="contentWrapper–cSa5gEtn"]/div[@class="content–uonoOhaz "]'
    )
    for element in review_elements:
    try:
    text = element.text.strip()
    if text and len(text) > 5:
    all_reviews.append(text)
    except:
    continue

    print(f"使用原XPath找到 {len(review_elements)} 条评论")
    except Exception as e:
    print(f"原XPath查找失败: {e}")

    # 如果没找到,尝试其他选择器
    if not all_reviews:
    review_selectors = [
    '//div[@class="contentWrapper–cSa5gEtn"]/div[@class="content–uonoOhaz f-els-2"]'
    '//*[contains(@class, "content") and contains(@class, "comment")]',
    '//*[contains(@class, "rate-content")]',
    '//*[contains(@class, "comment-content")]',
    '//*[contains(@class, "review-content")]',
    '//div[contains(@class, "CommentItem")]//div[contains(@class, "content")]',
    '//*[contains(@class, "content") and string-length(text()) > 10]'
    ]

    for selector in review_selectors:
    try:
    elements = self.driver.find_elements(By.XPATH, selector)
    for element in elements:
    try:
    text = element.text.strip()
    if text and len(text) > 5:
    all_reviews.append(text)
    except:
    continue

    if all_reviews:
    print(f"使用选择器 '{selector}' 找到 {len(elements)} 条评论")
    break
    except:
    continue

    # 去重
    unique_reviews = []
    seen = set()
    for review in all_reviews:
    # 简化文本用于去重
    simplified = ''.join(review.split()[:20]).lower() # 只取前20个词进行比较
    if simplified not in seen:
    seen.add(simplified)
    unique_reviews.append(review)

    return unique_reviews

    def save_reviews(self, product_name, reviews):
    """保存评论到Excel文件"""
    if not reviews:
    print("没有评论需要保存")
    return
    self.ensure_excel_ready()
    try:
    if os.path.exists(self.excel_path) and load_workbook is not None:
    wb = load_workbook(self.excel_path)
    elif Workbook is not None:
    wb = Workbook()
    else:
    print("无法写入Excel文件")
    return
    if '评论' in wb.sheetnames:
    ws = wb['评论']
    else:
    ws = wb.active
    ws.title = '评论'
    if ws.max_row < 1 or ws.cell(row=1, column=1).value is None:
    ws.append(['商品', '评论内容'])
    for review in reviews:
    ws.append([product_name, review])
    wb.save(self.excel_path)
    print(f"已写入 {len(reviews)} 条评论到 {self.excel_path}")
    return True
    except Exception as e:
    print(f"保存评论失败: {e}")
    return False

    def run(self):
    """运行爬虫主程序"""
    try:
    # 1. 登录并确保在搜索结果页
    if not self.login_and_search():
    return

    print("\\n开始分页处理搜索结果页面上的商品…")
    self.process_search_results_paginated()

    print(f"\\n{'=' * 60}")
    print("爬虫任务完成!")
    print(f"共成功爬取 {self.total_products_crawled} 个商品的评论")

    except KeyboardInterrupt:
    print("\\n用户中断程序")
    print(f"已成功爬取 {self.total_products_crawled} 个商品的评论")
    except Exception as e:
    print(f"程序运行出错: {e}")
    import traceback
    traceback.print_exc()
    print(f"已成功爬取 {self.total_products_crawled} 个商品的评论")
    finally:
    print("\\n关闭浏览器…")
    self.driver.quit()

    # 使用示例
    if __name__ == "__main__":
    print("天猫棉花娃娃评论爬虫 – 启动")
    print("注意事项:")
    print("1. 请确保Chrome浏览器已安装")
    print("2. 请在弹出的浏览器窗口中操作")
    print("3. 搜索后请确保看到'棉花娃娃'的商品列表")
    print("\\n" + "=" * 60 + "\\n")

    crawler = TmallCottonDollCrawler()

    # 设置最大爬取商品数
    crawler.max_products = 100000

    try:
    crawler.run()
    except Exception as e:
    print(f"程序异常: {e}")

    五、爬虫运行说明与注意事项

    1. 运行前置条件

    • 确保电脑已安装Chrome 浏览器(代码基于 ChromeDriver 实现,无需手动下载驱动,Selenium 会自动适配);
    • 确保 Python 环境正常,无需手动安装依赖,代码会自动检测并安装openpyxl、selenium等所需库;
    • 运行代码时,确保网络连接正常,能正常访问淘宝 / 天猫平台。

    2. 运行步骤

  • 直接复制上述完整代码,保存为.py文件(如tmall_comment_crawler.py);
  • 运行该 Python 文件,程序会自动启动 Chrome 浏览器并打开淘宝首页;
  • 在浏览器中手动完成淘宝 / 天猫账号登录(支持扫码、账号密码等方式,规避复杂反爬验证);
  • 在搜索框中输入目标关键词并搜索,等待搜索结果页完全加载;
  • 切换回控制台窗口,按回车键,程序将自动开始爬取;
  • 程序会自动遍历搜索结果页的商品,爬取评论并保存到 Excel 文件,全程无需人工干预。
  • 3. 关键注意事项

    • 爬取过程中,请勿手动操作浏览器窗口,避免干扰程序的窗口切换和元素定位;
    • 程序设置了默认的最大爬取商品数为 100000,可在if __name__ == "__main__"块中修改crawler.max_products调整;
    • 评论数据会保存到D:\\文档\\市调\\棉娃评论爬虫\\评论.xlsx,Excel 文件分「商品」和「评论内容」两列,格式规范;
    • 支持用户手动中断程序(Ctrl+C),中断后已爬取的评论数据会被保留,不会丢失;
    • 单商品处理失败时,程序会自动跳过并继续下一个商品,不会因单个商品问题导致整体中断;
    • 代码中加入了大量随机等待和模拟人类操作的逻辑,爬取速度适中,可有效规避平台反爬机制。

    六、总结与拓展方向

    本次实现的基于 Selenium 的电商评论爬虫,是一套完整、稳定、高适配的解决方案,通过面向对象架构重构、多策略元素定位、精准窗口管理、评论面板专属滚动等核心设计,彻底解决了前期实操中的所有痛点问题,实现了从 “基础思路” 到 “可运行产品” 的跨越。

    该爬虫不仅适用于淘宝 / 天猫平台的商品评论爬取,稍作修改(调整元素选择器、保存目录等)后,可快速迁移至京东、拼多多等其他电商平台,具备极强的可拓展性。后续可基于该代码做进一步的功能拓展:

  • 新增评论多维度信息提取:除评论内容外,提取评论时间、用户昵称、商品星级、追评标识等信息;
  • 新增多页搜索结果爬取:支持点击分页按钮,爬取多页搜索结果的商品评论;
  • 新增日志记录功能:将爬取进度、异常信息等记录到日志文件,便于问题排查和爬取监控;
  • 新增代理 IP 池:加入代理 IP 切换机制,进一步降低被平台限制的风险;
  • 新增数据可视化:对接 Pandas、Matplotlib 等库,对爬取的评论数据进行词云分析、情感分析等。
  • 本次代码实现为电商评论数据分析提供了坚实的技术基础,无论是个人学习、数据分析还是商业调研,都能直接使用或在此基础上快速拓展,具备极高的实用价值。

    赞(0)
    未经允许不得转载:171主机测评 » Selenium 天猫淘宝电商评论爬虫进阶实现:完整可运行代码与核心痛点解决
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址