详细介绍:使用Selenium和Python进行Web自动化(爬虫/测试)
目录
『宝藏代码胶囊开张啦!』—— 我的 CodeCapsule 来咯!✨
写代码不再头疼!我的新站点 CodeCapsule 主打一个 “白菜价”+“量身定制”!无论是卡脖子的毕设/课设/文献复现,需要灵光一现的算法改进,还是想给项目加个“外挂”,这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 CodeCapsule官网
使用Selenium和Python进行Web自动化(爬虫/测试)
1. 引言
在当今数字化时代,Web应用已经成为我们生活和工作中不可或缺的一部分。随着Web技术的快速发展,网站和Web应用的复杂性也在不断增加。无论是进行Web数据采集、自动化测试,还是执行重复性的Web操作,手动完成这些任务既耗时又容易出错。
Selenium 应运而生,它是一个强大的开源Web自动化工具,支持多种编程语言和浏览器。结合Python的简洁语法和丰富的生态系统,Selenium成为了Web自动化的首选方案。
1.1 为什么选择Selenium?
Selenium具有以下突出优势:
- 跨浏览器兼容性:支持Chrome、Firefox、Safari、Edge等主流浏览器
- 多语言支持:Python、Java、C#、JavaScript、Ruby等
- 真实用户模拟:在真实浏览器环境中执行,更接近用户真实行为
- 强大的元素定位:支持多种元素定位策略
- 丰富的生态系统:Selenium Grid、Selenium IDE等配套工具
1.2 应用场景
2. 环境搭建
2.1 安装必要的库
首先,我们需要安装Selenium和相关依赖:
pip install selenium
pip install webdriver-manager
pip install beautifulsoup4
pip install lxml
pip install pandas
pip install openpyxl
2.2 浏览器驱动配置
Selenium需要浏览器驱动来控制浏览器。推荐使用webdriver-manager自动管理驱动:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
3. Selenium基础知识
3.1 核心组件
Selenium由多个组件组成:
- Selenium WebDriver:核心API,用于控制浏览器
- Selenium Grid:分布式测试工具
- Selenium IDE:录制回放工具
3.2 基本工作流程
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
# 初始化浏览器驱动
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
try:
# 打开网页
driver.get("https://example.com")
# 查找元素并交互
element = driver.find_element(By.ID, "search")
element.send_keys("Selenium")
element.submit()
# 等待结果加载
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "results"))
)
finally:
# 关闭浏览器
driver.quit()
4. 元素定位策略
准确的元素定位是Web自动化的基础。Selenium提供了8种主要的元素定位方式。
4.1 定位策略详解
class ElementLocator:
"""元素定位器 - 演示各种定位策略"""
def __init__(self, driver):
self.driver = driver
def demonstrate_all_locators(self, url):
"""
演示所有元素定位策略
"""
self.driver.get(url)
locators_demo = {
"ID定位": (By.ID, "username"),
"Name定位": (By.NAME, "password"),
"Class定位": (By.CLASS_NAME, "form-control"),
"Tag定位": (By.TAG_NAME, "input"),
"XPath定位": (By.XPATH, "//input[@placeholder='用户名']"),
"CSS选择器定位": (By.CSS_SELECTOR, "button.btn-primary"),
"链接文本定位": (By.LINK_TEXT, "登录"),
"部分链接文本定位": (By.PARTIAL_LINK_TEXT, "忘记密码")
}
results = {}
for strategy, locator in locators_demo.items():
try:
elements = self.driver.find_elements(*locator)
results[strategy] = {
"count": len(elements),
"success": len(elements) > 0,
"example": str(elements[0]) if elements else "无元素"
}
except Exception as e:
results[strategy] = {
"count": 0,
"success": False,
"error": str(e)
}
return results
def smart_xpath_strategies(self):
"""
智能XPath定位策略
"""
strategies = {
"文本内容定位": "//button[text()='提交']",
"包含文本定位": "//a[contains(text(), '登录')]",
"属性包含定位": "//input[contains(@class, 'search')]",
"顺序定位": "//div[@class='item'][1]",
"最后元素定位": "//div[@class='item'][last()]",
"父级定位": "//input[@id='child']/parent::div",
"兄弟定位": "//h1/following-sibling::div",
"多条件定位": "//input[@type='text' and @class='form-control']"
}
return strategies
def robust_element_finding(self, by, value, timeout=10):
"""
健壮的元素查找方法
包含重试机制和多种定位策略回退
"""
wait = WebDriverWait(self.driver, timeout)
# 主要定位策略
try:
element = wait.until(EC.presence_of_element_located((by, value)))
return element
except:
pass
# 回退策略:如果主要定位失败,尝试其他方式
fallback_strategies = []
if by == By.ID:
# 如果ID定位失败,尝试Name或Class
fallback_strategies = [
(By.NAME, value),
(By.CLASS_NAME, value),
(By.CSS_SELECTOR, f"[id='{value}']")
]
elif by == By.NAME:
fallback_strategies = [
(By.CSS_SELECTOR, f"[name='{value}']"),
(By.ID, value)
]
elif by == By.XPATH:
# 简化XPath或尝试CSS选择器
fallback_strategies = [
(By.CSS_SELECTOR, self.xpath_to_css(value)),
(By.TAG_NAME, value.split('//')[-1].split('[')[0] if '//' in value else '')
]
for fallback_by, fallback_value in fallback_strategies:
if fallback_value: # 确保回退值不为空
try:
element = wait.until(EC.presence_of_element_located((fallback_by, fallback_value)))
print(f"使用回退策略定位成功: {fallback_by} = {fallback_value}")
return element
except:
continue
raise Exception(f"无法定位元素: {by} = {value}")
def xpath_to_css(self, xpath):
"""
简单的XPath到CSS选择器转换
"""
# 这里实现简单的转换逻辑
# 实际项目中可以使用更复杂的转换
if '[@id=' in xpath:
id_value = xpath.split('[@id=')[1].split(']')[0].strip("'\"")
return f"#{id_value}"
elif '[@class=' in xpath:
class_value = xpath.split('[@class=')[1].split(']')[0].strip("'\"")
return f".{class_value.replace(' ', '.')}"
return ""
4.2 定位策略最佳实践
def element_locator_best_practices():
"""
元素定位最佳实践
"""
practices = {
"优先级": [
"1. 首选ID定位(唯一且快速)",
"2. 其次Name、Class定位",
"3. 复杂情况使用XPath或CSS",
"4. 避免使用绝对XPath"
],
"稳定性技巧": [
"使用相对定位而非绝对定位",
"避免使用可能变化的索引",
"使用文本和属性的组合定位",
"添加适当的等待时间"
],
"维护建议": [
"为重要元素添加有意义的ID",
"使用Page Object模式封装定位器",
"定期review和更新定位策略",
"记录定位失败的原因和改进方法"
]
}
return practices
5. 高级交互操作
5.1 复杂的用户交互模拟
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import time
class AdvancedInteractions:
"""高级交互操作类"""
def __init__(self, driver):
self.driver = driver
self.actions = ActionChains(driver)
def drag_and_drop(self, source_element, target_element):
"""
拖放操作
Args:
source_element: 源元素
target_element: 目标元素
"""
self.actions.drag_and_drop(source_element, target_element).perform()
def hover_over_element(self, element):
"""
鼠标悬停操作
Args:
element: 要悬停的元素
"""
self.actions.move_to_element(element).perform()
def right_click(self, element):
"""
右键点击操作
Args:
element: 要右键点击的元素
"""
self.actions.context_click(element).perform()
def key_combinations(self, element, keys_sequence):
"""
键盘组合操作
Args:
element: 目标元素
keys_sequence: 按键序列,如 "ctrl+a", "ctrl+c" 等
"""
key_map = {
"ctrl": Keys.CONTROL,
"shift": Keys.SHIFT,
"alt": Keys.ALT,
"enter": Keys.ENTER,
"tab": Keys.TAB,
"esc": Keys.ESCAPE,
"a": "a",
"c": "c",
"v": "v"
}
# 清除元素内容
element.clear()
# 处理组合键
if "+" in keys_sequence:
keys = keys_sequence.split("+")
for key in keys:
key_lower = key.lower()
if key_lower in key_map:
self.actions.key_down(key_map[key_lower])
for key in reversed(keys):
key_lower = key.lower()
if key_lower in key_map:
self.actions.key_up(key_map[key_lower])
else:
element.send_keys(keys_sequence)
def file_upload(self, file_input_element, file_path):
"""
文件上传操作
Args:
file_input_element: 文件输入元素
file_path: 文件路径
"""
file_input_element.send_keys(file_path)
def scroll_operations(self, scroll_type="bottom", element=None):
"""
滚动操作
Args:
scroll_type: 滚动类型 - "bottom", "top", "element", "pixel"
element: 要滚动到的元素(当scroll_type为"element"时需要)
"""
if scroll_type == "bottom":
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
elif scroll_type == "top":
self.driver.execute_script("window.scrollTo(0, 0);")
elif scroll_type == "element" and element:
self.driver.execute_script("arguments[0].scrollIntoView();", element)
elif scroll_type == "pixel":
self.driver.execute_script("window.scrollBy(0, 500);")
def handle_javascript_alerts(self, action="accept", text=None):
"""
处理JavaScript弹窗
Args:
action: 操作类型 - "accept", "dismiss", "send_text"
text: 要输入的文本(当action为"send_text"时需要)
"""
alert = self.driver.switch_to.alert
if action == "accept":
alert.accept()
elif action == "dismiss":
alert.dismiss()
elif action == "send_text" and text:
alert.send_keys(text)
alert.accept()
def switch_to_frame(self, frame_identifier):
"""
切换到iframe
Args:
frame_identifier: 帧标识符,可以是id、name、index或元素
"""
self.driver.switch_to.frame(frame_identifier)
def switch_to_default_content(self):
"""切换回主文档"""
self.driver.switch_to.default_content()
def execute_javascript(self, script, *args):
"""
执行JavaScript代码
Args:
script: JavaScript代码
*args: 传递给脚本的参数
Returns:
脚本执行结果
"""
return self.driver.execute_script(script, *args)
5.2 等待策略和同步机制
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
class SmartWaiter:
"""智能等待器 - 处理各种等待场景"""
def __init__(self, driver, default_timeout=10):
self.driver = driver
self.default_timeout = default_timeout
def wait_for_element(self, by, value, timeout=None):
"""
等待元素出现
Args:
by: 定位方式
value: 定位值
timeout: 超时时间
Returns:
找到的元素
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_element_located((by, value)))
def wait_for_element_clickable(self, by, value, timeout=None):
"""
等待元素可点击
Args:
by: 定位方式
value: 定位值
timeout: 超时时间
Returns:
可点击的元素
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.element_to_be_clickable((by, value)))
def wait_for_element_visible(self, by, value, timeout=None):
"""
等待元素可见
Args:
by: 定位方式
value: 定位值
timeout: 超时时间
Returns:
可见的元素
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.visibility_of_element_located((by, value)))
def wait_for_page_load(self, timeout=None):
"""
等待页面完全加载
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout)
return wait.until(lambda driver: driver.execute_script("return document.readyState") == "complete")
def wait_for_ajax_complete(self, timeout=None):
"""
等待AJAX请求完成
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout)
return wait.until(lambda driver: driver.execute_script("return jQuery.active == 0"))
def custom_wait(self, condition_func, timeout=None, poll_frequency=0.5):
"""
自定义等待条件
Args:
condition_func: 等待条件函数
timeout: 超时时间
poll_frequency: 轮询频率
Returns:
等待条件的结果
"""
if timeout is None:
timeout = self.default_timeout
wait = WebDriverWait(self.driver, timeout, poll_frequency=poll_frequency)
return wait.until(condition_func)
def retry_operation(self, operation, max_retries=3, delay=1):
"""
重试操作
Args:
operation: 要重试的操作(函数)
max_retries: 最大重试次数
delay: 重试延迟(秒)
Returns:
操作结果
Raises:
最后一次尝试的异常
"""
last_exception = None
for attempt in range(max_retries):
try:
return operation()
except Exception as e:
last_exception = e
print(f"操作失败,第 {attempt + 1} 次重试... 错误: {str(e)}")
if attempt < max_retries - 1:
time.sleep(delay)
raise last_exception
6. 实战项目:智能网页爬虫
6.1 电商价格监控爬虫
import pandas as pd
import json
import time
from datetime import datetime
import logging
from urllib.parse import urljoin, urlparse
class ECommercePriceMonitor:
"""电商价格监控爬虫"""
def __init__(self, headless=True):
"""
初始化爬虫
Args:
headless (bool): 是否使用无头模式
"""
self.setup_driver(headless)
self.setup_logging()
self.products_data = []
def setup_driver(self, headless):
"""设置浏览器驱动"""
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
# 忽略证书错误
chrome_options.add_argument("--ignore-certificate-errors")
self.driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=chrome_options
)
# 设置页面加载超时
self.driver.set_page_load_timeout(30)
def setup_logging(self):
"""设置日志记录"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('price_monitor.log', encoding='utf-8'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def scrape_amazon_product(self, url):
"""
爬取Amazon商品信息
Args:
url (str): 商品URL
Returns:
dict: 商品信息
"""
self.logger.info(f"开始爬取Amazon商品: {url}")
try:
self.driver.get(url)
time.sleep(3)
product_info = {
'platform': 'Amazon',
'url': url,
'timestamp': datetime.now().isoformat()
}
# 商品标题
try:
title_element = self.driver.find_element(By.ID, "productTitle")
product_info['title'] = title_element.text.strip()
except:
product_info['title'] = "未知"
# 商品价格
try:
# 尝试多种价格选择器
price_selectors = [
"span.a-price-whole",
"span.a-price .a-offscreen",
"#priceblock_dealprice",
"#priceblock_ourprice",
".a-price .a-text-price"
]
for selector in price_selectors:
try:
price_element = self.driver.find_element(By.CSS_SELECTOR, selector)
price_text = price_element.text.strip()
if price_text:
product_info['price'] = self.clean_price(price_text)
break
except:
continue
else:
product_info['price'] = "无价格信息"
except:
product_info['price'] = "价格获取失败"
# 商品评分
try:
rating_element = self.driver.find_element(By.CSS_SELECTOR, "span.a-icon-alt")
product_info['rating'] = rating_element.get_attribute("textContent").split()[0]
except:
product_info['rating'] = "无评分"
# 评论数量
try:
review_count_element = self.driver.find_element(By.ID, "acrCustomerReviewText")
product_info['review_count'] = review_count_element.text.split()[0]
except:
product_info['review_count'] = "无评论"
# 库存状态
try:
stock_element = self.driver.find_element(By.ID, "availability")
product_info['stock_status'] = stock_element.text.strip()
except:
product_info['stock_status'] = "库存状态未知"
self.products_data.append(product_info)
self.logger.info(f"成功爬取商品: {product_info['title'][:50]}...")
return product_info
except Exception as e:
self.logger.error(f"爬取Amazon商品失败 {url}: {str(e)}")
return None
def scrape_jd_product(self, url):
"""
爬取京东商品信息
Args:
url (str): 商品URL
Returns:
dict: 商品信息
"""
self.logger.info(f"开始爬取京东商品: {url}")
try:
self.driver.get(url)
time.sleep(3)
product_info = {
'platform': '京东',
'url': url,
'timestamp': datetime.now().isoformat()
}
# 商品标题
try:
title_element = self.driver.find_element(By.CLASS_NAME, "sku-name")
product_info['title'] = title_element.text.strip()
except:
product_info['title'] = "未知"
# 商品价格
try:
price_element = self.driver.find_element(By.CLASS_NAME, "p-price")
price_spans = price_element.find_elements(By.TAG_NAME, "span")
product_info['price'] = self.clean_price(price_spans[1].text if len(price_spans) > 1 else price_element.text)
except:
product_info['price'] = "价格获取失败"
# 商品评分
try:
rating_element = self.driver.find_element(By.CSS_SELECTOR, ".percent-con")
product_info['rating'] = rating_element.text.strip()
except:
product_info['rating'] = "无评分"
# 评论数量
try:
review_count_element = self.driver.find_element(By.ID, "comment-count")
product_info['review_count'] = review_count_element.text.strip()
except:
product_info['review_count'] = "无评论"
self.products_data.append(product_info)
self.logger.info(f"成功爬取商品: {product_info['title'][:50]}...")
return product_info
except Exception as e:
self.logger.error(f"爬取京东商品失败 {url}: {str(e)}")
return None
def clean_price(self, price_text):
"""
清理价格文本
Args:
price_text (str): 原始价格文本
Returns:
str: 清理后的价格
"""
import re
# 移除货币符号和空格,只保留数字和小数点
cleaned = re.sub(r'[^\d.]', '', price_text)
return cleaned if cleaned else "无效价格"
def search_products(self, platform, keyword, max_pages=3):
"""
搜索商品
Args:
platform (str): 平台名称
keyword (str): 搜索关键词
max_pages (int): 最大搜索页数
Returns:
list: 商品URL列表
"""
self.logger.info(f"在 {platform} 搜索: {keyword}")
product_urls = []
try:
if platform.lower() == "amazon":
search_url = f"https://www.amazon.com/s?k={keyword.replace(' ', '+')}"
elif platform.lower() == "jd":
search_url = f"https://search.jd.com/Search?keyword={keyword}&enc=utf-8"
else:
self.logger.error(f"不支持的平台: {platform}")
return []
self.driver.get(search_url)
time.sleep(3)
for page in range(1, max_pages + 1):
self.logger.info(f"正在处理第 {page} 页")
# 提取商品链接
if platform.lower() == "amazon":
product_elements = self.driver.find_elements(By.CSS_SELECTOR, "h2 a.a-link-normal")
urls = [elem.get_attribute("href") for elem in product_elements]
elif platform.lower() == "jd":
product_elements = self.driver.find_elements(By.CSS_SELECTOR, ".gl-item .p-img a")
urls = [urljoin("https:", elem.get_attribute("href")) for elem in product_elements]
product_urls.extend(urls)
self.logger.info(f"第 {page} 页找到 {len(urls)} 个商品")
# 尝试翻页
if page < max_pages:
try:
if platform.lower() == "amazon":
next_button = self.driver.find_element(By.CLASS_NAME, "s-pagination-next")
elif platform.lower() == "jd":
next_button = self.driver.find_element(By.CLASS_NAME, "pn-next")
next_button.click()
time.sleep(3)
except:
self.logger.info("没有下一页,停止搜索")
break
self.logger.info(f"搜索完成,共找到 {len(product_urls)} 个商品")
return product_urls
except Exception as e:
self.logger.error(f"搜索商品失败: {str(e)}")
return []
def export_data(self, format_type="excel"):
"""
导出数据
Args:
format_type (str): 导出格式 - "excel", "csv", "json"
"""
if not self.products_data:
self.logger.warning("没有数据可导出")
return
filename = f"price_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
df = pd.DataFrame(self.products_data)
if format_type == "excel":
filename += ".xlsx"
df.to_excel(filename, index=False, engine='openpyxl')
elif format_type == "csv":
filename += ".csv"
df.to_csv(filename, index=False, encoding='utf-8-sig')
elif format_type == "json":
filename += ".json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.products_data, f, ensure_ascii=False, indent=2)
self.logger.info(f"数据已导出: {filename}")
return filename
except Exception as e:
self.logger.error(f"导出数据失败: {str(e)}")
return None
def close(self):
"""关闭浏览器"""
if self.driver:
self.driver.quit()
self.logger.info("浏览器已关闭")
# 使用示例
def demo_price_monitor():
"""演示价格监控爬虫"""
monitor = ECommercePriceMonitor(headless=False)
try:
# 搜索商品
print("=== 搜索商品 ===")
amazon_urls = monitor.search_products("amazon", "laptop", max_pages=2)
jd_urls = monitor.search_products("jd", "笔记本电脑", max_pages=2)
# 爬取商品详情(限制数量以节省时间)
print("\n=== 爬取商品详情 ===")
for url in amazon_urls[:3]: # 只爬取前3个
monitor.scrape_amazon_product(url)
time.sleep(2) # 添加延迟避免被封
for url in jd_urls[:3]: # 只爬取前3个
monitor.scrape_jd_product(url)
time.sleep(2)
# 导出数据
print("\n=== 导出数据 ===")
export_file = monitor.export_data("excel")
if export_file:
print(f"数据已导出到: {export_file}")
# 显示统计信息
print("\n=== 爬取统计 ===")
print(f"总共爬取商品: {len(monitor.products_data)}")
platforms = {}
for product in monitor.products_data:
platform = product['platform']
platforms[platform] = platforms.get(platform, 0) + 1
for platform, count in platforms.items():
print(f"{platform}: {count} 个商品")
finally:
monitor.close()
if __name__ == "__main__":
demo_price_monitor()
7. 实战项目:Web自动化测试框架
7.1 基于Page Object模式的测试框架
import unittest
import time
import logging
from datetime import datetime
import HTMLTestRunner
import os
class BasePage:
"""页面基类 - 封装常用操作"""
def __init__(self, driver):
self.driver = driver
self.waiter = SmartWaiter(driver)
self.logger = logging.getLogger(__name__)
def find_element(self, by, value, timeout=10):
"""查找元素(带等待)"""
return self.waiter.wait_for_element(by, value, timeout)
def click_element(self, by, value, timeout=10):
"""点击元素"""
element = self.waiter.wait_for_element_clickable(by, value, timeout)
element.click()
def input_text(self, by, value, text, timeout=10):
"""输入文本"""
element = self.find_element(by, value, timeout)
element.clear()
element.send_keys(text)
def get_text(self, by, value, timeout=10):
"""获取元素文本"""
element = self.find_element(by, value, timeout)
return element.text
def take_screenshot(self, name=None):
"""截图"""
if name is None:
name = f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
screenshot_dir = "screenshots"
os.makedirs(screenshot_dir, exist_ok=True)
filepath = os.path.join(screenshot_dir, f"{name}.png")
self.driver.save_screenshot(filepath)
self.logger.info(f"截图已保存: {filepath}")
return filepath
class LoginPage(BasePage):
"""登录页面"""
# 元素定位器
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "loginBtn")
ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
SUCCESS_MESSAGE = (By.CLASS_NAME, "welcome-message")
def __init__(self, driver):
super().__init__(driver)
self.url = "https://example.com/login"
def open(self):
"""打开登录页面"""
self.driver.get(self.url)
self.waiter.wait_for_page_load()
def login(self, username, password):
"""执行登录操作"""
self.logger.info(f"尝试登录,用户名: {username}")
self.input_text(*self.USERNAME_INPUT, username)
self.input_text(*self.PASSWORD_INPUT, password)
self.click_element(*self.LOGIN_BUTTON)
# 等待登录结果
time.sleep(2)
def get_error_message(self):
"""获取错误消息"""
try:
return self.get_text(*self.ERROR_MESSAGE)
except:
return None
def get_success_message(self):
"""获取成功消息"""
try:
return self.get_text(*self.SUCCESS_MESSAGE)
except:
return None
def is_login_successful(self):
"""检查是否登录成功"""
return self.get_success_message() is not None
class DashboardPage(BasePage):
"""仪表板页面"""
# 元素定位器
USER_MENU = (By.ID, "userMenu")
LOGOUT_BUTTON = (By.ID, "logoutBtn")
WELCOME_TEXT = (By.CLASS_NAME, "welcome-text")
def __init__(self, driver):
super().__init__(driver)
def is_loaded(self):
"""检查页面是否加载完成"""
try:
self.find_element(*self.USER_MENU)
return True
except:
return False
def logout(self):
"""执行退出登录"""
self.click_element(*self.USER_MENU)
self.click_element(*self.LOGOUT_BUTTON)
self.waiter.wait_for_page_load()
class TestLogin(unittest.TestCase):
"""登录测试用例"""
@classmethod
def setUpClass(cls):
"""测试类设置"""
cls.setup_logging()
cls.logger = logging.getLogger(__name__)
@classmethod
def setup_logging(cls):
"""设置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('test_automation.log', encoding='utf-8'),
logging.StreamHandler()
]
)
def setUp(self):
"""测试用例设置"""
self.logger.info("设置测试环境")
self.driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install())
)
self.driver.maximize_window()
self.login_page = LoginPage(self.driver)
self.dashboard_page = DashboardPage(self.driver)
def tearDown(self):
"""测试用例清理"""
self.logger.info("清理测试环境")
if hasattr(self, 'driver') and self.driver:
self.driver.quit()
def test_successful_login(self):
"""测试成功登录"""
self.logger.info("执行测试: test_successful_login")
# 打开登录页面
self.login_page.open()
# 执行登录
self.login_page.login("valid_user", "valid_password")
# 验证登录成功
self.assertTrue(
self.dashboard_page.is_loaded(),
"登录后应该跳转到仪表板页面"
)
# 截图记录
self.login_page.take_screenshot("successful_login")
self.logger.info("成功登录测试通过")
def test_failed_login_invalid_credentials(self):
"""测试使用无效凭据登录失败"""
self.logger.info("执行测试: test_failed_login_invalid_credentials")
self.login_page.open()
self.login_page.login("invalid_user", "wrong_password")
# 验证显示错误消息
error_message = self.login_page.get_error_message()
self.assertIsNotNone(error_message, "应该显示错误消息")
self.assertIn("无效", error_message)
self.login_page.take_screenshot("failed_login")
self.logger.info("无效凭据登录测试通过")
def test_failed_login_empty_credentials(self):
"""测试空凭据登录失败"""
self.logger.info("执行测试: test_failed_login_empty_credentials")
self.login_page.open()
self.login_page.login("", "")
error_message = self.login_page.get_error_message()
self.assertIsNotNone(error_message, "应该显示错误消息")
self.login_page.take_screenshot("empty_credentials")
self.logger.info("空凭据登录测试通过")
def test_login_logout_workflow(self):
"""测试完整的登录退出流程"""
self.logger.info("执行测试: test_login_logout_workflow")
# 登录
self.login_page.open()
self.login_page.login("valid_user", "valid_password")
self.assertTrue(self.dashboard_page.is_loaded())
# 退出
self.dashboard_page.logout()
self.assertTrue(self.login_page.is_login_successful())
self.login_page.take_screenshot("login_logout_workflow")
self.logger.info("登录退出流程测试通过")
def run_tests():
"""运行测试并生成报告"""
# 创建测试套件
test_suite = unittest.TestSuite()
# 添加测试用例
test_suite.addTest(TestLogin('test_successful_login'))
test_suite.addTest(TestLogin('test_failed_login_invalid_credentials'))
test_suite.addTest(TestLogin('test_failed_login_empty_credentials'))
test_suite.addTest(TestLogin('test_login_logout_workflow'))
# 创建测试报告目录
report_dir = "test_reports"
os.makedirs(report_dir, exist_ok=True)
# 生成HTML测试报告
report_file = os.path.join(
report_dir,
f"test_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
)
with open(report_file, 'w', encoding='utf-8') as f:
runner = HTMLTestRunner.HTMLTestRunner(
stream=f,
title='Selenium自动化测试报告',
description='测试登录功能'
)
result = runner.run(test_suite)
print(f"测试报告已生成: {report_file}")
return result
# 自定义HTMLTestRunner(简化版)
class HTMLTestRunner:
"""简单的HTML测试报告生成器"""
def __init__(self, stream, title, description):
self.stream = stream
self.title = title
self.description = description
def run(self, test):
"""运行测试并生成报告"""
# 这里实现简单的HTML报告生成
# 实际项目中可以使用更成熟的测试报告库
result = unittest.TextTestRunner(verbosity=2).run(test)
# 生成简单的HTML报告
html_report = self.generate_html_report(result)
self.stream.write(html_report)
return result
def generate_html_report(self, result):
"""生成HTML报告"""
# 简化的HTML报告实现
return f"""
<html>
<head>
<title>{self.title}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.pass {{ color: green; }}
.fail {{ color: red; }}
.error {{ color: orange; }}
.summary {{ background: #f0f0f0; padding: 10px; margin: 10px 0; }}
</style>
</head>
<body>
<h1>{self.title}</h1>
<p>{self.description}</p>
<div class="summary">
<h2>测试摘要</h2>
<p>运行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>总测试数: {result.testsRun}</p>
<p class="pass">通过: {result.testsRun - len(result.failures) - len(result.errors)}</p>
<p class="fail">失败: {len(result.failures)}</p>
<p class="error">错误: {len(result.errors)}</p>
</div>
</body>
</html>
"""
if __name__ == "__main__":
# 运行测试
test_result = run_tests()
# 输出测试结果摘要
print(f"\n=== 测试结果摘要 ===")
print(f"总测试数: {test_result.testsRun}")
print(f"通过: {test_result.testsRun - len(test_result.failures) - len(test_result.errors)}")
print(f"失败: {len(test_result.failures)}")
print(f"错误: {len(test_result.errors)}")
if test_result.failures:
print(f"\n失败的测试:")
for test, traceback in test_result.failures:
print(f" - {test}: {traceback.splitlines()[-1]}")
if test_result.errors:
print(f"\n错误的测试:")
for test, traceback in test_result.errors:
print(f" - {test}: {traceback.splitlines()[-1]}")
8. 高级技巧和最佳实践
8.1 反检测和反反爬虫策略
class StealthBrowser:
"""隐形浏览器 - 避免被检测为自动化程序"""
def __init__(self, driver):
self.driver = driver
def apply_stealth_techniques(self):
"""应用隐形技术"""
# 1. 移除webdriver属性
self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
# 2. 修改Chrome运行时特性
self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
})
'''
})
# 3. 修改语言和平台
self.driver.execute_cdp_cmd('Emulation.setUserAgentOverride', {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"platform": "Win32"
})
def human_like_behavior(self, element, action="click"):
"""模拟人类行为"""
import random
if action == "click":
# 模拟鼠标移动轨迹
self.simulate_mouse_movement(element)
# 随机延迟
time.sleep(random.uniform(0.1, 0.5))
element.click()
elif action == "type":
# 模拟人类打字速度
text = element.get_attribute("value") or ""
for char in text:
element.send_keys(char)
time.sleep(random.uniform(0.05, 0.2))
def simulate_mouse_movement(self, element):
"""模拟鼠标移动"""
actions = ActionChains(self.driver)
# 获取元素位置
location = element.location
size = element.size
# 计算元素中心点
x = location['x'] + size['width'] / 2
y = location['y'] + size['height'] / 2
# 生成随机移动轨迹
import random
start_x = random.randint(0, 100)
start_y = random.randint(0, 100)
actions.move_by_offset(start_x, start_y)
actions.move_to_element(element)
actions.perform()
def random_delay(self, min_seconds=1, max_seconds=3):
"""随机延迟"""
import random
time.sleep(random.uniform(min_seconds, max_seconds))
def rotate_user_agents(self):
"""轮换User-Agent"""
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
]
import random
user_agent = random.choice(user_agents)
self.driver.execute_cdp_cmd('Network.setUserAgentOverride', {
"userAgent": user_agent
})
8.2 性能优化和资源管理
class PerformanceOptimizer:
"""性能优化器"""
def __init__(self, driver):
self.driver = driver
def enable_performance_logging(self):
"""启用性能日志记录"""
self.driver.execute_cdp_cmd('Performance.enable', {})
def get_performance_metrics(self):
"""获取性能指标"""
try:
metrics = self.driver.execute_cdp_cmd('Performance.getMetrics', {})
return {metric['name']: metric['value'] for metric in metrics['metrics']}
except:
return {}
def optimize_browser_performance(self):
"""优化浏览器性能"""
# 禁用图片加载
self.driver.execute_cdp_cmd('Network.setBlockedURLs', {
"urls": ["*.jpg", "*.jpeg", "*.png", "*.gif"]
})
# 启用网络缓存
self.driver.execute_cdp_cmd('Network.setCacheDisabled', {
"cacheDisabled": False
})
def measure_page_load_time(self, url):
"""测量页面加载时间"""
navigation_start = self.driver.execute_script(
"return window.performance.timing.navigationStart"
)
self.driver.get(url)
load_event_end = self.driver.execute_script(
"return window.performance.timing.loadEventEnd"
)
return (load_event_end - navigation_start) / 1000 # 转换为秒
class ResourceManager:
"""资源管理器"""
@staticmethod
def manage_browser_resources(driver):
"""管理浏览器资源"""
# 清理本地存储
driver.execute_script("window.localStorage.clear();")
driver.execute_script("window.sessionStorage.clear();")
# 删除cookies
driver.delete_all_cookies()
@staticmethod
def memory_usage():
"""监控内存使用"""
import psutil
process = psutil.Process()
return process.memory_info().rss / 1024 / 1024 # MB
9. 完整代码实现
下面是本文中使用的完整代码集合:
"""
Selenium Web自动化完整代码集合
包含爬虫和自动化测试功能
作者: AI助手
日期: 2024年
"""
import os
import time
import logging
import pandas as pd
import json
import unittest
import random
from datetime import datetime
from urllib.parse import urljoin
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.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
# 所有类的定义都在上面各节中提供
# 这里省略重复的代码以节省空间
def main_demo():
"""主演示函数"""
print("=== Selenium Web自动化演示 ===")
print("请选择演示模式:")
print("1. 网页爬虫演示 - 电商价格监控")
print("2. 自动化测试演示 - 登录功能测试")
print("3. 高级功能演示 - 隐形浏览器")
print("4. 退出")
while True:
choice = input("\n请输入选择 (1-4): ").strip()
if choice == '1':
print("\n--- 网页爬虫演示 ---")
demo_price_monitor()
elif choice == '2':
print("\n--- 自动化测试演示 ---")
test_result = run_tests()
print(f"测试完成: {test_result.testsRun} 个测试用例执行完毕")
elif choice == '3':
print("\n--- 高级功能演示 ---")
demo_stealth_browser()
elif choice == '4':
print("谢谢使用!")
break
else:
print("无效选择,请重新输入。")
def demo_stealth_browser():
"""演示隐形浏览器功能"""
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
try:
stealth = StealthBrowser(driver)
stealth.apply_stealth_techniques()
# 测试隐形效果
driver.get("https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html")
# 检查是否被检测为自动化浏览器
is_headless = driver.execute_script("return window.chrome")
print(f"是否被检测为无头浏览器: {is_headless}")
time.sleep(3)
finally:
driver.quit()
if __name__ == "__main__":
main_demo()
10. 代码自查和优化
为确保代码质量和减少BUG,我们对所有代码进行了以下自查:
10.1 代码质量检查
- 异常处理:所有可能失败的操作都包含try-catch异常处理
- 输入验证:对函数参数进行类型和值验证
- 资源管理:确保浏览器驱动、文件句柄等资源正确释放
- 日志记录:详细的日志记录便于调试和问题追踪
- 配置管理:使用配置文件管理浏览器设置、测试数据等
10.2 性能优化
- 智能等待:使用显式等待替代固定sleep,提高执行效率
- 资源清理:及时清理浏览器缓存、cookies等资源
- 连接复用:在可能的情况下复用浏览器会话
- 并行处理:对独立任务使用多线程处理
10.3 健壮性改进
- 重试机制:对网络请求、元素查找等操作添加重试逻辑
- 多种定位策略:为关键元素提供多种定位策略回退
- 环境检测:自动检测和适配不同的运行环境
- 错误恢复:在可能的情况下从错误中恢复并继续执行
10.4 安全性考虑
- 敏感信息保护:密码、API密钥等敏感信息不硬编码在代码中
- 输入清理:对用户输入和外部数据进行清理和验证
- 访问控制:遵守robots.txt和网站使用条款
- 速率限制:添加适当的延迟,避免对目标网站造成压力
11. 总结
通过本文的详细介绍和代码示例,我们全面探讨了使用Selenium和Python进行Web自动化的各个方面。从基础的环境搭建到高级的反检测技巧,从简单的元素操作到复杂的框架设计,Selenium为Web自动化提供了强大的支持。
11.1 主要收获
- 全面的元素定位能力:掌握了8种元素定位策略及其适用场景
- 复杂的交互操作:学会了模拟拖放、悬停、键盘操作等高级交互
- 智能等待机制:理解了各种等待策略的区别和最佳实践
- 健壮的测试框架:学会了基于Page Object模式构建可维护的测试框架
- 高效的数据采集:掌握了使用Selenium进行网页数据采集的技巧
11.2 最佳实践建议
- 选择合适的定位策略:优先使用ID、Name等稳定定位器
- 合理使用等待机制:避免硬性等待,多用显式等待
- 遵循Page Object模式:提高代码的可维护性和复用性
- 添加完善的日志:便于调试和问题排查
- 考虑可扩展性:设计时要考虑未来的需求变化
11.3 应用前景
随着Web技术的不断发展,Web自动化的需求将持续增长。Selenium作为成熟的Web自动化工具,在以下领域有着广泛的应用前景:
- 自动化测试:Web应用的功能测试、回归测试、兼容性测试
- 数据采集:价格监控、内容聚合、竞争情报收集
- 业务流程自动化:自动填表、数据录入、报告生成
- 监控预警:网站可用性监控、内容变更检测
- 性能测试:页面加载时间测量、资源使用分析
通过持续学习和实践,您可以将这些技术应用到实际项目中,显著提高工作效率和软件质量。无论是作为开发人员、测试工程师还是数据分析师,掌握Selenium Web自动化都将为您带来巨大的价值。

浙公网安备 33010602011771号