Python+Selenium自动化D-Link路由器配置备份与恢复实战

Python+Selenium自动化D-Link路由器配置备份与恢复实战

1. 项目概述与核心价值

最近在整理公司网络设备时,发现一个挺头疼的问题:手头几十台D-Link商用路由器,每次需要备份配置或者批量修改策略,都得一台台登录Web界面,手动点“导出配置”,费时费力还容易出错。更麻烦的是,一旦某台设备意外重启或配置丢失,恢复起来也是个手动活,对于运维来说,这种重复性劳动简直是“生命不能承受之重”。于是,我就琢磨着能不能用自动化脚本搞定这件事。

这个项目的核心,就是用Python配合Selenium这个浏览器自动化工具,模拟我们人工登录路由器后台、导航到配置页面、点击备份按钮、下载配置文件这一系列操作,并且还能反向操作,将本地的备份文件自动上传并恢复。听起来好像就是个简单的“按键精灵”,但实际做下来,你会发现里面涉及到网络设备交互的稳定性、Web元素的精准定位、文件上传下载的自动化处理,以及异常流程的健壮性控制,每一个环节都有不少门道。

对于网络管理员、运维工程师,或者任何需要管理多台网络设备的朋友来说,掌握这套自动化方法,价值巨大。它不仅能将你从繁琐的重复操作中解放出来,更重要的是,它能实现配置的版本化管理、定期无人值守备份,以及在故障时快速批量恢复,极大地提升了网络运维的效率和可靠性。即使你不是专业运维,只是家里有几台路由器想统一管理,这套思路也同样适用。接下来,我就把从零搭建这个自动化工具的全过程,包括踩过的坑和总结的技巧,毫无保留地分享给你。

2. 技术选型与工具准备

2.1 为什么是Python + Selenium?

首先得说说为什么选这个技术组合。实现路由器配置的自动备份与恢复,本质上是对设备Web管理界面进行自动化操作。市面上能实现Web自动化的工具不少,比如Playwright、Puppeteer,甚至早期的AutoIt。我选择Selenium,主要基于以下几点考量:

  1. 成熟与稳定:Selenium是Web自动化测试领域的事实标准,经过多年发展,其API稳定,社区资源极其丰富。这意味着你在遇到任何稀奇古怪的页面元素或交互问题时,几乎都能在网上找到解决方案或讨论。
  2. 跨浏览器与语言支持:Selenium支持Chrome、Firefox、Edge等多种浏览器。虽然路由器管理界面通常不挑浏览器,但多一种选择就多一份保障。更重要的是,它支持多种编程语言绑定,Python是其中应用最广泛、生态最完善的一个,这对于快速开发脚本非常友好。
  3. 对动态页面的友好性:很多路由器的Web界面并非纯静态,可能会使用一些JavaScript来实现交互。Selenium能够完整地加载并执行页面中的JS,确保我们操作的元素在页面上是真实存在且可交互的,这一点对于成功模拟人工操作至关重要。
  4. 清晰的元素定位策略:Selenium提供了ID、Name、XPath、CSS Selector等多种定位元素的方式。路由器管理页面的HTML结构虽然可能不标准,但通过组合使用这些定位器,我们总能找到可靠的方法来“抓住”那个备份按钮或文件上传框。

相比之下,像requests库直接发送HTTP请求的方式,对于需要处理登录Session、应对复杂JS渲染、执行文件上传的表单页面来说,逆向和模拟的复杂度会高很多,不如Selenium这种“所见即所得”的模拟方式直观和稳健。

2.2 环境搭建与核心库安装

工欲善其事,必先利其器。我们的开发环境需要以下核心组件:

  1. Python环境:建议使用Python 3.7及以上版本。我习惯用Anaconda来管理环境,但用官方Python安装包配合venv创建虚拟环境也一样。关键是保持环境纯净。

  2. 安装Selenium库:这是我们的核心驱动。打开终端或命令提示符,执行以下命令:

    pip install selenium
  3. 下载浏览器驱动:Selenium需要通过一个名为“WebDriver”的组件来控制浏览器。你需要下载与你电脑上安装的Chrome浏览器版本匹配的chromedriver

    • 查看Chrome版本:在浏览器地址栏输入chrome://settings/help
    • 下载驱动:访问ChromeDriver官网或国内镜像站,下载对应版本的驱动。
    • 放置驱动:将下载的chromedriver.exe文件放在一个固定的目录(如C:\WebDriver\/usr/local/bin/),并将该目录添加到系统的PATH环境变量中。这是最关键的一步,否则Selenium会找不到驱动而报错。

    注意:驱动版本与浏览器版本必须匹配,否则可能会出现无法启动浏览器或各种诡异错误。如果遇到问题,首先检查版本匹配性。

  4. 辅助库:我们可能还会用到time(强制等待)、os(路径操作)、datetime(生成时间戳备份文件名)等Python标准库,它们都是内置的,无需额外安装。

环境准备好后,我们可以写一个最简单的脚本来测试Selenium是否能正常工作:

from selenium import webdriver driver = webdriver.Chrome() # 这会启动一个Chrome浏览器窗口 driver.get("http://www.baidu.com") print(driver.title) # 应该打印出“百度一下,你就知道” driver.quit() # 关闭浏览器

如果这段代码能成功打开百度页面并打印标题,那么恭喜你,基础环境搭建成功。

3. D-Link路由器Web界面分析与元素定位

3.1 登录流程与界面结构

不同型号的D-Link路由器,其Web管理界面可能略有差异,但核心逻辑大同小异。通常的访问地址是http://192.168.0.1http://dlinkrouter.local,默认用户名和密码通常在设备底部的标签上,常见的是admin和空密码。

用Selenium自动化登录,第一步是分析登录页面的HTML结构。我们以常见的D-Link DIR-8xx系列界面为例(实际操作时,请以你设备的实际页面为准)。

  1. 打开页面并等待加载:使用driver.get()打开登录页后,不要立即操作。因为页面加载需要时间,特别是如果路由器性能一般。这里引入第一个重要技巧:使用显式等待(WebDriverWait)

    from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver.get("http://192.168.0.1") # 等待直到“用户名”输入框出现,最多等10秒 username_field = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "username")) # 假设用户名输入框的ID是‘username’ )

    这里By.ID是定位方式,"username"是定位器的值。我们需要通过浏览器的开发者工具(F12)来查看实际的元素ID或Name。

  2. 定位元素并输入:找到输入框后,使用send_keys()方法输入用户名和密码。

    username_field.send_keys("admin") # 同理定位密码框并输入 password_field = driver.find_element(By.ID, "password") password_field.send_keys("your_password")
  3. 定位并点击登录按钮:找到登录按钮(可能是<input type="submit"><button>),并点击。

    login_button = driver.find_element(By.ID, "login_submit") # 同样需要查看实际ID login_button.click()

实操心得:很多路由器的登录表单没有明显的ID,这时就需要用其他定位器。比如,如果用户名输入框有name="login_username"属性,就可以用By.NAME。如果什么都没有,最后的手段是使用By.XPATH。例如,通过XPath找到页面上第二个类型为password的输入框://input[@type='password'][2]强烈建议在浏览器的开发者工具中使用“检查(Inspect)”功能,并尝试右键点击元素 -> Copy -> Copy XPath,来快速获取可用的XPath,但要注意其稳定性。

3.2 导航至配置备份/恢复页面

成功登录后,通常会进入一个主仪表盘或菜单页面。我们需要找到通往“系统工具”、“管理”、“维护”或类似标签下的“备份配置”、“恢复配置”页面的路径。

  1. 分析菜单结构:D-Link的菜单可能是侧边栏导航,也可能是顶部标签页。同样使用开发者工具查看菜单项的链接(<a>标签)或可点击元素的属性。
  2. 点击菜单项:如果菜单项是一个带有href的链接,直接driver.get(完整的URL)可能是最快捷的方式。但很多时候,菜单点击会触发JavaScript来加载内容。这时,我们需要先定位到那个菜单元素(如<li><a>),然后执行.click()
    # 假设“系统工具”菜单的链接文本就是“系统工具” system_tools_menu = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.LINK_TEXT, "系统工具")) ) system_tools_menu.click() # 然后可能在展开的子菜单中再点击“备份配置” backup_item = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.LINK_TEXT, "备份配置")) ) backup_item.click()
    这里用了EC.element_to_be_clickable,它比presence_of_element_located更严格,要求元素不仅存在,还要可点击,避免了元素被遮挡或禁用时误操作。

注意事项:页面跳转或动态加载内容后,之前的元素引用可能会“失效”(StaleElementReferenceException)。安全的做法是,在每次页面发生显著变化(如点击菜单加载新区域)后,重新定位你需要操作的元素。

4. 核心功能实现:自动备份配置

4.1 定位备份按钮与处理文件下载

导航到备份配置页面后,页面上通常会有一个明显的按钮,比如“备份设置”、“保存配置到文件”或“Backup Settings”。我们的目标就是模拟点击这个按钮。

  1. 定位备份按钮:同样使用开发者工具,找到这个按钮的元素。它可能是一个<input type="button">,也可能是一个<button>,或者是一个带有onclick事件的<a>标签。用合适的定位器找到它。
    backup_button = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "backup_button_id")) # 替换为实际ID # 或者用XPath: (By.XPATH, "//button[contains(text(),'备份')]") )
  2. 点击并触发下载:直接backup_button.click()。对于大多数现代浏览器和Selenium,点击一个触发文件下载的链接,文件会默认下载到浏览器预设的下载目录。

这里有一个巨大的坑:如何让Selenium自动处理文件下载,并指定下载路径?

默认情况下,Chrome驱动下载文件会弹出“另存为”对话框,这会导致自动化脚本阻塞。我们必须通过Chrome选项来预先设置下载行为。

from selenium import webdriver from selenium.webdriver.chrome.options import Options # 创建Chrome选项 chrome_options = Options() prefs = { "download.default_directory": r"C:\Router_Backups", # 指定下载目录,请确保路径存在 "download.prompt_for_download": False, # 禁止弹出下载对话框 "download.directory_upgrade": True, "safebrowsing.enabled": True # 可选,安全浏览 } chrome_options.add_experimental_option("prefs", prefs) # 还可以添加一些常用选项,让自动化更稳定 chrome_options.add_argument('--no-sandbox') # 在Linux/Docker中有时需要 chrome_options.add_argument('--disable-dev-shm-usage') # 解决共享内存问题 chrome_options.add_argument('--headless') # 无头模式,不显示浏览器窗口 # 使用配置了选项的驱动 driver = webdriver.Chrome(options=chrome_options)

设置了download.default_directorydownload.prompt_for_downloadFalse后,点击下载链接,文件就会静默下载到指定目录。

4.2 文件命名与存储管理

路由器备份的文件名通常是固定的,比如config.binrouter_config.cfg。如果我们定期备份,新文件会覆盖旧文件。为了保存历史版本,我们需要在下载时重命名文件。

然而,Selenium无法直接控制下载对话框中的“保存”按钮(因为我们禁用了它),也无法在下载时直接重命名。我们的策略是:

  1. 让文件以默认名称下载到指定文件夹。
  2. 在Python脚本中,监听该文件夹,一旦发现新文件(config.bin),就立即将其重命名为我们想要的名称。

这里需要用到ostime库,以及一个简单的文件系统监控逻辑(可以通过对比目录列表变化来实现)。

import os import time import shutil from datetime import datetime def download_and_rename_backup(download_dir, expected_filename="config.bin"): """ 等待文件下载完成并重命名。 download_dir: 下载目录路径 expected_filename: 路由器默认的备份文件名 """ backup_file_path = os.path.join(download_dir, expected_filename) new_filename = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.bin" new_file_path = os.path.join(download_dir, new_filename) # 方法1:简单等待固定时间(不推荐,不可靠) # time.sleep(10) # 假设10秒足够下载 # 方法2:轮询等待文件出现且大小稳定(推荐) max_wait = 30 # 最大等待30秒 interval = 1 # 检查间隔1秒 start_time = time.time() last_size = -1 stable_count = 0 while time.time() - start_time < max_wait: if os.path.exists(backup_file_path): current_size = os.path.getsize(backup_file_path) if current_size == last_size: stable_count += 1 if stable_count >= 2: # 连续2次检查大小不变,认为下载完成 print(f"文件下载完成,大小: {current_size} bytes") # 重命名文件 shutil.move(backup_file_path, new_file_path) print(f"已重命名为: {new_filename}") return new_file_path else: last_size = current_size stable_count = 0 # 大小变化,重置稳定计数器 time.sleep(interval) print("等待下载超时或文件未找到。") return None # 在点击备份按钮后调用 backup_button.click() saved_file = download_and_rename_backup(r"C:\Router_Backups") if saved_file: print(f"备份成功保存至: {saved_file}")

这段代码实现了一个相对稳健的下载完成检测机制,避免了因网络延迟导致文件未下载完就进行重命名操作。

5. 核心功能实现:自动恢复配置

5.1 定位文件上传元素

恢复配置的页面通常会有一个文件选择输入框(<input type="file">)和一个“上传”或“恢复”按钮。

  1. 导航到恢复页面:流程和去备份页面类似。
  2. 定位文件输入框:这是关键。文件上传输入框通常有type="file"属性。
    file_input = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, "//input[@type='file']")) )
    如果页面上有多个type="file"的元素,可能需要用更精确的XPath,比如结合其附近的标签文字://label[contains(text(),'选择文件')]/following-sibling::input[@type='file']

5.2 使用send_keys上传文件

找到了文件输入框元素,上传就非常简单了。千万不要尝试去模拟点击“浏览”按钮然后操作系统文件对话框,那会极其复杂且跨平台兼容性差。Selenium提供了直接向<input type="file">元素发送本地文件路径的方法。

# 假设我们要上传之前备份的某个文件 backup_file_to_restore = r"C:\Router_Backups\backup_20231027_143022.bin" # 将文件的绝对路径发送给文件输入框元素 file_input.send_keys(backup_file_to_restore)

执行完这行代码后,你会发现页面上那个文件选择框旁边已经显示了你传入的文件名,就像你手动点击“浏览”并选择了一样。

重要提示send_keys()传入的必须是文件的绝对路径。相对路径可能会因为Selenium的工作目录问题导致找不到文件。

5.3 定位并点击恢复/上传按钮

文件“选择”好后,页面上会有一个“上传配置”、“恢复设置”或类似的按钮来触发真正的恢复操作。定位并点击它。

restore_button = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "restore_button_id")) # 替换为实际ID或定位器 ) restore_button.click()

5.4 处理恢复确认与等待重启

点击恢复按钮后,路由器通常会做两件事:

  1. 弹出确认对话框:例如“此操作将覆盖当前配置并可能重启设备,是否继续?”。我们需要处理这个JavaScript弹窗(Alert)。
    try: WebDriverWait(driver, 5).until(EC.alert_is_present()) alert = driver.switch_to.alert print(f"弹窗提示: {alert.text}") alert.accept() # 点击“确定”或“OK” # 如果有点击“取消”的需求,用 alert.dismiss() except: print("未出现确认弹窗,或等待超时。")
  2. 设备重启:配置恢复后,路由器很可能会自动重启。这意味着Web会话会中断,页面连接会丢失。我们的脚本必须能妥善处理这个情况。
    • 等待重启完成:在点击确认后,立即开始等待一段时间(例如60-120秒),让设备有足够的时间重启。
    • 重新连接检测:等待结束后,尝试重新访问路由器管理地址,并检查是否能够再次登录,以验证恢复是否成功。
    import time # ... 点击恢复并确认后 ... print("配置已上传,设备正在重启,等待90秒...") time.sleep(90) # 等待重启 # 尝试重新访问 driver.get("http://192.168.0.1") # 可以尝试检查登录页面是否再次出现,或者直接尝试用脚本重新登录 # 这里可以封装一个登录函数来复用
    注意事项:硬编码的sleep时间并不完美,因为不同型号路由器重启时间不同。更高级的做法是写一个循环,定期(比如每5秒)尝试ping路由器的IP地址或访问一个轻量级页面,直到成功响应为止。

6. 脚本健壮性优化与异常处理

一个能用于生产环境的自动化脚本,绝不能是“一次性”的。我们必须考虑各种异常情况,并让脚本能够优雅地处理或记录它们。

6.1 封装核心操作为函数

将登录、导航、备份、恢复等操作封装成独立的函数,提高代码的可读性和复用性。

class DLinkRouterManager: def __init__(self, ip, username, password, download_dir): self.ip = ip self.username = username self.password = password self.download_dir = download_dir self.driver = None self.setup_driver() def setup_driver(self): chrome_options = Options() prefs = {...} # 同上文下载设置 chrome_options.add_experimental_option("prefs", prefs) self.driver = webdriver.Chrome(options=chrome_options) self.driver.implicitly_wait(10) # 设置隐式等待,为所有find_element操作提供等待时间 def login(self): try: self.driver.get(f"http://{self.ip}") # ... 定位和输入登录信息 ... print("登录成功") return True except Exception as e: print(f"登录失败: {e}") self.save_screenshot("login_error.png") return False def backup_configuration(self, backup_name_prefix="backup"): # 导航到备份页面,点击备份,处理下载和重命名 # 返回最终备份文件的路径 pass def restore_configuration(self, config_file_path): # 导航到恢复页面,上传文件,处理确认和重启 pass def save_screenshot(self, filename): """保存当前页面截图,用于调试""" self.driver.save_screenshot(filename) def quit(self): if self.driver: self.driver.quit()

6.2 添加全面的异常处理与日志

使用try...except块包裹可能出错的操作,并记录详细的日志,方便事后排查。

import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('router_manager.log'), logging.StreamHandler()]) logger = logging.getLogger(__name__) def backup_configuration(self, backup_name_prefix="backup"): try: # 导航操作 self._navigate_to_backup_page() # 点击备份按钮 backup_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.ID, "backup_btn")) ) backup_btn.click() logger.info("已触发备份下载。") # 处理文件下载 saved_path = self._wait_for_download_and_rename(backup_name_prefix) if saved_path: logger.info(f"备份成功,文件位于: {saved_path}") return saved_path else: logger.error("备份文件下载或重命名失败。") return None except TimeoutException as e: logger.error(f"在备份过程中等待元素超时: {e}") self.save_screenshot("backup_timeout.png") return None except Exception as e: logger.error(f"备份过程中发生未知错误: {e}", exc_info=True) # exc_info=True会打印堆栈跟踪 self.save_screenshot("backup_error.png") return None

6.3 使用Page Object模式(进阶)

对于更复杂或需要维护多个型号路由器脚本的情况,可以采用Page Object设计模式。将每个页面(登录页、主页面、备份页、恢复页)封装成一个类,类里面包含该页面的元素定位器和操作方法。这样能使测试代码更清晰,元素定位信息更集中,易于维护。

class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.submit_button = (By.ID, "login_submit") def login(self, username, password): WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(self.username_input) ).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.submit_button).click()

7. 实战:构建一个完整的定时备份脚本

将上面的所有模块组合起来,我们就可以创建一个实用的、可以定时运行的自动备份脚本。

7.1 脚本主流程设计

脚本的主要逻辑流程如下:

  1. 初始化路由器管理器(传入IP、账号、密码、下载目录)。
  2. 调用login()方法登录。
  3. 登录成功后,调用backup_configuration()方法进行备份。
  4. 备份成功后,可以选择将备份文件复制到网络存储或云盘(这部分需额外实现)。
  5. 记录本次备份结果(成功/失败)到日志文件。
  6. 退出浏览器驱动。

7.2 加入定时任务

在Windows上,可以使用系统的“任务计划程序”;在Linux/Mac上,可以使用cron。我们只需要让系统在指定时间(例如每天凌晨2点)运行我们的Python脚本即可。

例如,创建一个backup_script.py

# backup_script.py import sys sys.path.append('/path/to/your/modules') # 如果你的类在别的文件 from router_manager import DLinkRouterManager def main(): routers = [ {"ip": "192.168.0.1", "user": "admin", "pass": "password1", "name": "Floor1_Router"}, {"ip": "192.168.1.1", "user": "admin", "pass": "password2", "name": "Floor2_Router"}, ] backup_dir = r"D:\Network_Backups" for router in routers: manager = DLinkRouterManager( ip=router['ip'], username=router['user'], password=router['pass'], download_dir=backup_dir ) try: if manager.login(): backup_file = manager.backup_configuration(backup_name_prefix=router['name']) # 这里可以添加将backup_file上传到FTP/S3等的代码 else: print(f"{router['name']} 登录失败,跳过备份。") except Exception as e: print(f"处理路由器 {router['name']} 时发生错误: {e}") finally: manager.quit() if __name__ == "__main__": main()

然后,在Linux的crontab中添加一行:

0 2 * * * /usr/bin/python3 /path/to/backup_script.py >> /var/log/router_backup.log 2>&1

这样,每天凌晨2点,脚本就会自动运行,备份所有配置好的路由器。

7.3 扩展思考:配置差异对比与版本管理

仅仅备份还不够高级。我们可以进一步,在每次备份后,与上一次的备份文件进行对比,看看配置发生了哪些变化。对于文本格式的配置文件(有些路由器导出的是明文文本),可以使用difflib库来生成差异报告。对于二进制文件(如.bin),可以计算其MD5或SHA256哈希值,如果哈希值变了,说明配置有更新,值得关注。

更进一步,可以将备份文件纳入Git等版本控制系统,每次备份都是一次提交,配合有意义的提交信息(如“2023-10-27 常规备份”或“2023-10-28 修改了DHCP地址池”),就能实现路由器配置的完整版本历史管理,回滚到任意历史版本将变得轻而易举。这需要将备份脚本与Git命令结合,是提升运维水平的一个很棒的方向。