Selenium基础操作

Selenium 基础操作

作为一名资深爬虫工程师,我将带您全面掌握Selenium自动化测试与网页爬取技术。

本教程基于Python 3.12,使用uv进行依赖管理,并通过FastAPI搭建模拟网站供实战练习。

第一章:环境搭建

1.1 安装Python 3.12

首先确保您已安装Python 3.12,可以从Python官网下载安装。

1.2 安装uv包管理器

uv是一个快速的Python包管理器,替代传统的pip:

# 安装uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# 或者使用pip安装
pip install uv

1.3 创建项目并安装依赖

# 创建项目目录
mkdir selenium-tutorial && cd selenium-tutorial

# 初始化项目
uv init -p 3.12

# 初始化虚拟环境并指定python版本
uv venv .venv

# 激活虚拟环境
# windows
.venv\Scripts\activate

# macos | linux
source .venv\bin\activate

# 安装所需依赖
uv add selenium fastapi uvicorn jinja2 python-multipart webdriver-manager

或者

# 创建项目目录
uv init selenium-tutorial -p 3.12 && cd selenium-tutorial

# 初始化虚拟环境并指定python版本
uv venv .venv

# 激活虚拟环境
# windows
.venv\Scripts\activate

# macos | linux
source .venv\bin\activate

# 安装所需依赖
uv add selenium fastapi uvicorn jinja2 python-multipart webdriver-manager

1.4 浏览器驱动配置

Selenium需要对应浏览器的驱动程序,我们使用webdriver-manager自动管理:

  • Chrome: 会自动下载对应版本的chromedriver
  • Firefox: 会自动下载geckodriver
  • Edge: 会自动下载msedgedriver

无需手动下载和配置路径,webdriver-manager会处理一切。

第二章:FastAPI模拟网站搭建

为了进行安全合法的练习,我们搭建一个模拟网站作为爬取目标。

app.py

"""
code: app.py
"""
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import random
import string

# 创建FastAPI应用
app = FastAPI(title="Selenium 练习网站")

# 设置模板目录
templates = Jinja2Templates(directory="templates")


# 生成随机token用于演示token验证
def generate_token():
  return ''.join(random.choices(string.ascii_letters + string.digits, k=16))


# 首页
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
  # 生成页面token
  token = generate_token()
  return templates.TemplateResponse("index.html", {
    "request": request,
    "token": token,
    "products": [
      {"id": 1, "name": "笔记本电脑", "price": 5999, "category": "电子产品"},
      {"id": 2, "name": "机械键盘", "price": 399, "category": "电脑配件"},
      {"id": 3, "name": "无线鼠标", "price": 199, "category": "电脑配件"},
      {"id": 4, "name": "蓝牙耳机", "price": 799, "category": "音频设备"}
    ]
  })


# 登录页面
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
  token = generate_token()
  return templates.TemplateResponse("login.html", {
    "request": request,
    "token": token
  })


# 处理登录请求
@app.post("/login", response_class=HTMLResponse)
async def login(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    token: str = Form(...)
):
  # 简单验证逻辑
  if username == "test" and password == "password123":
    return templates.TemplateResponse("dashboard.html", {
      "request": request,
      "message": "登录成功",
      "username": username
    })
  else:
    return templates.TemplateResponse("login.html", {
      "request": request,
      "error": "用户名或密码错误",
      "token": generate_token()
    })


# 动态内容页面(用于演示等待机制)
@app.get("/dynamic", response_class=HTMLResponse)
async def dynamic_content(request: Request):
  return templates.TemplateResponse("dynamic.html", {"request": request})


# 表单页面
@app.get("/form", response_class=HTMLResponse)
async def form_page(request: Request):
  token = generate_token()
  return templates.TemplateResponse("form.html", {
    "request": request,
    "token": token
  })


# 文件上传页面
@app.get("/upload", response_class=HTMLResponse)
async def upload_page(request: Request):
  return templates.TemplateResponse("upload.html", {"request": request})


if __name__ == "__main__":
  import uvicorn

  uvicorn.run(app, host="0.0.0.0", port=8000)

创建模板文件

创建templates目录,并添加以下HTML文件:

templates/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Selenium练习网站</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
        .product { border: 1px solid #ddd; padding: 10px; margin: 10px; display: inline-block; width: 200px; }
        .nav { margin-bottom: 20px; }
        .nav a { margin-right: 15px; text-decoration: none; color: #333; }
    </style>
    <input type="hidden" id="csrf_token" value="{{ token }}">
</head>
<body>
    <div class="nav">
        <a href="/" id="home-link">首页</a>
        <a href="/login" class="nav-link">登录</a>
        <a href="/dynamic" class="nav-link">动态内容</a>
        <a href="/form" class="nav-link">表单</a>
        <a href="/upload" class="nav-link">文件上传</a>
    </div>
    
    <h1>产品列表</h1>
    <div id="products">
        {% for product in products %}
        <div class="product" data-id="{{ product.id }}">
            <h3 class="product-name">{{ product.name }}</h3>
            <p class="product-price">价格: ¥{{ product.price }}</p>
            <p class="category">{{ product.category }}</p>
            <button class="add-to-cart" data-product="{{ product.id }}">加入购物车</button>
        </div>
        {% endfor %}
    </div>
</body>
</html>
templates/login.html
<!DOCTYPE html>
<html>
<head>
    <title>登录 - Selenium练习网站</title>
    <style>
        .container { max-width: 400px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; }
        .error { color: red; }
    </style>
</head>
<body>
    <div class="container">
        <h1>用户登录</h1>
        {% if error %}
        <p class="error">{{ error }}</p>
        {% endif %}
        <form method="post">
            <input type="hidden" name="token" value="{{ token }}">
            <div>
                <label for="username">用户名:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div>
                <label for="password">密码:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit" id="submit-btn">登录</button>
        </form>
    </div>
</body>
</html>
templates/dashboard.html
<!DOCTYPE html>
<html>
<head>
    <title>用户中心 - Selenium练习网站</title>
    <style>
        .container { max-width: 800px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; }
        .success { color: green; font-size: 1.2em; }
        .user-info { margin: 20px 0; padding: 15px; background-color: #f5f5f5; }
        .nav { margin-bottom: 20px; }
        .nav a { margin-right: 15px; text-decoration: none; color: #333; }
    </style>
</head>
<body>
    <div class="nav">
        <a href="/" id="home-link">首页</a>
        <a href="/login" class="nav-link">登录</a>
        <a href="/dynamic" class="nav-link">动态内容</a>
        <a href="/form" class="nav-link">表单</a>
        <a href="/upload" class="nav-link">文件上传</a>
    </div>
    
    <div class="container">
        <h1>用户中心</h1>
        <p class="success">{{ message }}</p>
        
        <div class="user-info">
            <p>用户名: {{ username }}</p>
            <p>登录时间: {{ now }}</p>
            <p>账户状态: 正常</p>
        </div>
        
        <h3>最近活动</h3>
        <ul>
            <li>浏览了产品列表</li>
            <li>查看了动态内容</li>
            <li>提交了测试表单</li>
        </ul>
    </div>
</body>
</html>
    
templates/dynamic.html
<!DOCTYPE html>
<html>
<head>
    <title>动态内容 - Selenium练习网站</title>
    <style>
        .container { max-width: 800px; margin: 50px auto; padding: 20px; }
        .dynamic-content { margin: 20px 0; padding: 15px; border: 1px solid #ccc; display: none; }
        .visible-after-delay { margin: 20px 0; padding: 15px; background-color: #e3f2fd; display: none; }
        #delayed-button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer; display: none; }
        #status-message { margin-top: 20px; padding: 10px; }
        .nav { margin-bottom: 20px; }
        .nav a { margin-right: 15px; text-decoration: none; color: #333; }
    </style>
</head>
<body>
    <div class="nav">
        <a href="/" id="home-link">首页</a>
        <a href="/login" class="nav-link">登录</a>
        <a href="/dynamic" class="nav-link">动态内容</a>
        <a href="/form" class="nav-link">表单</a>
        <a href="/upload" class="nav-link">文件上传</a>
    </div>
    
    <div class="container">
        <h1>动态内容演示</h1>
        <p>本页面展示各种动态加载的内容,用于测试Selenium的等待机制。</p>
        
        <div id="dynamic-content" class="dynamic-content">
            这是延迟加载的动态内容,通常通过JavaScript在页面加载后一段时间显示。
        </div>
        
        <div class="visible-after-delay">
            这是另一个延迟显示的内容,使用了不同的延迟时间。
        </div>
        
        <button id="delayed-button">点击我</button>
        <div id="status-message"></div>
    </div>

    <script>
        // 模拟动态内容加载
        setTimeout(() => {
            document.getElementById('dynamic-content').style.display = 'block';
        }, 2000); // 2秒后显示
        
        // 另一个延迟显示的元素
        setTimeout(() => {
            document.querySelector('.visible-after-delay').style.display = 'block';
        }, 4000); // 4秒后显示
        
        // 延迟显示按钮并添加点击事件
        setTimeout(() => {
            const button = document.getElementById('delayed-button');
            button.style.display = 'inline-block';
            
            button.addEventListener('click', () => {
                document.getElementById('status-message').textContent = '按钮已点击,操作成功!';
                document.getElementById('status-message').style.backgroundColor = '#dff0d8';
            });
        }, 6000); // 6秒后显示按钮
        
        // 存储CSRF Token到JavaScript变量,用于演示
        window.csrfToken = 'dynamic_' + Math.random().toString(36).substring(2);
    </script>
</body>
</html>
    
templates/form.html
<!DOCTYPE html>
<html>
<head>
    <title>表单示例 - Selenium练习网站</title>
    <style>
        .container { max-width: 600px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input, select, textarea { width: 100%; padding: 8px; box-sizing: border-box; }
        button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
        .success { color: green; margin-top: 15px; padding: 10px; background-color: #dff0d8; display: none; }
        .error { color: red; margin-top: 15px; }
        .nav { margin-bottom: 20px; }
        .nav a { margin-right: 15px; text-decoration: none; color: #333; }
    </style>
</head>
<body>
    <div class="nav">
        <a href="/" id="home-link">首页</a>
        <a href="/login" class="nav-link">登录</a>
        <a href="/dynamic" class="nav-link">动态内容</a>
        <a href="/form" class="nav-link">表单</a>
        <a href="/upload" class="nav-link">文件上传</a>
    </div>
    
    <div class="container">
        <h1>用户信息表单</h1>
        <form id="user-form" method="post">
            <input type="hidden" name="token" value="{{ token }}">
            
            <div class="form-group">
                <label for="name">姓名:</label>
                <input type="text" id="name" name="name" required>
            </div>
            
            <div class="form-group">
                <label for="email">邮箱:</label>
                <input type="email" id="email" name="email" required>
            </div>
            
            <div class="form-group">
                <label for="age">年龄:</label>
                <input type="number" id="age" name="age" min="1" max="120">
            </div>
            
            <div class="form-group">
                <label for="gender">性别:</label>
                <select id="gender" name="gender">
                    <option value="">请选择</option>
                    <option value="male"></option>
                    <option value="female"></option>
                    <option value="other">其他</option>
                </select>
            </div>
            
            <div class="form-group">
                <label>兴趣爱好:</label>
                <div>
                    <input type="checkbox" id="hobby1" name="hobbies" value="reading">
                    <label for="hobby1">阅读</label>
                    
                    <input type="checkbox" id="hobby2" name="hobbies" value="sports">
                    <label for="hobby2">运动</label>
                    
                    <input type="checkbox" id="hobby3" name="hobbies" value="music">
                    <label for="hobby3">音乐</label>
                </div>
            </div>
            
            <div class="form-group">
                <label for="message">留言:</label>
                <textarea id="message" name="message" rows="4"></textarea>
            </div>
            
            <button type="submit" id="submit-form">提交</button>
        </form>
        
        <div id="form-success" class="success">表单提交成功!</div>
        {% if error %}
        <div class="error">{{ error }}</div>
        {% endif %}
    </div>

    <script>
        // 简单的表单验证
        document.getElementById('user-form').addEventListener('submit', function(e) {
            const name = document.getElementById('name').value;
            const email = document.getElementById('email').value;
            
            if (!name || !email) {
                alert('请填写姓名和邮箱');
                e.preventDefault();
                return false;
            }
            
            // 在实际提交前更新success消息的显示状态
            document.getElementById('form-success').style.display = 'block';
            return true;
        });
    </script>
</body>
</html>
    
templates/upload.html
<!DOCTYPE html>
<html>
<head>
    <title>文件上传 - Selenium练习网站</title>
    <style>
        .container { max-width: 600px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="file"] { margin: 10px 0; }
        button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
        .upload-area { border: 2px dashed #ccc; padding: 30px; text-align: center; margin-bottom: 20px; }
        .upload-area.dragover { border-color: #4CAF50; background-color: #f5f5f5; }
        .message { margin-top: 20px; padding: 10px; display: none; }
        .success { background-color: #dff0d8; color: #3c763d; }
        .error { background-color: #f2dede; color: #a94442; }
        .nav { margin-bottom: 20px; }
        .nav a { margin-right: 15px; text-decoration: none; color: #333; }
    </style>
</head>
<body>
    <div class="nav">
        <a href="/" id="home-link">首页</a>
        <a href="/login" class="nav-link">登录</a>
        <a href="/dynamic" class="nav-link">动态内容</a>
        <a href="/form" class="nav-link">表单</a>
        <a href="/upload" class="nav-link">文件上传</a>
    </div>
    
    <div class="container">
        <h1>文件上传演示</h1>
        <p>本页面用于测试文件上传功能,可以上传图片、文档等文件。</p>
        
        <form id="upload-form" method="post" enctype="multipart/form-data">
            <div class="form-group">
                <label for="file-title">文件标题:</label>
                <input type="text" id="file-title" name="title" required>
            </div>
            
            <div class="form-group">
                <label>选择文件:</label>
                <div class="upload-area" id="upload-area">
                    点击或拖拽文件到这里上传
                    <input type="file" id="file-upload" name="file" multiple style="display: none;">
                </div>
                <p id="file-name" style="margin-top: 10px;"></p>
            </div>
            
            <div class="form-group">
                <label for="file-description">文件描述:</label>
                <textarea id="file-description" name="description" rows="3"></textarea>
            </div>
            
            <button type="submit" id="upload-btn">上传文件</button>
        </form>
        
        <div id="success-message" class="message success">文件上传成功!</div>
        <div id="error-message" class="message error">文件上传失败,请重试。</div>
    </div>

    <script>
        // 处理拖拽上传
        const uploadArea = document.getElementById('upload-area');
        const fileInput = document.getElementById('file-upload');
        const fileNameDisplay = document.getElementById('file-name');
        
        // 点击上传区域触发文件选择
        uploadArea.addEventListener('click', () => {
            fileInput.click();
        });
        
        // 显示选择的文件名
        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                const fileNames = Array.from(e.target.files).map(file => file.name).join(', ');
                fileNameDisplay.textContent = `已选择: ${fileNames}`;
            }
        });
        
        // 拖拽相关事件
        uploadArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadArea.classList.add('dragover');
        });
        
        uploadArea.addEventListener('dragleave', () => {
            uploadArea.classList.remove('dragover');
        });
        
        uploadArea.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadArea.classList.remove('dragover');
            
            if (e.dataTransfer.files.length > 0) {
                // 这里只是模拟,实际项目中需要额外处理
                const fileNames = Array.from(e.dataTransfer.files).map(file => file.name).join(', ');
                fileNameDisplay.textContent = `已选择: ${fileNames}`;
            }
        });
        
        // 表单提交处理
        document.getElementById('upload-form').addEventListener('submit', function(e) {
            // 简单验证
            if (!fileInput.files.length) {
                alert('请选择要上传的文件');
                e.preventDefault();
                return false;
            }
            
            // 显示成功消息(实际应用中会由服务器处理)
            setTimeout(() => {
                document.getElementById('success-message').style.display = 'block';
                document.getElementById('error-message').style.display = 'none';
            }, 1000);
            
            return true;
        });
    </script>
</body>
</html>
    

启动模拟网站:

uv run app.py

访问 http://localhost:8000 即可看到我们创建的模拟网站。

第三章:Selenium基础

3.0 牛刀小试

# -*- coding: utf-8 -*-
"""
02_start_browser.py
作用:用三种方式启动 Chrome
"""
from time import sleep

from selenium import webdriver  # 总入口
from selenium.webdriver.chrome.service import Service  # 驱动服务
from selenium.webdriver.chrome.options import Options  # 浏览器参数
from webdriver_manager.chrome import ChromeDriverManager

# 方法1:最简方式(驱动已放 PATH)
driver1 = webdriver.Chrome()
driver1.maximize_window() # 浏览器窗口最大化
driver1.get("https://www.baidu.com")
sleep(10)
driver1.quit()

# 方法2:指定驱动路径
service = Service(executable_path=r"C:\Users\李昊哲\.wdm\drivers\chromedriver\win64\140.0.7339.82\chromedriver-win32\chromedriver.exe")
driver2 = webdriver.Chrome(service=service)
driver2.maximize_window() # 浏览器窗口最大化
driver2.get("https://www.sogou.com")
sleep(10)
driver2.quit()

# 方法3:无头模式 + 常用参数
options = Options()
options.add_argument("--headless") # 无头模式
options.add_argument("--window-size=1920x1080") # 设置浏览器窗体尺寸
driver3 = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
driver3.get("https://cn.bing.com/")
print("页面标题:", driver3.title)
driver3.quit()

3.1 第一个Selenium脚本

first_script.py

"""
code: first_script.py
"""
# 导入必要的库
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time

def main():
    # 初始化Chrome浏览器驱动
    # 使用webdriver_manager自动管理驱动,无需手动下载
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        # 打开我们的模拟网站首页
        driver.get("http://localhost:8000")
        
        # 打印当前页面标题
        print(f"页面标题: {driver.title}")
        
        # 打印当前页面URL
        print(f"当前URL: {driver.current_url}")
        
        # 等待3秒,让我们看清效果
        time.sleep(3)
        
        # 刷新页面
        driver.refresh()
        print("页面已刷新")
        time.sleep(2)
        
        # 导航到登录页面
        driver.get("http://localhost:8000/login")
        print("已导航到登录页面")
        time.sleep(2)
        
        # 后退到上一页
        driver.back()
        print("已后退到首页")
        time.sleep(2)
        
        # 前进到下一页
        driver.forward()
        print("已前进到登录页面")
        time.sleep(2)
        
    finally:
        # 关闭浏览器
        driver.quit()
        print("浏览器已关闭")

if __name__ == "__main__":
    main()

3.2 代码解析

  1. 导入库:

    • webdriver:Selenium的核心库,提供各种浏览器的驱动接口
    • Service:用于管理浏览器驱动的服务
    • ChromeDriverManager:自动管理Chrome驱动的安装和版本匹配
  2. 初始化浏览器:

    • webdriver.Chrome():创建Chrome浏览器实例
    • 通过ServiceChromeDriverManager自动处理驱动
  3. 基本操作:

    • get(url):打开指定URL
    • title:获取页面标题
    • current_url:获取当前页面URL
    • refresh():刷新页面
    • back():后退到上一页
    • forward():前进到下一页
    • quit():关闭浏览器并释放资源

第四章:8种元素定位方式

Selenium提供了8种元素定位方式,掌握这些是进行自动化操作的基础。

4.1 通过ID定位 (find_element_by_id)

locate_by_id.py

"""
code: locate_by_id.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    # 初始化浏览器驱动
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        # 打开登录页面
        driver.get("http://localhost:8000/login")
        time.sleep(1)
        
        # 1. 通过ID定位用户名输入框
        # 思路:找到id为"username"的元素,这是最直接可靠的定位方式
        username_input = driver.find_element(By.ID, "username")
        
        # 操作元素:输入用户名
        username_input.send_keys("test_user")
        time.sleep(1)
        
        # 2. 通过ID定位密码输入框
        password_input = driver.find_element(By.ID, "password")
        password_input.send_keys("test_password")
        time.sleep(1)
        
        # 3. 通过ID定位提交按钮
        submit_btn = driver.find_element(By.ID, "submit-btn")
        
        # 操作元素:点击按钮
        submit_btn.click()
        time.sleep(2)
        
    finally:
        # 关闭浏览器
        driver.quit()

if __name__ == "__main__":
    main()

4.2 通过Name定位 (find_element_by_name)

locate_by_name.py

"""
code: locate_by_name.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000/login")
        time.sleep(1)
        
        # 通过Name定位用户名输入框
        # 思路:当元素有name属性时,可以使用此方法,适合表单元素
        username_input = driver.find_element(By.NAME, "username")
        username_input.send_keys("test")
        time.sleep(1)
        
        # 通过Name定位密码输入框
        password_input = driver.find_element(By.NAME, "password")
        password_input.send_keys("password123")
        time.sleep(1)
        
        # 通过Name定位token字段(隐藏字段)
        # 这在处理CSRF验证时很有用
        token_input = driver.find_element(By.NAME, "token")
        print(f"获取到的token值: {token_input.get_attribute('value')}")
        
        # 提交表单
        driver.find_element(By.ID, "submit-btn").click()
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.3 通过Class Name定位 (find_element_by_class_name)

locate_by_class.py

"""
code: locate_by_class.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 通过Class Name定位单个元素
        # 思路:定位导航链接,class为"nav-link"
        first_link = driver.find_element(By.CLASS_NAME, "nav-link")
        print(f"第一个导航链接文本: {first_link.text}")
        first_link.click()
        time.sleep(2)
        
        # 返回首页
        driver.back()
        time.sleep(1)
        
        # 通过Class Name定位多个元素
        # 思路:获取所有产品项,class为"product"
        products = driver.find_elements(By.CLASS_NAME, "product")
        print(f"找到 {len(products)} 个产品")
        
        # 遍历所有产品并打印名称
        for product in products:
            # 在每个产品元素内部查找产品名称
            name = product.find_element(By.CLASS_NAME, "product-name")
            print(f"产品名称: {name.text}")
        
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.4 通过Tag Name定位 (find_element_by_tag_name)

locate_by_tag.py

"""
code: locate_by_tag.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 通过Tag Name定位元素
        # 思路:获取所有链接标签<a>
        links = driver.find_elements(By.TAG_NAME, "a")
        print(f"页面上有 {len(links)} 个链接")
        
        # 打印所有链接的文本和URL
        for link in links:
            print(f"链接文本: {link.text}, URL: {link.get_attribute('href')}")
        
        # 通过Tag Name定位标题
        # 思路:找到第一个h1标签
        heading = driver.find_element(By.TAG_NAME, "h1")
        print(f"页面主标题: {heading.text}")
        
        # 在表单中通过标签名定位输入框(结合其他定位方式更有效)
        driver.get("http://localhost:8000/login")
        inputs = driver.find_elements(By.TAG_NAME, "input")
        print(f"登录表单中有 {len(inputs)} 个输入框")
        
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.5 通过Link Text定位 (find_element_by_link_text)

locate_by_link_text.py

"""
code: locate_by_link_text.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 通过完整链接文本定位
        # 思路:精确匹配链接的全部文本
        login_link = driver.find_element(By.LINK_TEXT, "登录")
        print(f"找到登录链接: {login_link.get_attribute('href')}")
        login_link.click()
        time.sleep(2)
        
        # 返回首页
        driver.back()
        time.sleep(1)
        
        # 定位另一个链接
        dynamic_link = driver.find_element(By.LINK_TEXT, "动态内容")
        dynamic_link.click()
        time.sleep(2)
        
        # 返回首页
        driver.back()
        time.sleep(1)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.6 通过Partial Link Text定位 (find_element_by_partial_link_text)

locate_by_partial_link.py

"""
code: locate_by_partial_link.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 通过部分链接文本定位
        # 思路:只需匹配链接文本的一部分,适用于文本较长或动态变化的情况
        form_link = driver.find_element(By.PARTIAL_LINK_TEXT, "表")
        print(f"找到包含'表'字的链接: {form_link.text}")
        form_link.click()
        time.sleep(2)
        
        # 返回首页
        driver.back()
        time.sleep(1)
        
        # 另一个示例
        upload_link = driver.find_element(By.PARTIAL_LINK_TEXT, "上传")
        upload_link.click()
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.7 通过XPath定位 (find_element_by_xpath)

XPath是一种在XML文档中定位元素的语言,也可用于HTML。它是最灵活的定位方式之一。

locate_by_xpath.py

"""
code: locate_by_xpath.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 1. 绝对路径定位(不推荐,维护性差)
        # 思路:从根节点开始的完整路径,页面结构变化会导致失效
        home_link = driver.find_element(By.XPATH, "/html/body/div/a[1]")
        print(f"通过绝对路径找到的链接: {home_link.text}")
        
        # 2. 相对路径定位
        # 思路:从任意节点开始,更灵活
        products = driver.find_elements(By.XPATH, "//div[@class='product']")
        print(f"通过相对路径找到 {len(products)} 个产品")
        
        # 3. 属性匹配
        # 思路:通过元素的属性值定位
        username_input = driver.find_element(By.XPATH, "//input[@id='username']")
        # 如果上面找不到(因为在首页),我们导航到登录页
        if not username_input.is_displayed():
            driver.get("http://localhost:8000/login")
            username_input = driver.find_element(By.XPATH, "//input[@id='username']")
        username_input.send_keys("test")
        
        # 4. 部分属性匹配
        # 思路:匹配属性值的一部分,使用contains()
        password_input = driver.find_element(By.XPATH, "//input[contains(@name, 'pass')]")
        password_input.send_keys("password123")
        
        # 5. 文本匹配
        # 思路:通过元素的文本内容定位
        submit_btn = driver.find_element(By.XPATH, "//button[text()='登录']")
        submit_btn.click()
        time.sleep(2)
        
        # 返回首页
        driver.back()
        time.sleep(1)
        
        # 6. 层级定位
        # 思路:结合父子关系定位
        first_product_price = driver.find_element(By.XPATH, "//div[@class='product'][1]//p[@class='product-price']")
        print(f"第一个产品价格: {first_product_price.text}")
        
        # 7. 逻辑运算
        # 思路:使用and/or组合多个条件
        dynamic_link = driver.find_element(By.XPATH, "//a[@class='nav-link' and contains(text(), '动态')]")
        dynamic_link.click()
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

4.8 通过CSS Selector定位 (find_element_by_css_selector)

CSS选择器是另一种强大的元素定位方式,通常比XPath更简洁。

locate_by_css.py

"""
code: locate_by_css.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000")
        time.sleep(1)
        
        # 1. ID选择器
        # 思路:使用#符号加ID值
        home_link = driver.find_element(By.CSS_SELECTOR, "#home-link")
        print(f"通过ID选择器找到: {home_link.text}")
        
        # 2. Class选择器
        # 思路:使用.符号加class值
        nav_links = driver.find_elements(By.CSS_SELECTOR, ".nav-link")
        print(f"通过Class选择器找到 {len(nav_links)} 个导航链接")
        
        # 3. 标签选择器
        # 思路:直接使用标签名
        headings = driver.find_elements(By.CSS_SELECTOR, "h1, h3")
        print(f"找到 {len(headings)} 个标题元素")
        
        # 4. 属性选择器
        # 思路:通过元素属性定位
        token_input = driver.find_element(By.CSS_SELECTOR, "input[type='hidden'][id='csrf_token']")
        print(f"CSRF Token值: {token_input.get_attribute('value')}")
        
        # 5. 层级选择器
        # 思路:通过元素层级关系定位
        product_prices = driver.find_elements(By.CSS_SELECTOR, ".product .product-price")
        print("所有产品价格:")
        for price in product_prices:
            print(price.text)
        
        # 6. 伪类选择器
        # 思路:使用CSS伪类定位
        first_product = driver.find_element(By.CSS_SELECTOR, ".product:first-child")
        print(f"第一个产品名称: {first_product.find_element(By.CSS_SELECTOR, '.product-name').text}")
        
        # 导航到登录页
        driver.get("http://localhost:8000/login")
        time.sleep(1)
        
        # 7. 组合选择器
        # 思路:组合多种条件定位
        form_elements = driver.find_elements(By.CSS_SELECTOR, "form div input")
        print(f"登录表单中有 {len(form_elements)} 个输入元素")
        
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

第五章:等待机制

在自动化测试中,页面元素的加载往往需要时间,使用合适的等待机制至关重要。

selenium_waits.py

"""
code: selenium_waits.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
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
import time

def main():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        # 1. 隐式等待
        # 思路:设置全局等待时间,对所有元素查找操作生效
        # 注意:隐式等待会影响整个driver的生命周期
        driver.implicitly_wait(10)  # 等待10秒
        
        driver.get("http://localhost:8000/dynamic")
        print("已打开动态内容页面")
        
        # 2. 强制等待(不推荐)
        # 思路:固定等待一段时间,不管元素是否已加载
        # 缺点:会浪费不必要的时间,或因加载慢而失败
        print("使用强制等待...")
        time.sleep(3)  # 强制等待3秒
        
        # 3. 显式等待
        # 思路:针对特定元素设置等待条件和超时时间
        print("使用显式等待...")
        try:
            # 等待动态加载的元素出现,最长等待10秒,每500毫秒检查一次
            dynamic_element = WebDriverWait(driver, 10, 0.5).until(
                EC.presence_of_element_located((By.ID, "dynamic-content"))
            )
            print(f"找到动态内容: {dynamic_element.text}")
            
            # 等待元素可见
            visible_element = WebDriverWait(driver, 10).until(
                EC.visibility_of_element_located((By.CLASS_NAME, "visible-after-delay"))
            )
            print(f"可见元素内容: {visible_element.text}")
            
            # 等待元素可点击
            clickable_button = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((By.ID, "delayed-button"))
            )
            print("点击延迟加载的按钮")
            clickable_button.click()
            
            # 等待文本出现
            WebDriverWait(driver, 10).until(
                EC.text_to_be_present_in_element((By.ID, "status-message"), "已点击")
            )
            status = driver.find_element(By.ID, "status-message")
            print(f"状态: {status.text}")
            
        except TimeoutException:
            print("超时:未能在指定时间内找到元素")
        
        time.sleep(2)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

常用的Expected Conditions

  1. presence_of_element_located:元素存在于DOM中
  2. visibility_of_element_located:元素可见(存在且可见)
  3. element_to_be_clickable:元素可点击
  4. text_to_be_present_in_element:元素包含特定文本
  5. title_contains:页面标题包含特定文本
  6. invisibility_of_element_located:元素不可见
  7. frame_to_be_available_and_switch_to_it:frame可用并切换到该frame

第六章:突破Token限制

许多网站使用Token(如CSRF Token)来防止自动化脚本,以下是突破这些限制的常用方法:


bypass_token.py

"""
code; bypass_token.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

def bypass_token_method1():
    """方法1:从页面中提取Token并使用"""
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000/login")
        
        # 1. 从页面中提取token
        # 思路:先获取页面中的token值,再在后续操作中使用
        token_element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.NAME, "token"))
        )
        token_value = token_element.get_attribute("value")
        print(f"提取到的token值: {token_value}")
        
        # 2. 填写表单
        driver.find_element(By.ID, "username").send_keys("test")
        driver.find_element(By.ID, "password").send_keys("password123")
        
        # 3. 提交表单(会自动带上token)
        driver.find_element(By.ID, "submit-btn").click()
        
        # 验证是否登录成功
        try:
            success_message = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.XPATH, "//*[contains(text(), '登录成功')]"))
            )
            print("登录成功,Token验证通过")
        except:
            print("登录失败")
            
        time.sleep(3)
        
    finally:
        driver.quit()

def bypass_token_method2():
    """方法2:使用浏览器上下文保留Token"""
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        # 1. 首次访问获取Token并保存在浏览器中
        driver.get("http://localhost:8000")
        print("首次访问获取初始Token")
        time.sleep(2)
        
        # 2. 导航到其他页面,Token会通过Cookie保持
        driver.get("http://localhost:8000/form")
        print("导航到表单页面,使用保持的Token")
        
        # 3. 填写并提交表单,此时会自动使用Cookie中的Token
        driver.find_element(By.ID, "name").send_keys("测试用户")
        driver.find_element(By.ID, "email").send_keys("test@example.com")
        driver.find_element(By.ID, "submit-form").click()
        
        # 验证表单提交是否成功
        try:
            success_msg = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.ID, "form-success"))
            )
            print(f"表单提交成功: {success_msg.text}")
        except:
            print("表单提交失败")
            
        time.sleep(3)
        
    finally:
        driver.quit()

def bypass_token_method3():
    """方法3:使用JavaScript直接设置Token"""
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    
    try:
        driver.get("http://localhost:8000/login")
        
        # 1. 执行JavaScript获取或设置Token
        # 思路:有些网站的Token可能存储在JavaScript变量中
        token = driver.execute_script("""
            // 模拟从JavaScript变量获取Token
            if (window.csrfToken) {
                return window.csrfToken;
            }
            // 或者直接设置Token元素的值
            var tokenInput = document.querySelector('input[name="token"]');
            if (tokenInput) {
                // 这里可以替换为你获取到的有效Token
                tokenInput.value = 'override_token_value';
                return tokenInput.value;
            }
            return null;
        """)
        
        print(f"通过JS操作的Token值: {token}")
        
        # 2. 填写登录信息
        driver.find_element(By.ID, "username").send_keys("test")
        driver.find_element(By.ID, "password").send_keys("password123")
        driver.find_element(By.ID, "submit-btn").click()
        
        time.sleep(3)
        
    finally:
        driver.quit()

if __name__ == "__main__":
    print("=== 方法1:从页面中提取Token ===")
    bypass_token_method1()
    
    print("\n=== 方法2:使用浏览器上下文保留Token ===")
    bypass_token_method2()
    
    print("\n=== 方法3:使用JavaScript直接设置Token ===")
    bypass_token_method3()

第七章:实战案例

下面是一个综合实战案例,展示如何使用Selenium进行完整的网站爬取和交互:

selenium_practical.py

"""
code: selenium_practical.py
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
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
import json
from dataclasses import dataclass, asdict
from typing import List

# 数据类用于存储产品信息
@dataclass
class Product:
    id: int
    name: str
    price: float
    category: str

def crawl_products(driver) -> List[Product]:
    """爬取产品信息"""
    products = []
    
    try:
        # 导航到产品页面
        driver.get("http://localhost:8000")
        
        # 等待产品加载完成
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "products"))
        )
        
        # 获取所有产品元素
        product_elements = driver.find_elements(By.CLASS_NAME, "product")
        
        for element in product_elements:
            try:
                # 提取产品信息
                product_id = int(element.get_attribute("data-id"))
                name = element.find_element(By.CLASS_NAME, "product-name").text
                price_text = element.find_element(By.CLASS_NAME, "product-price").text
                price = float(price_text.replace("价格: ¥", ""))
                category = element.find_element(By.CLASS_NAME, "category").text
                
                # 创建产品对象
                product = Product(
                    id=product_id,
                    name=name,
                    price=price,
                    category=category
                )
                products.append(product)
                print(f"已爬取产品: {name}")
                
                # 模拟点击"加入购物车"按钮
                add_button = element.find_element(By.CLASS_NAME, "add-to-cart")
                add_button.click()
                time.sleep(0.5)
                
            except Exception as e:
                print(f"爬取单个产品时出错: {str(e)}")
        
        print(f"共爬取 {len(products)} 个产品")
        return products
        
    except Exception as e:
        print(f"爬取产品列表时出错: {str(e)}")
        return []

def login(driver, username: str, password: str) -> bool:
    """登录网站"""
    try:
        # 导航到登录页
        driver.get("http://localhost:8000/login")
        
        # 等待页面加载
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "submit-btn"))
        )
        
        # 提取并使用token
        token = driver.find_element(By.NAME, "token").get_attribute("value")
        print(f"登录使用的token: {token}")
        
        # 填写登录表单
        driver.find_element(By.ID, "username").send_keys(username)
        driver.find_element(By.ID, "password").send_keys(password)
        
        # 提交表单
        driver.find_element(By.ID, "submit-btn").click()
        
        # 验证登录是否成功
        try:
            WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.XPATH, "//*[contains(text(), '登录成功')]"))
            )
            print("登录成功")
            return True
        except TimeoutException:
            print("登录失败")
            return False
            
    except Exception as e:
        print(f"登录过程出错: {str(e)}")
        return False

def main():
    # 配置Chrome选项
    chrome_options = webdriver.ChromeOptions()
    # 添加用户配置,避免被识别为机器人
    chrome_options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
    # 禁用自动化控制特征
    chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
    chrome_options.add_experimental_option("useAutomationExtension", False)
    
    # 初始化驱动
    driver = webdriver.Chrome(
        service=Service(ChromeDriverManager().install()),
        options=chrome_options
    )
    
    # 进一步隐藏自动化特征
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            })
        """
    })
    
    try:
        # 1. 爬取产品信息
        products = crawl_products(driver)
        
        # 保存产品信息到JSON文件
        with open("products.json", "w", encoding="utf-8") as f:
            json.dump([asdict(p) for p in products], f, ensure_ascii=False, indent=2)
        print("产品信息已保存到products.json")
        
        # 2. 登录网站
        login_success = login(driver, "test", "password123")
        if login_success:
            time.sleep(2)
            
            # 3. 访问其他页面
            driver.get("http://localhost:8000/dynamic")
            try:
                # 等待动态内容加载
                dynamic_content = WebDriverWait(driver, 15).until(
                    EC.presence_of_element_located((By.ID, "dynamic-content"))
                )
                print(f"动态内容: {dynamic_content.text}")
            except TimeoutException:
                print("未能加载动态内容")
                
            time.sleep(3)
        
    finally:
        # 关闭浏览器
        driver.quit()
        print("爬虫完成,浏览器已关闭")

if __name__ == "__main__":
    main()

第八章:最佳实践与反反爬策略

8.1 避免被识别为机器人

  1. 设置合理的用户代理:模拟真实浏览器的用户代理
  2. 添加随机延迟:避免操作过于规律
  3. 禁用自动化特征:隐藏Selenium的特征标识
  4. 使用真实浏览器配置:加载真实的浏览器配置文件
  5. 模拟人类行为:随机化点击位置、添加鼠标移动等

8.2 代码组织与维护

  1. 封装常用操作:将重复的操作封装为函数或类
  2. 使用Page Object模式:将页面元素和操作封装为对象
  3. 异常处理:完善的异常处理机制,提高稳定性
  4. 日志记录:记录关键操作和错误信息
  5. 配置分离:将配置信息与代码分离,便于维护

8.3 性能优化

  1. 减少不必要的等待:合理设置等待时间
  2. 批量操作:尽量减少与浏览器的交互次数
  3. 无头模式:在不需要可视化时使用无头模式
  4. 资源限制:限制图片、CSS等非必要资源的加载

总结

本教程全面介绍了Selenium的使用方法,从环境搭建到高级技巧,涵盖了8种元素定位方式、等待机制、突破Token限制等关键内容。

通过FastAPI搭建的模拟网站,您可以安全合法地进行练习。

Selenium是一个强大的工具,不仅可用于网页爬取,还广泛应用于自动化测试。掌握这些技能将极大提升您在Web自动化领域的能力。

随着网站反爬技术的不断升级,爬虫工程师也需要不断学习和适应新的挑战。始终记住,在进行网络爬虫时,要遵守网站的robots协议和相关法律法规。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李昊哲小课

桃李不言下自成蹊

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

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

打赏作者

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

抵扣说明:

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

余额充值