1. 项目概述与核心价值

最近在帮一个做外贸的朋友分析市场,他需要了解阿里巴巴国际站上特定品类(比如“蓝牙耳机”)的商家分布、价格区间和产品趋势。手动一个个去翻,几百上千个商家,看得眼花缭乱不说,数据还容易出错。这种重复、机械的网页信息抓取工作,正是自动化脚本大显身手的地方。于是,我用 Python 配合 Selenium 写了一套自动化采集脚本,不仅能自动登录、翻页、抓取关键字段,还能把数据清洗后直接生成带可视化图表的 Excel 报告。整个过程从手动“人肉”变成了“一键运行”,效率提升了几十倍。

这个项目本质上是一个 Web数据采集(爬虫)与自动化报表生成 的实战案例。它解决的痛点非常明确:从结构化的电商平台页面中,高效、准确、批量化地提取商业信息,并将原始数据转化为直观、可分析的报告。适合谁呢?如果你是外贸从业者、市场分析师、电商运营,或者是对Python自动化感兴趣的开发者,这个项目都能给你提供一套从“打开浏览器”到“产出分析报告”的完整思路和可复现的代码方案。核心关键词就是:Python, Selenium, 自动化采集,阿里巴巴国际站,Excel可视化。

2. 技术选型与整体设计思路

2.1 为什么是 Selenium 而不是 Requests/Scrapy?

提到网页抓取,很多人第一反应是 Requests + BeautifulSoup,或者更专业的 Scrapy 框架。它们效率高、资源占用少,确实是静态页面抓取的首选。但对于阿里巴巴国际站这类大型电商平台,情况就复杂多了。

首先, 页面动态渲染 。国际站的大量商品列表和详情数据是通过 JavaScript 异步加载的,直接拿 Requests 请求到的 HTML 源码,里面很可能没有我们想要的数据,只是一个空的页面骨架。Selenium 的核心优势在于它能驱动一个真实的浏览器(如 Chrome),完整地执行页面上的 JavaScript 代码,等到所有元素都渲染出来后,我们再从中提取信息,这就完美解决了动态加载问题。

其次, 反爬机制应对 。平台为了防爬虫,会设置一些交互验证,比如登录状态维持、复杂的滑块验证码(虽然本方案会尽量规避触发)等。Selenium 模拟的是真人操作浏览器的行为,包括输入账号密码、点击按钮、等待页面跳转,其行为模式比简单的 HTTP 请求更接近真实用户,相对更难被简单的反爬策略直接屏蔽。

最后, 开发与调试便捷性 。Selenium 的每一步操作(打开网页、点击、输入)都对应着浏览器的可视化反馈。哪里出错了,页面卡在哪儿了,一目了然,对于调试复杂的采集流程非常友好。当然,它的缺点也很明显:速度慢、资源消耗大。但对于需要模拟登录、处理动态页面且数据量不是天文数字的商业数据采集任务,Selenium 的利远大于弊。

2.2 工具链全景图

整个项目的工具链可以概括为“采集-处理-输出”三个环节:

  1. 采集层 (Selenium)

    • 浏览器驱动 ChromeDriver GeckoDriver (对应 Firefox)。这是 Selenium 控制浏览器的桥梁。
    • 核心库 selenium 。提供所有浏览器自动化的 API。
    • 等待策略 WebDriverWait expected_conditions 。这是稳定性的关键,用于智能等待页面元素加载完成,避免因网络延迟或脚本执行过快导致的定位失败。
  2. 数据处理层 (Python 核心库)

    • 数据容器 list , dict 。临时存储从网页抓取的结构化数据。
    • 数据清洗 re (正则表达式),用于从杂乱的文本中提取价格、数字等。
    • 结构化存储 pandas 。虽然不是标准库,但它是数据处理的“瑞士军刀”,能将列表字典轻松转为 DataFrame,便于后续分析和输出。
  3. 报告生成层 (Excel与可视化)

    • Excel 操作 openpyxl pandas.ExcelWriter openpyxl 功能更强大,可以精细控制单元格格式、图表插入; pandas 则更简洁,适合快速将 DataFrame 写入表格。
    • 可视化 openpyxl 的图表模块,或者先用 matplotlib 生成图表图片,再插入 Excel。前者更集成,后者更灵活美观。

注意 :关于浏览器选择,我强烈推荐 Chrome。其开发者工具(F12)对前端调试的支持最为完善,能方便地使用“检查”功能复制元素的 CSS Selector 或 XPath,极大提升开发效率。同时,ChromeDriver 的更新和维护也最活跃。

2.3 整体流程架构设计

脚本的运行主线逻辑必须清晰,这决定了代码的健壮性和可维护性。我设计的核心流程如下,它形成了一个闭环:

  1. 初始化与环境启动 :配置浏览器选项(如无头模式、禁用图片加载以加速),启动 WebDriver。
  2. 登录与导航 :访问国际站登录页,注入已保存的 Cookies 或模拟输入完成登录,然后跳转到目标搜索列表页(例如: https://www.alibaba.com/trade/search?SearchText=bluetooth+earphone )。
  3. 列表页遍历与数据提取
    • 使用 WebDriverWait 等待商品列表容器加载。
    • 在单页内,通过定位器获取所有商品卡片元素。
    • 对每个卡片元素,提取核心字段:商家名称、商品标题、价格、最小起订量(MOQ)、成交额/评价、商品详情页链接等。
    • 实现自动翻页逻辑,循环直至抓取指定页数或所有页面。
  4. 数据清洗与存储 :将每页抓取的原始数据(通常是字符串列表)进行清洗(如去除货币符号、统一单位),并追加到一个总的列表或 pandas DataFrame 中。
  5. 深入详情页采集(可选) :如果需要更详细的信息(如商品描述、规格参数、更多图片),可以遍历上一步存储的商品链接,逐个打开详情页进行二次提取。这一步速度较慢,需谨慎评估需求。
  6. 生成 Excel 可视化报告
    • 使用 pandas 将清洗后的 DataFrame 写入 Excel 的一个工作表。
    • 利用 pandas 的内置统计功能或结合 openpyxl,在 Excel 中创建新的工作表,生成图表,例如:价格分布直方图、商家数量 Top 10 柱状图、价格与最小起订量散点图等。
    • 调整表格样式,冻结首行,设置合适的列宽。
  7. 资源释放 :关闭浏览器驱动,结束脚本。

这个设计将复杂的任务模块化,每个环节相对独立,便于调试和后期扩展(例如,增加代理IP池、更换提取字段等)。

3. 核心细节解析与实操要点

3.1 Selenium 定位策略与稳健性保障

定位页面元素是 Selenium 一切操作的基础。国际站的页面结构可能会变,因此选择 稳健、不易失效的定位器 至关重要。

  • 首选 CSS Selector :在绝大多数情况下,CSS Selector 是性能最好、可读性最佳的定位方式。可以通过浏览器开发者工具,右键点击元素选择“Copy” -> “Copy selector”快速获取。例如,商品标题可能类似 .title a
  • 慎用 XPath :XPath 功能强大,但绝对路径(如 /html/body/div[5]/div[2]/div[1]/a )极其脆弱,页面结构微调就会失效。应尽量使用包含属性或文本的相对路径,例如 //a[contains(@class, ‘title’)] //div[text()=“具体文本”]
  • 组合定位与等待 :不要指望一次 find_element 就能成功。必须与 显式等待 WebDriverWait 结合使用。这是避免 NoSuchElementException 错误的核心技巧。
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 错误示范:直接定位,网络慢时必失败
# product_list = driver.find_elements(By.CSS_SELECTOR, ‘.list-item’)

# 正确示范:等待元素群出现后再定位
wait = WebDriverWait(driver, 10) # 最多等10秒
product_container = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ‘.list-container’)))
product_list = product_container.find_elements(By.CSS_SELECTOR, ‘.list-item’)

实操心得 :国际站同一类元素的 CSS 类名可能包含随机哈希值(如 class=“j-random123” )。这时不要硬编码类名,而是寻找其父级或子级具有稳定特征的容器进行定位,或者使用 contains 功能的 CSS 选择器,例如 div[class*=“title”] 来匹配类名中包含 “title” 的 div。

3.2 数据提取的精确性与异常处理

从元素对象中提取文本看似简单,但暗藏玄机。

  • 文本提取与清洗 element.text 返回的是可见文本。对于价格 “US $1.99 - 2.50 / Piece”,你需要用正则表达式或字符串方法清洗出数字部分。注意,有些信息可能在属性里,如 href data-* 属性,需要用 element.get_attribute(‘href’) 获取。
import re

price_element = product.find_element(By.CSS_SELECTOR, ‘.price’)
price_text = price_element.text
# 使用正则表达式提取主要价格数字
price_match = re.search(r‘[\d.,]+’, price_text)
price = float(price_match.group().replace(‘,’, ‘’)) if price_match else None
  • 关键字段的容错处理 :不是每个商品卡片都有所有字段。比如,有些新商家可能没有“成交额”信息。你的代码必须能优雅地处理这种缺失,否则一个元素找不到,整个循环就会崩溃。
data = {}
try:
    data[‘title’] = product.find_element(By.CSS_SELECTOR, ‘.title’).text
except:
    data[‘title’] = ‘N/A’ # 或 None

try:
    moq_text = product.find_element(By.CSS_SELECTOR, ‘.moq’).text
    data[‘moq’] = int(re.search(r‘\d+’, moq_text).group())
except:
    data[‘moq’] = None

注意事项 :过度频繁的请求会触发国际站的反爬,可能弹出验证码或直接封锁IP。解决方案包括:在循环中增加随机延时( time.sleep(random.uniform(1, 3)) )、使用代理IP池、以及最关键的——控制采集速度和总量,模拟人类浏览的节奏。

3.3 高效翻页与循环控制

自动翻页是扩大采集范围的关键。需要分析国际站翻页组件的特性。

  • “下一页”按钮定位 :最常见的模式是点击“下一页”按钮或特定的页码。同样需要结合等待。
def go_to_next_page(driver):
    try:
        next_button = WebDriverWait(driver, 5).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ‘.next’))
        )
        next_button.click()
        # 点击后等待新页面加载
        WebDriverWait(driver, 10).until(
            EC.staleness_of(product_list[0]) # 等待旧列表的第一个元素失效
        )
        return True
    except Exception as e:
        print(f“翻页失败或已是最后一页: {e}”)
        return False
  • URL 参数翻页 :有些网站翻页是通过改变 URL 参数(如 &page=2 )实现的。直接构造URL并 driver.get(new_url) 可能更简单高效,但要注意跳转后页面状态的初始化。
  • 循环终止条件 :必须设置明确的终止条件,防止无限循环。可以是指定最大页数( max_pages ),或者当“下一页”按钮变为不可点击/消失时退出。

4. 实操过程与核心环节实现

4.1 环境搭建与初始化配置

首先,确保你的 Python 环境(建议 3.8+)已就绪。然后通过 pip 安装必要的库:

pip install selenium pandas openpyxl

接下来是下载与 Chrome 浏览器版本匹配的 ChromeDriver 。这是最容易出错的一步。务必检查你本地 Chrome 的版本(在浏览器地址栏输入 chrome://version/ 查看),然后去官方仓库或国内镜像站下载对应版本号的驱动。将下载的 chromedriver.exe (Windows)放在项目目录下,或将其路径添加到系统环境变量中。

启动浏览器时,进行一些优化配置可以提升采集效率和稳定性:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

def init_driver(headless=False):
    chrome_options = Options()
    if headless:
        chrome_options.add_argument(‘--headless’) # 无头模式,不显示GUI
    chrome_options.add_argument(‘--disable-gpu’) # 禁用GPU,某些系统需要
    chrome_options.add_argument(‘--no-sandbox’) # 解决Linux下的某些问题
    chrome_options.add_argument(‘--disable-dev-shm-usage’) # 解决共享内存问题
    chrome_options.add_argument(‘--window-size=1920,1080’) # 设置窗口大小
    # 禁用图片加载,大幅提升页面加载速度
    prefs = {“profile.managed_default_content_settings.images”: 2}
    chrome_options.add_experimental_option(“prefs”, prefs)
    # 可选:添加代理
    # chrome_options.add_argument(‘--proxy-server=http://your-proxy:port’)

    driver = webdriver.Chrome(options=chrome_options)
    # 设置页面加载超时和脚本超时
    driver.set_page_load_timeout(30)
    driver.set_script_timeout(30)
    return driver

提示 :开发调试阶段建议关闭 headless 模式,便于观察。正式运行时开启可节省资源。禁用图片加载对数据采集任务无影响,却能极大加快速度。

4.2 模拟登录与状态维持

直接处理国际站的登录表单(输入用户名密码)比较复杂,且容易触发额外的安全验证。更实用的方法是 使用已保存的 Cookies 进行登录状态恢复

  1. 手动获取 Cookies :先用浏览器正常登录阿里巴巴国际站。
  2. 导出 Cookies :在开发者工具(F12)的 Console 选项卡中,输入 document.cookie 并回车,复制输出的整个字符串。或者使用浏览器插件导出为 JSON 文件。
  3. 脚本加载 Cookies
def login_with_cookies(driver, cookie_str):
    driver.get(“https://www.alibaba.com”) # 先访问首页
    # 清除旧cookies(如果有)
    driver.delete_all_cookies()
    # 将cookie字符串解析为字典并添加
    # 注意:这里简化处理,实际cookie_str可能需要按分号分割
    for cookie in cookie_str.split(‘; ‘):
        if ‘=’ in cookie:
            name, value = cookie.split(‘=’, 1)
            driver.add_cookie({‘name’: name.strip(), ‘value’: value.strip(), ‘domain’: ‘.alibaba.com’})
    # 刷新页面,使cookies生效
    driver.refresh()
    # 验证是否登录成功,例如检查右上角是否显示用户名
    try:
        WebDriverWait(driver, 5).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ‘.user-account’))
        )
        print(“登录成功!”)
        return True
    except:
        print(“登录状态恢复失败,请检查cookies。”)
        return False

这种方法避免了在脚本中硬编码密码,也更安全。但 Cookies 会过期,需要定期更新。

4.3 列表页数据采集核心循环

这是脚本的“心脏”。我们以搜索“bluetooth earphone”为例。

def scrape_product_list(driver, keyword, max_pages=5):
    base_url = f“https://www.alibaba.com/trade/search?SearchText={keyword}”
    driver.get(base_url)
    all_products_data = []

    current_page = 1
    while current_page <= max_pages:
        print(f“正在采集第 {current_page} 页...”)
        time.sleep(random.uniform(2, 4)) # 随机延时,模拟人工

        # 1. 等待商品列表加载
        try:
            wait = WebDriverWait(driver, 15)
            list_container = wait.until(
                EC.presence_of_element_located((By.CSS_SELECTOR, ‘.organic-list’)) # 需根据实际页面调整
            )
        except:
            print(“列表容器加载超时,可能页面结构已变或网络问题。”)
            break

        # 2. 定位当前页所有商品卡片
        product_elements = list_container.find_elements(By.CSS_SELECTOR, ‘.organic-card’) # 需调整

        if not product_elements:
            print(“未找到商品卡片,可能已无数据或定位器失效。”)
            break

        # 3. 遍历每个卡片提取数据
        for idx, product in enumerate(product_elements):
            # 使用 try-except 包裹每个商品的提取过程,防止单个失败影响整体
            product_data = extract_single_product_data(product)
            if product_data: # 如果提取到有效数据
                all_products_data.append(product_data)

        # 4. 尝试翻页
        if not go_to_next_page(driver): # 调用前面定义的翻页函数
            print(“已到达最后一页或翻页失败,停止采集。”)
            break

        current_page += 1

    print(f“采集结束,共获取 {len(all_products_data)} 条商品数据。”)
    return all_products_data

def extract_single_product_data(product_element):
    “”“从单个商品卡片元素中提取数据,并做基础清洗”“”
    data = {}
    try:
        # 商品标题
        title_elem = product_element.find_element(By.CSS_SELECTOR, ‘.title a’)
        data[‘title’] = title_elem.text.strip()
        data[‘product_url’] = title_elem.get_attribute(‘href’)
    except:
        data[‘title’] = ‘N/A’
        data[‘product_url’] = ‘N/A’

    try:
        # 价格 - 这里逻辑需要根据实际页面展示调整,可能有多段价格
        price_elem = product_element.find_element(By.CSS_SELECTOR, ‘.price’)
        price_text = price_elem.text
        # 示例:提取第一个出现的价格数字
        match = re.search(r‘[\d.,]+’, price_text)
        data[‘price’] = float(match.group().replace(‘,’, ‘’)) if match else None
        data[‘price_unit’] = ‘USD’ # 根据实际情况判断
    except:
        data[‘price’] = None
        data[‘price_unit’] = ‘N/A’

    try:
        # 最小起订量 (MOQ)
        moq_elem = product_element.find_element(By.CSS_SELECTOR, ‘.moq’)
        moq_text = moq_elem.text
        moq_match = re.search(r‘(\d+)\s*[Pp]ieces?’, moq_text) # 匹配 “100 Pieces”
        data[‘moq’] = int(moq_match.group(1)) if moq_match else None
    except:
        data[‘moq’] = None

    try:
        # 商家名称
        supplier_elem = product_element.find_element(By.CSS_SELECTOR, ‘.company-name’)
        data[‘supplier’] = supplier_elem.text.strip()
    except:
        data[‘supplier’] = ‘N/A’

    # 可以继续添加其他字段,如评价、响应率等
    data[‘scraped_time’] = datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)
    return data

这个循环结构清晰,包含了等待、提取、翻页和异常处理的核心逻辑。你需要根据国际站实际的 HTML 结构,调整 organic-list organic-card 等 CSS 选择器。

4.4 使用 Pandas 进行数据清洗与整合

采集到的原始数据是字典列表,用 pandas 处理起来非常方便。

import pandas as pd

def clean_and_analyze_data(product_data_list):
    # 1. 转换为DataFrame
    df = pd.DataFrame(product_data_list)

    # 2. 查看基础信息
    print(df.info())
    print(df.head())

    # 3. 数据清洗示例
    # 去除标题完全为‘N/A’或空值的行
    df = df[df[‘title’] != ‘N/A’]
    df = df.dropna(subset=[‘title’])
    # 价格处理:将None替换为NaN,方便pandas统计
    df[‘price’] = pd.to_numeric(df[‘price’], errors=‘coerce’)
    # 去除价格异常(如为0或极高)的样本,根据业务逻辑设定阈值
    df = df[(df[‘price’] > 0.1) & (df[‘price’] < 10000)]

    # 4. 基础分析
    if not df.empty:
        print(f“有效数据共 {len(df)} 条。”)
        print(f“平均价格: {df[‘price’].mean():.2f} {df[‘price_unit’].iloc[0]}”)
        print(f“价格中位数: {df[‘price’].median():.2f}”)
        print(f“共有 {df[‘supplier’].nunique()} 个不同的供应商。”)
        # 统计各供应商商品数量
        supplier_counts = df[‘supplier’].value_counts().head(10)
        print(“Top 10 供应商商品数量:”)
        print(supplier_counts)

    return df

pandas 的强大之处在于,几行代码就能完成过滤、类型转换和统计分析,为后续生成报告打下基础。

4.5 生成带图表的 Excel 报告

这是项目的“交付物”环节。我们将使用 openpyxl 来创建格式更丰富的报告。

from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference, Series
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter

def generate_excel_report(df, output_filename=‘alibaba_report.xlsx’):
    if df.empty:
        print(“没有有效数据,无法生成报告。”)
        return

    # 1. 创建Workbook和Sheet
    wb = Workbook()
    ws_data = wb.active
    ws_data.title = “原始数据”

    # 2. 将DataFrame写入工作表
    # 写入表头
    for col_idx, column_name in enumerate(df.columns, start=1):
        cell = ws_data.cell(row=1, column=col_idx, value=column_name)
        cell.font = Font(bold=True)
        cell.alignment = Alignment(horizontal=‘center’)
        # 设置边框
        thin_border = Border(left=Side(style=‘thin’), right=Side(style=‘thin’),
                             top=Side(style=‘thin’), bottom=Side(style=‘thin’))
        cell.border = thin_border

    # 写入数据行
    for row_idx, row in enumerate(df.itertuples(index=False), start=2):
        for col_idx, value in enumerate(row, start=1):
            cell = ws_data.cell(row=row_idx, column=col_idx, value=value)
            cell.border = thin_border

    # 自动调整列宽(近似)
    for column in ws_data.columns:
        max_length = 0
        column_letter = get_column_letter(column[0].column)
        for cell in column:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass
        adjusted_width = min(max_length + 2, 50) # 设置最大宽度
        ws_data.column_dimensions[column_letter].width = adjusted_width

    # 3. 创建分析图表工作表
    ws_chart = wb.create_sheet(title=“分析图表”)

    # 示例1:生成价格分布直方图(需要先计算频数)
    # 这里简化,假设我们已经在df中计算好了价格区间
    # 实际中可能需要用 pandas.cut 来分箱
    price_bins = pd.cut(df[‘price’], bins=10).value_counts().sort_index()
    ws_chart[‘A1’] = ‘价格区间’
    ws_chart[‘B1’] = ‘商品数量’
    for i, (bin_name, count) in enumerate(price_bins.items(), start=2):
        ws_chart.cell(row=i, column=1, value=str(bin_name))
        ws_chart.cell(row=i, column=2, value=count)

    chart1 = BarChart()
    chart1.type = “col”
    chart1.style = 10
    chart1.title = “商品价格分布”
    chart1.y_axis.title = ‘商品数量’
    chart1.x_axis.title = ‘价格区间’

    data = Reference(ws_chart, min_col=2, min_row=1, max_row=len(price_bins)+1, max_col=2)
    cats = Reference(ws_chart, min_col=1, min_row=2, max_row=len(price_bins)+1)
    chart1.add_data(data, titles_from_data=True)
    chart1.set_categories(cats)
    ws_chart.add_chart(chart1, “D2”)

    # 示例2:Top 10 供应商柱状图
    supplier_top10 = df[‘supplier’].value_counts().head(10)
    ws_chart[‘A20’] = ‘供应商’
    ws_chart[‘B20’] = ‘商品数量’
    for i, (supplier, count) in enumerate(supplier_top10.items(), start=21):
        ws_chart.cell(row=i, column=1, value=supplier)
        ws_chart.cell(row=i, column=2, value=count)

    chart2 = BarChart()
    chart2.type = “col”
    chart2.style = 10
    chart2.title = “Top 10 供应商商品数量”
    chart2.y_axis.title = ‘商品数量’
    data2 = Reference(ws_chart, min_col=2, min_row=20, max_row=20+len(supplier_top10), max_col=2)
    cats2 = Reference(ws_chart, min_col=1, min_row=21, max_row=20+len(supplier_top10))
    chart2.add_data(data2, titles_from_data=True)
    chart2.set_categories(cats2)
    ws_chart.add_chart(chart2, “D20”)

    # 4. 保存文件
    wb.save(output_filename)
    print(f“报告已生成: {output_filename}”)

这段代码创建了一个包含原始数据表和两个基本图表的 Excel 文件。 openpyxl 的图表 API 功能丰富,你可以根据需要创建折线图、散点图等,来展示价格与起订量的关系等更深入的洞察。

5. 常见问题与排查技巧实录

在实际操作中,你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方案。

5.1 元素定位失败 (NoSuchElementException)

这是最常遇到的问题,没有之一。

  • 可能原因1:页面未加载完成
    • 排查 :在 find_element 前添加 time.sleep(5) 看是否成功。如果成功,就是等待问题。
    • 解决 :将 time.sleep 替换为针对特定元素的 显式等待 WebDriverWait ,如前文所述。这是最佳实践。
  • 可能原因2:定位器(Selector/XPath)失效
    • 排查 :在浏览器的开发者工具 Elements 面板中,使用 Ctrl+F ,输入你的定位器,看是否能高亮匹配到元素。如果匹配不到或匹配到多个,定位器就有问题。
    • 解决 :重新检查元素属性。优先使用唯一的 id ,其次是用稳定的 class data-* 属性组合的 CSS Selector。避免使用绝对 XPath。
  • 可能原因3:元素在 iframe 或 Shadow DOM 内
    • 排查 :在开发者工具中检查目标元素的上级结构,看是否被 <iframe> #shadow-root 包裹。
    • 解决 :对于 iframe,需要使用 driver.switch_to.frame(frame_element_or_name) 切换到对应的 frame 后再定位。对于 Shadow DOM,需要使用 driver.execute_script 执行 JavaScript 来穿透。
  • 可能原因4:页面结构已更新
    • 排查 :对比你写脚本时的页面和当前页面结构。
    • 解决 :更新你的定位器。考虑使用更“模糊”但更稳定的定位方式,比如通过部分文本内容 ( contains(text()) 在 XPath 中) 或属性值 ( [class*=“part-of-class”] 在 CSS 中)。

5.2 被网站检测到自动化脚本

表现为弹出验证码、登录失败、数据返回为空或跳转到异常页面。

  • 可能原因 :Selenium 驱动浏览器会留下一些自动化特征(如 navigator.webdriver=true )。
  • 解决策略(组合使用效果更佳)
    1. 添加实验性选项 :在启动 Chrome 时加入 chrome_options.add_experimental_option(“excludeSwitches”, [“enable-automation”]) chrome_options.add_experimental_option(‘useAutomationExtension’, False)
    2. 执行 CDP 命令 :使用 driver.execute_cdp_cmd(‘Page.addScriptToEvaluateOnNewDocument’, {‘source’: ‘Object.defineProperty(navigator, “webdriver”, {get: () => undefined})’}) 来隐藏 webdriver 属性。这需要在 driver.get() 之前执行。
    3. 模拟人类行为 :在操作之间加入随机延时和随机鼠标移动轨迹(可使用 ActionChains 轻微移动)。
    4. 使用更隐蔽的工具 :如果上述方法无效,可以考虑使用 undetected-chromedriver 这类专门修改过的驱动,或者转向 Playwright ,其默认的自动化特征更少。
    5. 终极方案——降低频率 :这是最有效的方法。大幅增加请求间隔,模拟真人浏览速度。不要试图在几分钟内抓取成千上万条数据。

5.3 翻页后数据重复或丢失

  • 可能原因 :翻页后没有等待新内容加载完成就开始采集,导致采集到的还是旧页面的数据,或者页面是 AJAX 加载,URL 没变。
  • 解决
    • 等待元素刷新 :如前文代码所示,使用 EC.staleness_of 等待旧页面的某个标志性元素失效,或者 EC.presence_of_element_located 等待新页面的某个标志性元素出现。
    • 检查翻页机制 :确认点击“下一页”后,是页面跳转(URL 改变)还是 AJAX 加载(URL 不变)。对于 AJAX 加载,可能需要等待特定的网络请求完成或数据包更新。

5.4 数据提取不准确或混乱

  • 可能原因 :页面信息展示格式不统一,例如价格有时是 “$1.99”,有时是 “From $2.50”,有时是范围 “$1.99 - $2.50”。
  • 解决
    • 加强数据清洗逻辑 :编写更健壮的正则表达式或字符串处理函数来应对多种格式。例如,先判断字符串是否包含 “-“,然后分别处理。
    • 提取多个备选字段 :如果可能,尝试从不同的 HTML 元素或属性中提取同一信息,选择最可靠的一个。
    • 人工抽样核对 :定期打印几条提取出来的原始数据,与浏览器中肉眼看到的数据进行比对,确保清洗逻辑正确。

5.5 脚本运行速度慢

Selenium 本身就不快,但我们可以优化。

  • 禁用图片和 CSS :如前文配置所示,这是提速最有效的方法。
  • 使用无头模式 :节省 GUI 渲染开销。
  • 减少不必要的等待 :用精准的显式等待替代固定的 time.sleep
  • 并行化考虑(高级) :对于海量数据采集,可以考虑使用多线程或多进程,每个进程驱动一个浏览器实例处理不同的搜索词或页码区间。但这会大幅增加复杂度和资源消耗,且更容易触发反爬,需谨慎使用。

最后一点心得 :Web 自动化采集是一个“道高一尺,魔高一丈”的领域。国际站这类平台的前端工程师也在不断更新反爬策略。因此,你的脚本需要有良好的日志记录功能(记录成功、失败、当前页码),并且设计上要易于维护和调整定位器。将配置信息(如选择器、等待时间、URL)放在脚本开头的常量或配置文件中,而不是硬编码在逻辑里,这样当网站改版时,你只需要修改一个地方,而不是满世界找代码。这个项目最有价值的部分,不是那几百行代码,而是这套应对动态网站、处理异常、优化性能的完整方法论。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐