前言

经过前面33天的学习,我们已经掌握了Selenium的基础操作、元素定位、等待机制,以及进阶的JavaScript执行、Cookie管理和浏览器配置。今天,我们将迎来第三阶段的终极实战项目——开发一个电商商品信息抓取机器人。

本项目将综合运用之前所学的所有技能,实现一个完整的自动化流程:

  • 模拟用户搜索商品
  • 处理动态加载内容
  • 自动翻页抓取多页数据
  • 提取商品名称、价格、评论数
  • 将数据保存为结构化CSV文件

让我们开始这段代码之旅吧!🚀


一、项目概述与技术选型

1.1 项目目标

编写一个Python脚本,实现以下功能:

  1. 打开电商网站(以京东为例,页面结构相对稳定)
  2. 自动搜索指定关键词(如“Python书籍”)
  3. 滚动页面加载所有商品
  4. 提取当前页所有商品的名称、价格、评论数
  5. 自动点击“下一页”按钮,重复步骤3-5
  6. 将所有数据保存到CSV文件中

1.2 为什么选择Selenium?

电商网站普遍采用动态加载技术,传统的requests+BeautifulSoup方式无法获取JavaScript渲染后的内容。Selenium通过操作真实浏览器,能够完美应对这类场景。

对比项 Selenium Requests+BS4
动态内容 ✅ 完全支持 ❌ 无法获取
交互操作 ✅ 点击、滚动、输入 ❌ 仅限静态请求
反爬绕过 模拟真人操作,效果较好 容易被识别封IP
执行速度 较慢 非常快

对于本项目,速度和复杂度的平衡点在于:我们牺牲部分速度,换取稳定性和数据完整性

1.3 技术栈与环境搭建

# 所需库
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
import time
import pandas as pd
import csv
import os

安装命令

pip install selenium pandas

同时确保下载与本地Chrome版本匹配的ChromeDriver,并将其路径加入系统PATH。


二、代码实现:分步详解

2.1 初始化浏览器配置

为了提升效率并减少资源消耗,我们将配置无头模式禁用图片加载

class EcommerceSpider:
    def __init__(self, headless=True):
        """
        初始化爬虫,配置浏览器选项
        :param headless: 是否使用无头模式
        """
        self.options = Options()
        
        if headless:
            self.options.add_argument('--headless=new')  # 新版无头模式
            self.options.add_argument('--disable-gpu')
        
        # 禁用图片加载,提升速度
        prefs = {"profile.managed_default_content_settings.images": 2}
        self.options.add_experimental_option("prefs", prefs)
        
        # 隐藏自动化提示
        self.options.add_experimental_option('excludeSwitches', ['enable-automation'])
        self.options.add_argument('--disable-blink-features=AutomationControlled')
        
        # 设置窗口大小(对响应式布局很重要)
        self.options.add_argument('--window-size=1920,1080')
        
        self.driver = webdriver.Chrome(options=self.options)
        self.wait = WebDriverWait(self.driver, 10)
        self.data = []  # 存储所有商品数据

代码解读

  • --headless=new:新版Chrome无头模式,更稳定
  • 禁用图片加载:可提升30%以上的加载速度
  • 隐藏自动化特征:降低被反爬虫识别的概率

2.2 实现商品搜索

打开京东首页,定位搜索框,输入关键词并提交。

    def search_product(self, keyword):
        """
        打开京东并搜索商品
        :param keyword: 搜索关键词
        """
        print(f"正在搜索关键词: {keyword}")
        self.driver.get('https://www.jd.com/')
        
        try:
            # 等待搜索框出现
            search_input = self.wait.until(
                EC.presence_of_element_located((By.ID, 'key'))
            )
            search_input.clear()
            search_input.send_keys(keyword)
            
            # 点击搜索按钮
            search_button = self.driver.find_element(By.CSS_SELECTOR, 'button.button')
            search_button.click()
            
            # 等待搜索结果加载
            time.sleep(3)
            print("搜索完成,页面标题:", self.driver.title)
            
        except Exception as e:
            print(f"搜索失败: {e}")
            raise

定位技巧

  • 京东搜索框ID为key,搜索按钮class包含button
  • 使用显式等待确保元素加载完成

2.3 滚动页面加载全部商品

京东的商品列表采用懒加载模式,只有滚动到页面底部才会加载更多商品。我们需要通过JavaScript模拟滚动操作。

    def scroll_to_load_all(self):
        """
        滚动页面至底部,触发懒加载
        """
        print("正在滚动页面加载商品...")
        last_height = self.driver.execute_script("return document.body.scrollHeight")
        
        while True:
            # 滚动到底部
            self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(2)  # 等待新内容加载
            
            # 再次获取滚动高度,判断是否到底
            new_height = self.driver.execute_script("return document.body.scrollHeight")
            if new_height == last_height:
                break  # 没有新内容加载,退出循环
            last_height = new_height
        
        # 最后再滚动一次,确保所有图片占位符都加载
        self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(1)
        print("页面滚动完成")

原理说明

  • 通过document.body.scrollHeight获取页面总高度
  • 循环滚动到底部,直到高度不再变化,表示所有内容加载完毕

2.4 提取当前页商品信息

京东商品列表页的结构如下:

  • 每个商品被包含在//*[@id="J_goodsList"]/ul/li
  • 商品名称:.//div[@class="p-name"]/a/em
  • 商品价格:.//div[@class="p-price"]
  • 评论数量:.//div[@class="p-commit"]/strong
    def extract_page_data(self):
        """
        提取当前页所有商品信息
        :return: 当前页数据列表
        """
        # 定位所有商品项
        li_list = self.driver.find_elements(By.XPATH, '//*[@id="J_goodsList"]/ul/li')
        print(f"本页找到 {len(li_list)} 个商品")
        
        page_data = []
        for li in li_list:
            try:
                # 商品名称
                name_elem = li.find_element(By.XPATH, './/div[@class="p-name"]/a/em')
                name = name_elem.text.strip()
                
                # 商品价格
                price_elem = li.find_element(By.XPATH, './/div[@class="p-price"]')
                price = price_elem.text.strip()
                
                # 评论数量
                commit_elem = li.find_element(By.XPATH, './/div[@class="p-commit"]/strong')
                commit = commit_elem.text.strip()
                
                # 组装数据
                item = {
                    'name': name,
                    'price': price,
                    'commit': commit,
                    'crawl_time': time.strftime('%Y-%m-%d %H:%M:%S')
                }
                page_data.append(item)
                
            except Exception as e:
                # 单个商品提取失败,跳过继续
                print(f"提取商品信息时出错: {e}")
                continue
        
        return page_data

注意事项

  • 使用相对XPath(以.开头)在当前商品范围内查找,避免跨商品混淆
  • 异常处理确保某个商品解析失败不影响整体流程

2.5 自动翻页控制

翻页的关键在于判断是否到达最后一页。京东的“下一页”按钮在最后一页时,class会变为pn-next disabled

    def has_next_page(self):
        """
        判断是否存在下一页
        :return: True/False
        """
        page_source = self.driver.page_source
        # 查找禁用状态的下一页按钮
        if 'pn-next disabled' in page_source:
            return False
        
        try:
            next_btn = self.driver.find_element(By.CLASS_NAME, 'pn-next')
            return next_btn.is_enabled() and next_btn.is_displayed()
        except:
            return False

    def go_to_next_page(self):
        """
        点击下一页按钮
        """
        try:
            next_btn = self.driver.find_element(By.CLASS_NAME, 'pn-next')
            # 滚动到按钮位置,确保可点击
            self.driver.execute_script("arguments[0].scrollIntoView();", next_btn)
            time.sleep(1)
            next_btn.click()
            time.sleep(3)  # 等待新页面加载
            return True
        except Exception as e:
            print(f"翻页失败: {e}")
            return False

翻页逻辑

  1. 检查页面源码是否包含pn-next disabled,包含则说明是最后一页
  2. 否则找到下一页按钮,滚动到可视区域后点击
  3. 等待新页面加载

2.6 数据存储为CSV

使用pandas将数据导出为CSV文件,确保中文不乱码。

    def save_to_csv(self, filename='products.csv'):
        """
        将抓取的数据保存为CSV文件
        :param filename: 文件名
        """
        if not self.data:
            print("没有数据可保存")
            return
        
        df = pd.DataFrame(self.data)
        
        # 指定编码为utf-8-sig,防止Excel打开乱码
        df.to_csv(filename, index=False, encoding='utf-8-sig')
        print(f"数据已保存至 {filename},共 {len(self.data)} 条记录")

    def run(self, keyword, max_pages=5):
        """
        运行爬虫主流程
        :param keyword: 搜索关键词
        :param max_pages: 最大抓取页数
        """
        try:
            # 1. 搜索商品
            self.search_product(keyword)
            
            page_num = 1
            while page_num <= max_pages:
                print(f"\n--- 正在抓取第 {page_num} 页 ---")
                
                # 2. 滚动加载当前页所有商品
                self.scroll_to_load_all()
                
                # 3. 提取当前页数据
                page_data = self.extract_page_data()
                self.data.extend(page_data)
                print(f"第 {page_num} 页提取到 {len(page_data)} 条数据")
                
                # 4. 判断是否继续翻页
                if page_num < max_pages and self.has_next_page():
                    if not self.go_to_next_page():
                        print("翻页失败,停止抓取")
                        break
                    page_num += 1
                else:
                    print("已到达最后一页或达到最大页数限制")
                    break
            
            # 5. 保存数据
            self.save_to_csv(f'{keyword}_products.csv')
            
        finally:
            self.driver.quit()
            print("浏览器已关闭")

三、完整代码与运行效果

3.1 完整脚本

将以上所有类方法整合,形成完整脚本:

# jd_spider.py
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.chrome.options import Options
import time
import pandas as pd

class JdSpider:
    # ... 将上述所有方法粘贴在这里 ...

if __name__ == '__main__':
    spider = JdSpider(headless=False)  # 调试时可关闭无头模式
    spider.run('Python书籍', max_pages=3)

3.2 运行结果示例

正在搜索关键词: Python书籍
搜索完成,页面标题: Python书籍 - 商品搜索 - 京东

--- 正在抓取第 1 页 ---
正在滚动页面加载商品...
页面滚动完成
本页找到 60 个商品
第 1 页提取到 60 条数据

--- 正在抓取第 2 页 ---
正在滚动页面加载商品...
页面滚动完成
本页找到 60 个商品
第 2 页提取到 60 条数据

--- 正在抓取第 3 页 ---
正在滚动页面加载商品...
页面滚动完成
本页找到 60 个商品
第 3 页提取到 60 条数据
已到达最后一页或达到最大页数限制
数据已保存至 Python书籍_products.csv,共 180 条记录
浏览器已关闭

生成的CSV文件内容示例:

name price commit crawl_time
Python编程 从入门到实践 第2版 ¥52.50 200+条评价 2025-02-28 15:30:22
Python编程三剑客套装 ¥206.90 5万+条评价 2025-02-28 15:30:23
零基础学Python(全彩版) ¥37.50 10万+条评价 2025-02-28 15:30:24

四、常见问题与优化建议

4.1 常见问题排查

问题 可能原因 解决方案
找不到元素 页面未完全加载/选择器过期 增加等待时间,更新XPath
翻页点击无效 按钮被遮挡/未滚动到可视区域 先用scrollIntoView()滚动再点击
数据重复 翻页逻辑错误 检查翻页条件,添加去重机制
速度太慢 无头模式未启用/图片未禁用 开启无头模式,禁用图片

4.2 反爬虫应对策略

电商网站的反爬虫机制日益强大,建议采取以下措施:

  1. 随机延迟:在操作之间加入time.sleep(random.uniform(1,3))
  2. 更换User-Agent:使用fake_useragent库随机生成
  3. 使用代理IP:配置代理服务器
  4. 模拟鼠标轨迹:使用ActionChains模拟更真实的操作
# 随机延迟示例
import random
time.sleep(random.uniform(1.5, 3.5))

4.3 扩展功能建议

  • 多线程抓取:使用concurrent.futures同时抓取多个关键词
  • 数据库存储:将数据存入MySQL或MongoDB
  • 异常恢复:断点续传,记录已抓取页数
  • 价格监控:定时运行脚本,监控价格变化

五、项目总结

通过这个实战项目,我们完整地实践了以下核心技能:

  1. 浏览器配置:无头模式、禁用图片、隐藏自动化特征
  2. 元素定位:XPath与CSS选择器的综合运用
  3. 等待策略:显式等待与强制等待的配合
  4. JavaScript执行:滚动加载的实现
  5. 翻页控制:基于页面特征的边界判断
  6. 数据持久化:CSV文件的生成与编码处理

这个机器人虽然以京东为例,但其核心逻辑可以轻松移植到淘宝、拼多多、亚马逊等其他电商平台——只需要调整元素定位表达式即可。

思考与挑战

  • 如何应对网站改版导致的选择器失效?
  • 如何在抓取效率和反爬之间找到平衡?
  • 如果网站使用动态分页(点击加载更多而非传统翻页),代码该如何调整?

欢迎在评论区分享你的实践经验和遇到的问题!


:本文代码基于Selenium 4.x编写,请在遵守网站robots.txt和相关法律法规的前提下使用爬虫技术。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐